From 4bb678e9d3892e8fceb4af5272cc45eb722f6baf Mon Sep 17 00:00:00 2001
From: Iustin Pop <iustin@google.com>
Date: Wed, 3 Mar 2010 11:37:18 +0100
Subject: [PATCH] utils: Add a PathJoin function

This will replace os.path.join since it is not safe for directory
traversal issues.

Signed-off-by: Iustin Pop <iustin@google.com>
Reviewed-by: Michael Hanselmann <hansmi@google.com>
---
 lib/utils.py                  | 30 ++++++++++++++++++++++++++++++
 test/ganeti.utils_unittest.py | 19 ++++++++++++++++++-
 2 files changed, 48 insertions(+), 1 deletion(-)

diff --git a/lib/utils.py b/lib/utils.py
index 6877dc704..393f4f4f3 100644
--- a/lib/utils.py
+++ b/lib/utils.py
@@ -1936,6 +1936,36 @@ def IsNormAbsPath(path):
   return os.path.normpath(path) == path and os.path.isabs(path)
 
 
+def PathJoin(*args):
+  """Safe-join a list of path components.
+
+  Requirements:
+      - the first argument must be an absolute path
+      - no component in the path must have backtracking (e.g. /../),
+        since we check for normalization at the end
+
+  @param args: the path components to be joined
+  @raise ValueError: for invalid paths
+
+  """
+  # ensure we're having at least one path passed in
+  assert args
+  # ensure the first component is an absolute and normalized path name
+  root = args[0]
+  if not IsNormAbsPath(root):
+    raise ValueError("Invalid parameter to PathJoin: '%s'" % str(args[0]))
+  result = os.path.join(*args)
+  # ensure that the whole path is normalized
+  if not IsNormAbsPath(result):
+    raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args))
+  # check that we're still under the original prefix
+  prefix = os.path.commonprefix([root, result])
+  if prefix != root:
+    raise ValueError("Error: path joining resulted in different prefix"
+                     " (%s != %s)" % (prefix, root))
+  return result
+
+
 def TailFile(fname, lines=20):
   """Return the last lines from a file.
 
diff --git a/test/ganeti.utils_unittest.py b/test/ganeti.utils_unittest.py
index 4d19f8599..550bf2503 100755
--- a/test/ganeti.utils_unittest.py
+++ b/test/ganeti.utils_unittest.py
@@ -47,7 +47,7 @@ from ganeti.utils import IsProcessAlive, RunCmd, \
      ShellQuote, ShellQuoteArgs, TcpPing, ListVisibleFiles, \
      SetEtcHostsEntry, RemoveEtcHostsEntry, FirstFree, OwnIpAddress, \
      TailFile, ForceDictType, SafeEncode, IsNormAbsPath, FormatTime, \
-     UnescapeAndSplit, RunParts
+     UnescapeAndSplit, RunParts, PathJoin
 
 from ganeti.errors import LockError, UnitParseError, GenericError, \
      ProgrammerError
@@ -1260,5 +1260,22 @@ class TestUnescapeAndSplit(unittest.TestCase):
       self.failUnlessEqual(UnescapeAndSplit(sep.join(a), sep=sep), b)
 
 
+class TestPathJoin(unittest.TestCase):
+  """Testing case for PathJoin"""
+
+  def testBasicItems(self):
+    mlist = ["/a", "b", "c"]
+    self.failUnlessEqual(PathJoin(*mlist), "/".join(mlist))
+
+  def testNonAbsPrefix(self):
+    self.failUnlessRaises(ValueError, PathJoin, "a", "b")
+
+  def testBackTrack(self):
+    self.failUnlessRaises(ValueError, PathJoin, "/a", "b/../c")
+
+  def testMultiAbs(self):
+    self.failUnlessRaises(ValueError, PathJoin, "/a", "/b")
+
+
 if __name__ == '__main__':
   testutils.GanetiTestProgram()
-- 
GitLab