Issue #19717: Makes Path.resolve() succeed on paths that do not exist (patch by Vajrasky Kok)

This commit is contained in:
Steve Dower 2016-11-09 12:58:31 -08:00
commit bb132fc34e
4 changed files with 81 additions and 17 deletions

View File

@ -919,7 +919,7 @@ call fails (for example because the path doesn't exist):
to an existing file or directory, it will be unconditionally replaced. to an existing file or directory, it will be unconditionally replaced.
.. method:: Path.resolve() .. method:: Path.resolve(strict=False)
Make the path absolute, resolving any symlinks. A new path object is Make the path absolute, resolving any symlinks. A new path object is
returned:: returned::
@ -936,10 +936,14 @@ call fails (for example because the path doesn't exist):
>>> p.resolve() >>> p.resolve()
PosixPath('/home/antoine/pathlib/setup.py') PosixPath('/home/antoine/pathlib/setup.py')
If the path doesn't exist, :exc:`FileNotFoundError` is raised. If an If the path doesn't exist and *strict* is ``True``, :exc:`FileNotFoundError`
infinite loop is encountered along the resolution path, is raised. If *strict* is ``False``, the path is resolved as far as possible
:exc:`RuntimeError` is raised. and any remainder is appended without checking whether it exists. If an
infinite loop is encountered along the resolution path, :exc:`RuntimeError`
is raised.
.. versionadded:: 3.6
The *strict* argument.
.. method:: Path.rglob(pattern) .. method:: Path.rglob(pattern)

View File

@ -178,12 +178,26 @@ class _WindowsFlavour(_Flavour):
def casefold_parts(self, parts): def casefold_parts(self, parts):
return [p.lower() for p in parts] return [p.lower() for p in parts]
def resolve(self, path): def resolve(self, path, strict=False):
s = str(path) s = str(path)
if not s: if not s:
return os.getcwd() return os.getcwd()
previous_s = None
if _getfinalpathname is not None: if _getfinalpathname is not None:
if strict:
return self._ext_to_normal(_getfinalpathname(s)) return self._ext_to_normal(_getfinalpathname(s))
else:
while True:
try:
s = self._ext_to_normal(_getfinalpathname(s))
except FileNotFoundError:
previous_s = s
s = os.path.abspath(os.path.join(s, os.pardir))
else:
if previous_s is None:
return s
else:
return s + os.path.sep + os.path.basename(previous_s)
# Means fallback on absolute # Means fallback on absolute
return None return None
@ -285,7 +299,7 @@ class _PosixFlavour(_Flavour):
def casefold_parts(self, parts): def casefold_parts(self, parts):
return parts return parts
def resolve(self, path): def resolve(self, path, strict=False):
sep = self.sep sep = self.sep
accessor = path._accessor accessor = path._accessor
seen = {} seen = {}
@ -315,7 +329,10 @@ class _PosixFlavour(_Flavour):
target = accessor.readlink(newpath) target = accessor.readlink(newpath)
except OSError as e: except OSError as e:
if e.errno != EINVAL: if e.errno != EINVAL:
if strict:
raise raise
else:
return newpath
# Not a symlink # Not a symlink
path = newpath path = newpath
else: else:
@ -1092,7 +1109,7 @@ class Path(PurePath):
obj._init(template=self) obj._init(template=self)
return obj return obj
def resolve(self): def resolve(self, strict=False):
""" """
Make the path absolute, resolving all symlinks on the way and also Make the path absolute, resolving all symlinks on the way and also
normalizing it (for example turning slashes into backslashes under normalizing it (for example turning slashes into backslashes under
@ -1100,7 +1117,7 @@ class Path(PurePath):
""" """
if self._closed: if self._closed:
self._raise_closed() self._raise_closed()
s = self._flavour.resolve(self) s = self._flavour.resolve(self, strict=strict)
if s is None: if s is None:
# No symlink resolution => for consistency, raise an error if # No symlink resolution => for consistency, raise an error if
# the path doesn't exist or is forbidden # the path doesn't exist or is forbidden

View File

@ -1486,8 +1486,8 @@ class _BasePathTest(object):
self.assertEqual(set(p.glob("../xyzzy")), set()) self.assertEqual(set(p.glob("../xyzzy")), set())
def _check_resolve(self, p, expected): def _check_resolve(self, p, expected, strict=True):
q = p.resolve() q = p.resolve(strict)
self.assertEqual(q, expected) self.assertEqual(q, expected)
# this can be used to check both relative and absolute resolutions # this can be used to check both relative and absolute resolutions
@ -1498,8 +1498,17 @@ class _BasePathTest(object):
P = self.cls P = self.cls
p = P(BASE, 'foo') p = P(BASE, 'foo')
with self.assertRaises(OSError) as cm: with self.assertRaises(OSError) as cm:
p.resolve() p.resolve(strict=True)
self.assertEqual(cm.exception.errno, errno.ENOENT) self.assertEqual(cm.exception.errno, errno.ENOENT)
# Non-strict
self.assertEqual(str(p.resolve(strict=False)),
os.path.join(BASE, 'foo'))
p = P(BASE, 'foo', 'in', 'spam')
self.assertEqual(str(p.resolve(strict=False)),
os.path.join(BASE, 'foo'))
p = P(BASE, '..', 'foo', 'in', 'spam')
self.assertEqual(str(p.resolve(strict=False)),
os.path.abspath(os.path.join('foo')))
# These are all relative symlinks # These are all relative symlinks
p = P(BASE, 'dirB', 'fileB') p = P(BASE, 'dirB', 'fileB')
self._check_resolve_relative(p, p) self._check_resolve_relative(p, p)
@ -1509,6 +1518,18 @@ class _BasePathTest(object):
self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB')) self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB'))
p = P(BASE, 'dirB', 'linkD', 'fileB') p = P(BASE, 'dirB', 'linkD', 'fileB')
self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB')) self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB'))
# Non-strict
p = P(BASE, 'dirA', 'linkC', 'fileB', 'foo', 'in', 'spam')
self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB', 'foo'), False)
p = P(BASE, 'dirA', 'linkC', '..', 'foo', 'in', 'spam')
if os.name == 'nt':
# In Windows, if linkY points to dirB, 'dirA\linkY\..'
# resolves to 'dirA' without resolving linkY first.
self._check_resolve_relative(p, P(BASE, 'dirA', 'foo'), False)
else:
# In Posix, if linkY points to dirB, 'dirA/linkY/..'
# resolves to 'dirB/..' first before resolving to parent of dirB.
self._check_resolve_relative(p, P(BASE, 'foo'), False)
# Now create absolute symlinks # Now create absolute symlinks
d = tempfile.mkdtemp(suffix='-dirD') d = tempfile.mkdtemp(suffix='-dirD')
self.addCleanup(support.rmtree, d) self.addCleanup(support.rmtree, d)
@ -1516,6 +1537,18 @@ class _BasePathTest(object):
os.symlink(join('dirB'), os.path.join(d, 'linkY')) os.symlink(join('dirB'), os.path.join(d, 'linkY'))
p = P(BASE, 'dirA', 'linkX', 'linkY', 'fileB') p = P(BASE, 'dirA', 'linkX', 'linkY', 'fileB')
self._check_resolve_absolute(p, P(BASE, 'dirB', 'fileB')) self._check_resolve_absolute(p, P(BASE, 'dirB', 'fileB'))
# Non-strict
p = P(BASE, 'dirA', 'linkX', 'linkY', 'foo', 'in', 'spam')
self._check_resolve_relative(p, P(BASE, 'dirB', 'foo'), False)
p = P(BASE, 'dirA', 'linkX', 'linkY', '..', 'foo', 'in', 'spam')
if os.name == 'nt':
# In Windows, if linkY points to dirB, 'dirA\linkY\..'
# resolves to 'dirA' without resolving linkY first.
self._check_resolve_relative(p, P(d, 'foo'), False)
else:
# In Posix, if linkY points to dirB, 'dirA/linkY/..'
# resolves to 'dirB/..' first before resolving to parent of dirB.
self._check_resolve_relative(p, P(BASE, 'foo'), False)
@with_symlinks @with_symlinks
def test_resolve_dot(self): def test_resolve_dot(self):
@ -1525,7 +1558,11 @@ class _BasePathTest(object):
self.dirlink(os.path.join('0', '0'), join('1')) self.dirlink(os.path.join('0', '0'), join('1'))
self.dirlink(os.path.join('1', '1'), join('2')) self.dirlink(os.path.join('1', '1'), join('2'))
q = p / '2' q = p / '2'
self.assertEqual(q.resolve(), p) self.assertEqual(q.resolve(strict=True), p)
r = q / '3' / '4'
self.assertRaises(FileNotFoundError, r.resolve, strict=True)
# Non-strict
self.assertEqual(r.resolve(strict=False), p / '3')
def test_with(self): def test_with(self):
p = self.cls(BASE) p = self.cls(BASE)
@ -1972,10 +2009,10 @@ class PathTest(_BasePathTest, unittest.TestCase):
class PosixPathTest(_BasePathTest, unittest.TestCase): class PosixPathTest(_BasePathTest, unittest.TestCase):
cls = pathlib.PosixPath cls = pathlib.PosixPath
def _check_symlink_loop(self, *args): def _check_symlink_loop(self, *args, strict=True):
path = self.cls(*args) path = self.cls(*args)
with self.assertRaises(RuntimeError): with self.assertRaises(RuntimeError):
print(path.resolve()) print(path.resolve(strict))
def test_open_mode(self): def test_open_mode(self):
old_mask = os.umask(0) old_mask = os.umask(0)
@ -2008,7 +2045,6 @@ class PosixPathTest(_BasePathTest, unittest.TestCase):
@with_symlinks @with_symlinks
def test_resolve_loop(self): def test_resolve_loop(self):
# Loop detection for broken symlinks under POSIX
# Loops with relative symlinks # Loops with relative symlinks
os.symlink('linkX/inside', join('linkX')) os.symlink('linkX/inside', join('linkX'))
self._check_symlink_loop(BASE, 'linkX') self._check_symlink_loop(BASE, 'linkX')
@ -2016,6 +2052,8 @@ class PosixPathTest(_BasePathTest, unittest.TestCase):
self._check_symlink_loop(BASE, 'linkY') self._check_symlink_loop(BASE, 'linkY')
os.symlink('linkZ/../linkZ', join('linkZ')) os.symlink('linkZ/../linkZ', join('linkZ'))
self._check_symlink_loop(BASE, 'linkZ') self._check_symlink_loop(BASE, 'linkZ')
# Non-strict
self._check_symlink_loop(BASE, 'linkZ', 'foo', strict=False)
# Loops with absolute symlinks # Loops with absolute symlinks
os.symlink(join('linkU/inside'), join('linkU')) os.symlink(join('linkU/inside'), join('linkU'))
self._check_symlink_loop(BASE, 'linkU') self._check_symlink_loop(BASE, 'linkU')
@ -2023,6 +2061,8 @@ class PosixPathTest(_BasePathTest, unittest.TestCase):
self._check_symlink_loop(BASE, 'linkV') self._check_symlink_loop(BASE, 'linkV')
os.symlink(join('linkW/../linkW'), join('linkW')) os.symlink(join('linkW/../linkW'), join('linkW'))
self._check_symlink_loop(BASE, 'linkW') self._check_symlink_loop(BASE, 'linkW')
# Non-strict
self._check_symlink_loop(BASE, 'linkW', 'foo', strict=False)
def test_glob(self): def test_glob(self):
P = self.cls P = self.cls

View File

@ -114,6 +114,9 @@ Core and Builtins
Library Library
------- -------
- Issue #19717: Makes Path.resolve() succeed on paths that do not exist.
Patch by Vajrasky Kok
- Issue #28563: Fixed possible DoS and arbitrary code execution when handle - Issue #28563: Fixed possible DoS and arbitrary code execution when handle
plural form selections in the gettext module. The expression parser now plural form selections in the gettext module. The expression parser now
supports exact syntax supported by GNU gettext. supports exact syntax supported by GNU gettext.