Account Options

  1. Sign in
The old Google Groups will be going away soon, but your browser is incompatible with the new version.
Google Groups Home
« Groups Home
Message from discussion bdev: Add verification for file storage paths

Received: by 10.180.94.98 with SMTP id db2mr5767514wib.2.1351000475167;
        Tue, 23 Oct 2012 06:54:35 -0700 (PDT)
X-BeenThere: ganeti-devel@googlegroups.com
Received: by 10.180.90.134 with SMTP id bw6ls11043830wib.1.canary; Tue, 23 Oct
 2012 06:54:33 -0700 (PDT)
Received: by 10.180.84.234 with SMTP id c10mr3601898wiz.4.1351000473185;
        Tue, 23 Oct 2012 06:54:33 -0700 (PDT)
Received: by 10.180.84.234 with SMTP id c10mr3601896wiz.4.1351000473174;
        Tue, 23 Oct 2012 06:54:33 -0700 (PDT)
Return-Path: <han...@google.com>
Received: from mail-wi0-f202.google.com (mail-wi0-f202.google.com [209.85.212.202])
        by gmr-mx.google.com with ESMTPS id bu8si1329874wib.2.2012.10.23.06.54.33
        (version=TLSv1/SSLv3 cipher=OTHER);
        Tue, 23 Oct 2012 06:54:33 -0700 (PDT)
Received-SPF: pass (google.com: domain of han...@google.com designates 209.85.212.202 as permitted sender) client-ip=209.85.212.202;
Authentication-Results: gmr-mx.google.com; spf=pass (google.com: domain of han...@google.com designates 209.85.212.202 as permitted sender) smtp.mail=han...@google.com; dkim=pass header...@google.com
Received: by mail-wi0-f202.google.com with SMTP id hr7so216763wib.3
        for <ganeti-devel@googlegroups.com>; Tue, 23 Oct 2012 06:54:33 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=google.com; s=20120113;
        h=from:to:subject:date:message-id:x-mailer:in-reply-to:references
         :mime-version:content-type:content-transfer-encoding;
        bh=pVKL5UWLI7HhtSMZ+qozXP0EJAg1ZlfySXjSFJzv+aI=;
        b=m2inxyo9PBp4jZLIkQXwj2Hftksa/QFZG0yH8oTMOLqIY5gwub5xFljxA7IWrKo5dF
         1D9hQsCHt0NKsm3EXIMFJEP3Rr7LESxNe3D5D5fal+Nu0R97BjezObtKYWUcK8KQfeYl
         3WgC9vxj75eE65f8ar/f2ttjd0wTz5bQFim4eReqZMX8Sfak34k3bI3F97SVGB+JmITt
         Jf+/sqBClL7JDh/1I5i+p5qfJs49HmDdu+4ldRhor0M8hsABSAHZmw3bOUHokRFObObm
         weY0n0r4HF6b5R3LF0/TYuiP/d0NbcWAc6L1AZyC8F4beocOG8SdlBI6jlOmV2+VLV5f
         PvQA==
        d=google.com; s=20120113;
        h=from:to:subject:date:message-id:x-mailer:in-reply-to:references
         :mime-version:content-type:content-transfer-encoding
         :x-gm-message-state;
        bh=pVKL5UWLI7HhtSMZ+qozXP0EJAg1ZlfySXjSFJzv+aI=;
        b=pXISC58Hp0223souJUb0WAjevnacRFxN75tXvSdLb8smwG3eivF6C0mtbYyJJX/l0V
         HwI96Sot5i1Ep/J8af8kORlRcJzyckMnyxY+SKOin8EW37Rd42VDY4x4d0Mg3uA4OyxT
         fCpu2kn6uKGpX/kImeSC8OKB4xG3fUZ20ic3wsMB5PR0n68WhBig1Js7N0fpZttf+XS9
         8pTcAxAgVs4a+3vEy3Ay97mwkaBAAiO24jUrgdrOs/28zvfvUtt1RK/xHEuzV5SoRuOt
         nTADbF6ImgsWZAx8DKpqTgj3o++1xzbvu0lkQ2tQJyHzOI9EDblEZQgbqlAp1ZlmaIAq
         zPIg==
Received: by 10.216.202.93 with SMTP id c71mr785583weo.3.1351000473013;
        Tue, 23 Oct 2012 06:54:33 -0700 (PDT)
Return-Path: <han...@google.com>
Received: from hpza9.eem.corp.google.com ([74.125.121.33])
        by gmr-mx.google.com with ESMTPS id z7si123079wiw.1.2012.10.23.06.54.32
        (version=TLSv1/SSLv3 cipher=AES128-SHA);
        Tue, 23 Oct 2012 06:54:33 -0700 (PDT)
Received: from hpgntaa-ubiq38.eem.corp.google.com (hpgntaa-ubiq38.eem.corp.google.com [172.25.129.80])
	by hpza9.eem.corp.google.com (Postfix) with ESMTP id BDDA25C0050
	for <ganeti-devel@googlegroups.com>; Tue, 23 Oct 2012 06:54:32 -0700 (PDT)
Received: by hpgntaa-ubiq38.eem.corp.google.com (Postfix, from userid 55155)
	id 820F3A3992; Tue, 23 Oct 2012 15:54:32 +0200 (CEST)
From: Michael Hanselmann <han...@google.com>
To: ganeti-devel@googlegroups.com
Subject: =?UTF-8?q?=5BPATCH=20master=204=C2=BD/8=5D=20bdev=3A=20Add=20verification=20for=20file=20storage=20paths?=
Date: Tue, 23 Oct 2012 15:54:32 +0200
Message-Id: <d09977f3bc3104e4eff70c26fb647eeb81746b99.1351000441.git.han...@google.com>
X-Mailer: git-send-email 1.7.7.3
In-Reply-To: <CAEsTj1tnpCD_7Cp=vK+iyEO4ztA9bCFKf6Ps8HKRwn9=4Az...@mail.gmail.com>
References: <CAEsTj1tnpCD_7Cp=vK+iyEO4ztA9bCFKf6Ps8HKRwn9=4Az...@mail.gmail.com>
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
X-Gm-Message-State: ALoCoQlDqvw4LCeWupYpb/HM/eh/aNPk5HYXVCEv7uHTNthkydmsgmZOiRgjfQRvFUmjHO9oCOudOKl7Rnh+V6o0scbk567yUnpMolmemZQjRj0xATi1CVtPB0LavMstr5ezVgdcxSWN9b3/7cr0kdBXwxJr00sFzhHnQpuDkubxSDVTQXQ4C3Vn6JoyILUUK03bbTZE4HBwIUWqbrDxiJQnTo4AyrKczrZUc/bBTT/36mqznoXPvuQ=

An earlier version of this patch series verified all paths in cmdlib in
the master daemon. With this change all that verification code is moved
to bdev to run inside the node daemon. The checks are much stricter
now--it is no longer possible to use forbidden paths (e.g. /bin) to
manipulate file storage devices (once these checks are being used).
---
 lib/backend.py               |    1 +
 lib/bdev.py                  |   63 ++++++++++++++++++++++++++++++++-
 test/ganeti.bdev_unittest.py |   79 +++++++++++++++++++++++++++++++++++++++--
 3 files changed, 137 insertions(+), 6 deletions(-)

diff --git a/lib/backend.py b/lib/backend.py
index 032547d..6da70d6 100644
--- a/lib/backend.py
+++ b/lib/backend.py
@@ -800,6 +800,7 @@ def VerifyNode(what, cluster_name):
     result[constants.NV_BRIDGES] = [bridge
                                     for bridge in what[constants.NV_BRIDGES]
                                     if not utils.BridgeExists(bridge)]
+
   return result
 
 
diff --git a/lib/bdev.py b/lib/bdev.py
index 8d41f53..487298d 100644
--- a/lib/bdev.py
+++ b/lib/bdev.py
@@ -29,6 +29,7 @@ import stat
 import pyparsing as pyp
 import os
 import logging
+import itertools
 
 from ganeti import utils
 from ganeti import errors
@@ -100,6 +101,58 @@ def _CanReadDevice(path):
     return False
 
 
+def _GetForbiddenFileStoragePaths():
+  """Builds a list of path prefixes which shouldn't be used for file storage.
+
+  @rtype: frozenset
+
+  """
+  paths = set([
+    "/boot",
+    "/dev",
+    "/etc",
+    "/home",
+    "/proc",
+    "/root",
+    "/sys",
+    ])
+
+  for prefix in ["", "/usr", "/usr/local"]:
+    paths.update(map(lambda s: "%s/%s" % (prefix, s),
+                     ["bin", "lib", "lib32", "lib64", "sbin"]))
+
+  return frozenset(map(os.path.normpath, paths))
+
+
+def _ComputeWrongFileStoragePaths(paths,
+                                  _forbidden=_GetForbiddenFileStoragePaths()):
+  """Cross-checks a list of paths for prefixes considered bad.
+
+  Some paths, e.g. "/bin", should not be used for file storage.
+
+  @type paths: list
+  @param paths: List of paths to be checked
+  @rtype: list
+  @return: Sorted list of paths for which the user should be warned
+
+  """
+  def _Check(path):
+    return (not os.path.isabs(path) or
+            path in _forbidden or
+            filter(lambda p: utils.IsBelowDir(p, path), _forbidden))
+
+  return utils.NiceSort(filter(_Check, map(os.path.normpath, paths)))
+
+
+def ComputeWrongFileStoragePaths(_filename=pathutils.FILE_STORAGE_PATHS_FILE):
+  """Returns a list of file storage paths whose prefix is considered bad.
+
+  See L{_VerifyFileStoragePaths}.
+
+  """
+  return _ComputeWrongFileStoragePaths(_LoadAllowedFileStoragePaths(_filename))
+
+
 def _CheckFileStoragePath(path, allowed):
   """Checks if a path is in a list of allowed paths for file storage.
 
@@ -126,7 +179,7 @@ def _CheckFileStoragePath(path, allowed):
                                       " storage" % path)
 
 
-def LoadAllowedFileStoragePaths(filename):
+def _LoadAllowedFileStoragePaths(filename):
   """Loads file containing allowed file storage paths.
 
   @rtype: list
@@ -149,7 +202,13 @@ def CheckFileStoragePath(path, _filename=pathutils.FILE_STORAGE_PATHS_FILE):
   @raise errors.FileStoragePathError: If the path is not allowed
 
   """
-  _CheckFileStoragePath(path, LoadAllowedFileStoragePaths(_filename))
+  allowed = _LoadAllowedFileStoragePaths(_filename)
+
+  if _ComputeWrongFileStoragePaths([path]):
+    raise errors.FileStoragePathError("Path '%s' uses a forbidden prefix" %
+                                      path)
+
+  _CheckFileStoragePath(path, allowed)
 
 
 class BlockDev(object):
diff --git a/test/ganeti.bdev_unittest.py b/test/ganeti.bdev_unittest.py
index e900719..d58ee22 100755
--- a/test/ganeti.bdev_unittest.py
+++ b/test/ganeti.bdev_unittest.py
@@ -373,7 +373,48 @@ class TestRADOSBlockDevice(testutils.GanetiTestCase):
                       output_extra_matches, volume_name)
 
 
-class TestCheckFileStoragePath(unittest.TestCase):
+class TestComputeWrongFileStoragePathsInternal(unittest.TestCase):
+  def testPaths(self):
+    paths = bdev._GetForbiddenFileStoragePaths()
+
+    for path in ["/bin", "/usr/local/sbin", "/lib64", "/etc", "/sys"]:
+      self.assertTrue(path in paths)
+
+    self.assertEqual(set(map(os.path.normpath, paths)), paths)
+
+  def test(self):
+    vfsp = bdev._ComputeWrongFileStoragePaths
+    self.assertEqual(vfsp([]), [])
+    self.assertEqual(vfsp(["/tmp"]), [])
+    self.assertEqual(vfsp(["/bin/ls"]), ["/bin/ls"])
+    self.assertEqual(vfsp(["/bin"]), ["/bin"])
+    self.assertEqual(vfsp(["/usr/sbin/vim", "/srv/file-storage"]),
+                     ["/usr/sbin/vim"])
+
+
+class TestComputeWrongFileStoragePaths(testutils.GanetiTestCase):
+  def test(self):
+    tmpfile = self._CreateTempFile()
+
+    utils.WriteFile(tmpfile, data="""
+      /tmp
+      x/y///z/relative
+      # This is a test file
+      /srv/storage
+      /bin
+      /usr/local/lib32/
+      relative/path
+      """)
+
+    self.assertEqual(bdev.ComputeWrongFileStoragePaths(_filename=tmpfile), [
+      "/bin",
+      "/usr/local/lib32",
+      "relative/path",
+      "x/y/z/relative",
+      ])
+
+
+class TestCheckFileStoragePathInternal(unittest.TestCase):
   def testNonAbsolute(self):
     for i in ["", "tmp", "foo/bar/baz"]:
       self.assertRaises(errors.FileStoragePathError,
@@ -395,14 +436,44 @@ class TestCheckFileStoragePath(unittest.TestCase):
     bdev._CheckFileStoragePath("/tmp/foo/a/x", ["/tmp/foo"])
 
 
+class TestCheckFileStoragePath(testutils.GanetiTestCase):
+  def testNonExistantFile(self):
+    filename = "/tmp/this/file/does/not/exist"
+    assert not os.path.exists(filename)
+    self.assertRaises(errors.FileStoragePathError,
+                      bdev.CheckFileStoragePath, "/bin/", _filename=filename)
+    self.assertRaises(errors.FileStoragePathError,
+                      bdev.CheckFileStoragePath, "/srv/file-storage",
+                      _filename=filename)
+
+  def testAllowedPath(self):
+    tmpfile = self._CreateTempFile()
+
+    utils.WriteFile(tmpfile, data="""
+      /srv/storage
+      """)
+
+    bdev.CheckFileStoragePath("/srv/storage/inst1", _filename=tmpfile)
+
+    # No additional path component
+    self.assertRaises(errors.FileStoragePathError,
+                      bdev.CheckFileStoragePath, "/srv/storage",
+                      _filename=tmpfile)
+
+    # Forbidden path
+    self.assertRaises(errors.FileStoragePathError,
+                      bdev.CheckFileStoragePath, "/usr/lib64/xyz",
+                      _filename=tmpfile)
+
+
 class TestLoadAllowedFileStoragePaths(testutils.GanetiTestCase):
   def testDevNull(self):
-    self.assertEqual(bdev.LoadAllowedFileStoragePaths("/dev/null"), [])
+    self.assertEqual(bdev._LoadAllowedFileStoragePaths("/dev/null"), [])
 
   def testNonExistantFile(self):
     filename = "/tmp/this/file/does/not/exist"
     assert not os.path.exists(filename)
-    self.assertEqual(bdev.LoadAllowedFileStoragePaths(filename), [])
+    self.assertEqual(bdev._LoadAllowedFileStoragePaths(filename), [])
 
   def test(self):
     tmpfile = self._CreateTempFile()
@@ -414,7 +485,7 @@ class TestLoadAllowedFileStoragePaths(testutils.GanetiTestCase):
       relative/path
       """)
 
-    self.assertEqual(bdev.LoadAllowedFileStoragePaths(tmpfile), [
+    self.assertEqual(bdev._LoadAllowedFileStoragePaths(tmpfile), [
       "/tmp",
       "/srv/storage",
       "relative/path",
-- 
1.7.7.3