mirror of
https://github.com/python/cpython.git
synced 2025-08-04 00:48:58 +00:00
[3.14] gh-135034: Normalize link targets in tarfile, add os.path.realpath(strict='allow_missing')
(gh-135037) (gh-135065)
Addresses CVEs 2024-12718, 2025-4138, 2025-4330, and 2025-4517.
(cherry picked from commit 3612d8f517
)
Signed-off-by: Łukasz Langa <lukasz@langa.pl>
Co-authored-by: Petr Viktorin <encukou@gmail.com>
Co-authored-by: Seth Michael Larson <seth@python.org>
Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com>
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
parent
78fd7ce3d2
commit
9e0ac76d96
11 changed files with 970 additions and 173 deletions
|
@ -7,7 +7,8 @@ import sys
|
|||
import unittest
|
||||
import warnings
|
||||
from test.support import cpython_only, os_helper
|
||||
from test.support import TestFailed, is_emscripten
|
||||
from test.support import TestFailed
|
||||
from ntpath import ALLOW_MISSING
|
||||
from test.support.os_helper import FakePath
|
||||
from test import test_genericpath
|
||||
from tempfile import TemporaryFile
|
||||
|
@ -77,6 +78,27 @@ def tester(fn, wantResult):
|
|||
%(str(fn), str(wantResult), repr(gotResult)))
|
||||
|
||||
|
||||
def _parameterize(*parameters):
|
||||
"""Simplistic decorator to parametrize a test
|
||||
|
||||
Runs the decorated test multiple times in subTest, with a value from
|
||||
'parameters' passed as an extra positional argument.
|
||||
Calls doCleanups() after each run.
|
||||
|
||||
Not for general use. Intended to avoid indenting for easier backports.
|
||||
|
||||
See https://discuss.python.org/t/91827 for discussing generalizations.
|
||||
"""
|
||||
def _parametrize_decorator(func):
|
||||
def _parameterized(self, *args, **kwargs):
|
||||
for parameter in parameters:
|
||||
with self.subTest(parameter):
|
||||
func(self, *args, parameter, **kwargs)
|
||||
self.doCleanups()
|
||||
return _parameterized
|
||||
return _parametrize_decorator
|
||||
|
||||
|
||||
class NtpathTestCase(unittest.TestCase):
|
||||
def assertPathEqual(self, path1, path2):
|
||||
if path1 == path2 or _norm(path1) == _norm(path2):
|
||||
|
@ -475,6 +497,27 @@ class TestNtpath(NtpathTestCase):
|
|||
tester("ntpath.realpath('.\\.')", expected)
|
||||
tester("ntpath.realpath('\\'.join(['.'] * 100))", expected)
|
||||
|
||||
def test_realpath_curdir_strict(self):
|
||||
expected = ntpath.normpath(os.getcwd())
|
||||
tester("ntpath.realpath('.', strict=True)", expected)
|
||||
tester("ntpath.realpath('./.', strict=True)", expected)
|
||||
tester("ntpath.realpath('/'.join(['.'] * 100), strict=True)", expected)
|
||||
tester("ntpath.realpath('.\\.', strict=True)", expected)
|
||||
tester("ntpath.realpath('\\'.join(['.'] * 100), strict=True)", expected)
|
||||
|
||||
def test_realpath_curdir_missing_ok(self):
|
||||
expected = ntpath.normpath(os.getcwd())
|
||||
tester("ntpath.realpath('.', strict=ALLOW_MISSING)",
|
||||
expected)
|
||||
tester("ntpath.realpath('./.', strict=ALLOW_MISSING)",
|
||||
expected)
|
||||
tester("ntpath.realpath('/'.join(['.'] * 100), strict=ALLOW_MISSING)",
|
||||
expected)
|
||||
tester("ntpath.realpath('.\\.', strict=ALLOW_MISSING)",
|
||||
expected)
|
||||
tester("ntpath.realpath('\\'.join(['.'] * 100), strict=ALLOW_MISSING)",
|
||||
expected)
|
||||
|
||||
def test_realpath_pardir(self):
|
||||
expected = ntpath.normpath(os.getcwd())
|
||||
tester("ntpath.realpath('..')", ntpath.dirname(expected))
|
||||
|
@ -487,24 +530,59 @@ class TestNtpath(NtpathTestCase):
|
|||
tester("ntpath.realpath('\\'.join(['..'] * 50))",
|
||||
ntpath.splitdrive(expected)[0] + '\\')
|
||||
|
||||
def test_realpath_pardir_strict(self):
|
||||
expected = ntpath.normpath(os.getcwd())
|
||||
tester("ntpath.realpath('..', strict=True)", ntpath.dirname(expected))
|
||||
tester("ntpath.realpath('../..', strict=True)",
|
||||
ntpath.dirname(ntpath.dirname(expected)))
|
||||
tester("ntpath.realpath('/'.join(['..'] * 50), strict=True)",
|
||||
ntpath.splitdrive(expected)[0] + '\\')
|
||||
tester("ntpath.realpath('..\\..', strict=True)",
|
||||
ntpath.dirname(ntpath.dirname(expected)))
|
||||
tester("ntpath.realpath('\\'.join(['..'] * 50), strict=True)",
|
||||
ntpath.splitdrive(expected)[0] + '\\')
|
||||
|
||||
def test_realpath_pardir_missing_ok(self):
|
||||
expected = ntpath.normpath(os.getcwd())
|
||||
tester("ntpath.realpath('..', strict=ALLOW_MISSING)",
|
||||
ntpath.dirname(expected))
|
||||
tester("ntpath.realpath('../..', strict=ALLOW_MISSING)",
|
||||
ntpath.dirname(ntpath.dirname(expected)))
|
||||
tester("ntpath.realpath('/'.join(['..'] * 50), strict=ALLOW_MISSING)",
|
||||
ntpath.splitdrive(expected)[0] + '\\')
|
||||
tester("ntpath.realpath('..\\..', strict=ALLOW_MISSING)",
|
||||
ntpath.dirname(ntpath.dirname(expected)))
|
||||
tester("ntpath.realpath('\\'.join(['..'] * 50), strict=ALLOW_MISSING)",
|
||||
ntpath.splitdrive(expected)[0] + '\\')
|
||||
|
||||
@os_helper.skip_unless_symlink
|
||||
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||
def test_realpath_basic(self):
|
||||
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
|
||||
def test_realpath_basic(self, kwargs):
|
||||
ABSTFN = ntpath.abspath(os_helper.TESTFN)
|
||||
open(ABSTFN, "wb").close()
|
||||
self.addCleanup(os_helper.unlink, ABSTFN)
|
||||
self.addCleanup(os_helper.unlink, ABSTFN + "1")
|
||||
|
||||
os.symlink(ABSTFN, ABSTFN + "1")
|
||||
self.assertPathEqual(ntpath.realpath(ABSTFN + "1"), ABSTFN)
|
||||
self.assertPathEqual(ntpath.realpath(os.fsencode(ABSTFN + "1")),
|
||||
self.assertPathEqual(ntpath.realpath(ABSTFN + "1", **kwargs), ABSTFN)
|
||||
self.assertPathEqual(ntpath.realpath(os.fsencode(ABSTFN + "1"), **kwargs),
|
||||
os.fsencode(ABSTFN))
|
||||
|
||||
# gh-88013: call ntpath.realpath with binary drive name may raise a
|
||||
# TypeError. The drive should not exist to reproduce the bug.
|
||||
drives = {f"{c}:\\" for c in string.ascii_uppercase} - set(os.listdrives())
|
||||
d = drives.pop().encode()
|
||||
self.assertEqual(ntpath.realpath(d), d)
|
||||
self.assertEqual(ntpath.realpath(d, strict=False), d)
|
||||
|
||||
# gh-106242: Embedded nulls and non-strict fallback to abspath
|
||||
if kwargs:
|
||||
with self.assertRaises(OSError):
|
||||
ntpath.realpath(os_helper.TESTFN + "\0spam",
|
||||
**kwargs)
|
||||
else:
|
||||
self.assertEqual(ABSTFN + "\0spam",
|
||||
ntpath.realpath(os_helper.TESTFN + "\0spam", **kwargs))
|
||||
|
||||
@os_helper.skip_unless_symlink
|
||||
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||
|
@ -527,51 +605,66 @@ class TestNtpath(NtpathTestCase):
|
|||
self.assertEqual(realpath(path, strict=False), path)
|
||||
# gh-106242: Embedded nulls should raise OSError (not ValueError)
|
||||
self.assertRaises(OSError, realpath, path, strict=True)
|
||||
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
|
||||
path = ABSTFNb + b'\x00'
|
||||
self.assertEqual(realpath(path, strict=False), path)
|
||||
self.assertRaises(OSError, realpath, path, strict=True)
|
||||
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
|
||||
path = ABSTFN + '\\nonexistent\\x\x00'
|
||||
self.assertEqual(realpath(path, strict=False), path)
|
||||
self.assertRaises(OSError, realpath, path, strict=True)
|
||||
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
|
||||
path = ABSTFNb + b'\\nonexistent\\x\x00'
|
||||
self.assertEqual(realpath(path, strict=False), path)
|
||||
self.assertRaises(OSError, realpath, path, strict=True)
|
||||
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
|
||||
path = ABSTFN + '\x00\\..'
|
||||
self.assertEqual(realpath(path, strict=False), os.getcwd())
|
||||
self.assertEqual(realpath(path, strict=True), os.getcwd())
|
||||
self.assertEqual(realpath(path, strict=ALLOW_MISSING), os.getcwd())
|
||||
path = ABSTFNb + b'\x00\\..'
|
||||
self.assertEqual(realpath(path, strict=False), os.getcwdb())
|
||||
self.assertEqual(realpath(path, strict=True), os.getcwdb())
|
||||
self.assertEqual(realpath(path, strict=ALLOW_MISSING), os.getcwdb())
|
||||
path = ABSTFN + '\\nonexistent\\x\x00\\..'
|
||||
self.assertEqual(realpath(path, strict=False), ABSTFN + '\\nonexistent')
|
||||
self.assertRaises(OSError, realpath, path, strict=True)
|
||||
self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFN + '\\nonexistent')
|
||||
path = ABSTFNb + b'\\nonexistent\\x\x00\\..'
|
||||
self.assertEqual(realpath(path, strict=False), ABSTFNb + b'\\nonexistent')
|
||||
self.assertRaises(OSError, realpath, path, strict=True)
|
||||
self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFNb + b'\\nonexistent')
|
||||
|
||||
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
|
||||
def test_realpath_invalid_unicode_paths(self, kwargs):
|
||||
realpath = ntpath.realpath
|
||||
ABSTFN = ntpath.abspath(os_helper.TESTFN)
|
||||
ABSTFNb = os.fsencode(ABSTFN)
|
||||
path = ABSTFNb + b'\xff'
|
||||
self.assertRaises(UnicodeDecodeError, realpath, path, strict=False)
|
||||
self.assertRaises(UnicodeDecodeError, realpath, path, strict=True)
|
||||
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
|
||||
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
|
||||
path = ABSTFNb + b'\\nonexistent\\\xff'
|
||||
self.assertRaises(UnicodeDecodeError, realpath, path, strict=False)
|
||||
self.assertRaises(UnicodeDecodeError, realpath, path, strict=True)
|
||||
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
|
||||
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
|
||||
path = ABSTFNb + b'\xff\\..'
|
||||
self.assertRaises(UnicodeDecodeError, realpath, path, strict=False)
|
||||
self.assertRaises(UnicodeDecodeError, realpath, path, strict=True)
|
||||
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
|
||||
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
|
||||
path = ABSTFNb + b'\\nonexistent\\\xff\\..'
|
||||
self.assertRaises(UnicodeDecodeError, realpath, path, strict=False)
|
||||
self.assertRaises(UnicodeDecodeError, realpath, path, strict=True)
|
||||
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
|
||||
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
|
||||
|
||||
@os_helper.skip_unless_symlink
|
||||
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||
def test_realpath_relative(self):
|
||||
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
|
||||
def test_realpath_relative(self, kwargs):
|
||||
ABSTFN = ntpath.abspath(os_helper.TESTFN)
|
||||
open(ABSTFN, "wb").close()
|
||||
self.addCleanup(os_helper.unlink, ABSTFN)
|
||||
self.addCleanup(os_helper.unlink, ABSTFN + "1")
|
||||
|
||||
os.symlink(ABSTFN, ntpath.relpath(ABSTFN + "1"))
|
||||
self.assertPathEqual(ntpath.realpath(ABSTFN + "1"), ABSTFN)
|
||||
self.assertPathEqual(ntpath.realpath(ABSTFN + "1", **kwargs), ABSTFN)
|
||||
|
||||
@os_helper.skip_unless_symlink
|
||||
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||
|
@ -723,7 +816,62 @@ class TestNtpath(NtpathTestCase):
|
|||
|
||||
@os_helper.skip_unless_symlink
|
||||
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||
def test_realpath_symlink_prefix(self):
|
||||
def test_realpath_symlink_loops_raise(self):
|
||||
# Symlink loops raise OSError in ALLOW_MISSING mode
|
||||
ABSTFN = ntpath.abspath(os_helper.TESTFN)
|
||||
self.addCleanup(os_helper.unlink, ABSTFN)
|
||||
self.addCleanup(os_helper.unlink, ABSTFN + "1")
|
||||
self.addCleanup(os_helper.unlink, ABSTFN + "2")
|
||||
self.addCleanup(os_helper.unlink, ABSTFN + "y")
|
||||
self.addCleanup(os_helper.unlink, ABSTFN + "c")
|
||||
self.addCleanup(os_helper.unlink, ABSTFN + "a")
|
||||
self.addCleanup(os_helper.unlink, ABSTFN + "x")
|
||||
|
||||
os.symlink(ABSTFN, ABSTFN)
|
||||
self.assertRaises(OSError, ntpath.realpath, ABSTFN, strict=ALLOW_MISSING)
|
||||
|
||||
os.symlink(ABSTFN + "1", ABSTFN + "2")
|
||||
os.symlink(ABSTFN + "2", ABSTFN + "1")
|
||||
self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1",
|
||||
strict=ALLOW_MISSING)
|
||||
self.assertRaises(OSError, ntpath.realpath, ABSTFN + "2",
|
||||
strict=ALLOW_MISSING)
|
||||
self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\\x",
|
||||
strict=ALLOW_MISSING)
|
||||
|
||||
# Windows eliminates '..' components before resolving links;
|
||||
# realpath is not expected to raise if this removes the loop.
|
||||
self.assertPathEqual(ntpath.realpath(ABSTFN + "1\\.."),
|
||||
ntpath.dirname(ABSTFN))
|
||||
self.assertPathEqual(ntpath.realpath(ABSTFN + "1\\..\\x"),
|
||||
ntpath.dirname(ABSTFN) + "\\x")
|
||||
|
||||
os.symlink(ABSTFN + "x", ABSTFN + "y")
|
||||
self.assertPathEqual(ntpath.realpath(ABSTFN + "1\\..\\"
|
||||
+ ntpath.basename(ABSTFN) + "y"),
|
||||
ABSTFN + "x")
|
||||
self.assertRaises(
|
||||
OSError, ntpath.realpath,
|
||||
ABSTFN + "1\\..\\" + ntpath.basename(ABSTFN) + "1",
|
||||
strict=ALLOW_MISSING)
|
||||
|
||||
os.symlink(ntpath.basename(ABSTFN) + "a\\b", ABSTFN + "a")
|
||||
self.assertRaises(OSError, ntpath.realpath, ABSTFN + "a",
|
||||
strict=ALLOW_MISSING)
|
||||
|
||||
os.symlink("..\\" + ntpath.basename(ntpath.dirname(ABSTFN))
|
||||
+ "\\" + ntpath.basename(ABSTFN) + "c", ABSTFN + "c")
|
||||
self.assertRaises(OSError, ntpath.realpath, ABSTFN + "c",
|
||||
strict=ALLOW_MISSING)
|
||||
|
||||
# Test using relative path as well.
|
||||
self.assertRaises(OSError, ntpath.realpath, ntpath.basename(ABSTFN),
|
||||
strict=ALLOW_MISSING)
|
||||
|
||||
@os_helper.skip_unless_symlink
|
||||
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
|
||||
def test_realpath_symlink_prefix(self, kwargs):
|
||||
ABSTFN = ntpath.abspath(os_helper.TESTFN)
|
||||
self.addCleanup(os_helper.unlink, ABSTFN + "3")
|
||||
self.addCleanup(os_helper.unlink, "\\\\?\\" + ABSTFN + "3.")
|
||||
|
@ -738,9 +886,9 @@ class TestNtpath(NtpathTestCase):
|
|||
f.write(b'1')
|
||||
os.symlink("\\\\?\\" + ABSTFN + "3.", ABSTFN + "3.link")
|
||||
|
||||
self.assertPathEqual(ntpath.realpath(ABSTFN + "3link"),
|
||||
self.assertPathEqual(ntpath.realpath(ABSTFN + "3link", **kwargs),
|
||||
ABSTFN + "3")
|
||||
self.assertPathEqual(ntpath.realpath(ABSTFN + "3.link"),
|
||||
self.assertPathEqual(ntpath.realpath(ABSTFN + "3.link", **kwargs),
|
||||
"\\\\?\\" + ABSTFN + "3.")
|
||||
|
||||
# Resolved paths should be usable to open target files
|
||||
|
@ -750,14 +898,17 @@ class TestNtpath(NtpathTestCase):
|
|||
self.assertEqual(f.read(), b'1')
|
||||
|
||||
# When the prefix is included, it is not stripped
|
||||
self.assertPathEqual(ntpath.realpath("\\\\?\\" + ABSTFN + "3link"),
|
||||
self.assertPathEqual(ntpath.realpath("\\\\?\\" + ABSTFN + "3link", **kwargs),
|
||||
"\\\\?\\" + ABSTFN + "3")
|
||||
self.assertPathEqual(ntpath.realpath("\\\\?\\" + ABSTFN + "3.link"),
|
||||
self.assertPathEqual(ntpath.realpath("\\\\?\\" + ABSTFN + "3.link", **kwargs),
|
||||
"\\\\?\\" + ABSTFN + "3.")
|
||||
|
||||
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||
def test_realpath_nul(self):
|
||||
tester("ntpath.realpath('NUL')", r'\\.\NUL')
|
||||
tester("ntpath.realpath('NUL', strict=False)", r'\\.\NUL')
|
||||
tester("ntpath.realpath('NUL', strict=True)", r'\\.\NUL')
|
||||
tester("ntpath.realpath('NUL', strict=ALLOW_MISSING)", r'\\.\NUL')
|
||||
|
||||
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||
@unittest.skipUnless(HAVE_GETSHORTPATHNAME, 'need _getshortpathname')
|
||||
|
@ -781,12 +932,20 @@ class TestNtpath(NtpathTestCase):
|
|||
|
||||
self.assertPathEqual(test_file_long, ntpath.realpath(test_file_short))
|
||||
|
||||
with os_helper.change_cwd(test_dir_long):
|
||||
self.assertPathEqual(test_file_long, ntpath.realpath("file.txt"))
|
||||
with os_helper.change_cwd(test_dir_long.lower()):
|
||||
self.assertPathEqual(test_file_long, ntpath.realpath("file.txt"))
|
||||
with os_helper.change_cwd(test_dir_short):
|
||||
self.assertPathEqual(test_file_long, ntpath.realpath("file.txt"))
|
||||
for kwargs in {}, {'strict': True}, {'strict': ALLOW_MISSING}:
|
||||
with self.subTest(**kwargs):
|
||||
with os_helper.change_cwd(test_dir_long):
|
||||
self.assertPathEqual(
|
||||
test_file_long,
|
||||
ntpath.realpath("file.txt", **kwargs))
|
||||
with os_helper.change_cwd(test_dir_long.lower()):
|
||||
self.assertPathEqual(
|
||||
test_file_long,
|
||||
ntpath.realpath("file.txt", **kwargs))
|
||||
with os_helper.change_cwd(test_dir_short):
|
||||
self.assertPathEqual(
|
||||
test_file_long,
|
||||
ntpath.realpath("file.txt", **kwargs))
|
||||
|
||||
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||
def test_realpath_permission(self):
|
||||
|
@ -807,12 +966,15 @@ class TestNtpath(NtpathTestCase):
|
|||
# Automatic generation of short names may be disabled on
|
||||
# NTFS volumes for the sake of performance.
|
||||
# They're not supported at all on ReFS and exFAT.
|
||||
subprocess.run(
|
||||
p = subprocess.run(
|
||||
# Try to set the short name manually.
|
||||
['fsutil.exe', 'file', 'setShortName', test_file, 'LONGFI~1.TXT'],
|
||||
creationflags=subprocess.DETACHED_PROCESS
|
||||
)
|
||||
|
||||
if p.returncode:
|
||||
raise unittest.SkipTest('failed to set short name')
|
||||
|
||||
try:
|
||||
self.assertPathEqual(test_file, ntpath.realpath(test_file_short))
|
||||
except AssertionError:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue