Skip to content

Commit 48eb01a

Browse files
[AutoPR- Security] Patch python3 for CVE-2025-6075 [LOW] (microsoft#15009)
Co-authored-by: Kanishk Bansal <[email protected]>
1 parent 21fec47 commit 48eb01a

File tree

6 files changed

+424
-27
lines changed

6 files changed

+424
-27
lines changed

SPECS/python3/CVE-2025-6075.patch

Lines changed: 392 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,392 @@
1+
From cbf2497d481c97e51290e8b13b7f77be03cc9a78 Mon Sep 17 00:00:00 2001
2+
From: =?UTF-8?q?=C5=81ukasz=20Langa?= <[email protected]>
3+
Date: Fri, 31 Oct 2025 17:05:53 +0100
4+
Subject: [PATCH] gh-136065: Fix quadratic complexity in os.path.expandvars()
5+
(GH-134952) (GH-140839)
6+
MIME-Version: 1.0
7+
Content-Type: text/plain; charset=UTF-8
8+
Content-Transfer-Encoding: 8bit
9+
10+
(cherry picked from commit f029e8db626ddc6e3a3beea4eff511a71aaceb5c)
11+
12+
Co-authored-by: Serhiy Storchaka <[email protected]>
13+
Co-authored-by: Łukasz Langa <[email protected]>
14+
Signed-off-by: Azure Linux Security Servicing Account <[email protected]>
15+
Upstream-reference: https://github.com/python/cpython/commit/2e6150adccaaf5bd95d4c19dfd04a36e0b325d8c.patch
16+
---
17+
Lib/ntpath.py | 126 ++++++------------
18+
Lib/posixpath.py | 43 +++---
19+
Lib/test/test_genericpath.py | 19 ++-
20+
Lib/test/test_ntpath.py | 23 +++-
21+
...-05-30-22-33-27.gh-issue-136065.bu337o.rst | 1 +
22+
5 files changed, 96 insertions(+), 116 deletions(-)
23+
create mode 100644 Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst
24+
25+
diff --git a/Lib/ntpath.py b/Lib/ntpath.py
26+
index 2588ea3..aa946d7 100644
27+
--- a/Lib/ntpath.py
28+
+++ b/Lib/ntpath.py
29+
@@ -335,17 +335,23 @@ def expanduser(path):
30+
# XXX With COMMAND.COM you can use any characters in a variable name,
31+
# XXX except '^|<>='.
32+
33+
+_varpattern = r"'[^']*'?|%(%|[^%]*%?)|\$(\$|[-\w]+|\{[^}]*\}?)"
34+
+_varsub = None
35+
+_varsubb = None
36+
+
37+
def expandvars(path):
38+
"""Expand shell variables of the forms $var, ${var} and %var%.
39+
40+
Unknown variables are left unchanged."""
41+
path = os.fspath(path)
42+
+ global _varsub, _varsubb
43+
if isinstance(path, bytes):
44+
if b'$' not in path and b'%' not in path:
45+
return path
46+
- import string
47+
- varchars = bytes(string.ascii_letters + string.digits + '_-', 'ascii')
48+
- quote = b'\''
49+
+ if not _varsubb:
50+
+ import re
51+
+ _varsubb = re.compile(_varpattern.encode(), re.ASCII).sub
52+
+ sub = _varsubb
53+
percent = b'%'
54+
brace = b'{'
55+
rbrace = b'}'
56+
@@ -354,94 +360,44 @@ def expandvars(path):
57+
else:
58+
if '$' not in path and '%' not in path:
59+
return path
60+
- import string
61+
- varchars = string.ascii_letters + string.digits + '_-'
62+
- quote = '\''
63+
+ if not _varsub:
64+
+ import re
65+
+ _varsub = re.compile(_varpattern, re.ASCII).sub
66+
+ sub = _varsub
67+
percent = '%'
68+
brace = '{'
69+
rbrace = '}'
70+
dollar = '$'
71+
environ = os.environ
72+
- res = path[:0]
73+
- index = 0
74+
- pathlen = len(path)
75+
- while index < pathlen:
76+
- c = path[index:index+1]
77+
- if c == quote: # no expansion within single quotes
78+
- path = path[index + 1:]
79+
- pathlen = len(path)
80+
- try:
81+
- index = path.index(c)
82+
- res += c + path[:index + 1]
83+
- except ValueError:
84+
- res += c + path
85+
- index = pathlen - 1
86+
- elif c == percent: # variable or '%'
87+
- if path[index + 1:index + 2] == percent:
88+
- res += c
89+
- index += 1
90+
- else:
91+
- path = path[index+1:]
92+
- pathlen = len(path)
93+
- try:
94+
- index = path.index(percent)
95+
- except ValueError:
96+
- res += percent + path
97+
- index = pathlen - 1
98+
- else:
99+
- var = path[:index]
100+
- try:
101+
- if environ is None:
102+
- value = os.fsencode(os.environ[os.fsdecode(var)])
103+
- else:
104+
- value = environ[var]
105+
- except KeyError:
106+
- value = percent + var + percent
107+
- res += value
108+
- elif c == dollar: # variable or '$$'
109+
- if path[index + 1:index + 2] == dollar:
110+
- res += c
111+
- index += 1
112+
- elif path[index + 1:index + 2] == brace:
113+
- path = path[index+2:]
114+
- pathlen = len(path)
115+
- try:
116+
- index = path.index(rbrace)
117+
- except ValueError:
118+
- res += dollar + brace + path
119+
- index = pathlen - 1
120+
- else:
121+
- var = path[:index]
122+
- try:
123+
- if environ is None:
124+
- value = os.fsencode(os.environ[os.fsdecode(var)])
125+
- else:
126+
- value = environ[var]
127+
- except KeyError:
128+
- value = dollar + brace + var + rbrace
129+
- res += value
130+
- else:
131+
- var = path[:0]
132+
- index += 1
133+
- c = path[index:index + 1]
134+
- while c and c in varchars:
135+
- var += c
136+
- index += 1
137+
- c = path[index:index + 1]
138+
- try:
139+
- if environ is None:
140+
- value = os.fsencode(os.environ[os.fsdecode(var)])
141+
- else:
142+
- value = environ[var]
143+
- except KeyError:
144+
- value = dollar + var
145+
- res += value
146+
- if c:
147+
- index -= 1
148+
+
149+
+ def repl(m):
150+
+ lastindex = m.lastindex
151+
+ if lastindex is None:
152+
+ return m[0]
153+
+ name = m[lastindex]
154+
+ if lastindex == 1:
155+
+ if name == percent:
156+
+ return name
157+
+ if not name.endswith(percent):
158+
+ return m[0]
159+
+ name = name[:-1]
160+
else:
161+
- res += c
162+
- index += 1
163+
- return res
164+
+ if name == dollar:
165+
+ return name
166+
+ if name.startswith(brace):
167+
+ if not name.endswith(rbrace):
168+
+ return m[0]
169+
+ name = name[1:-1]
170+
+
171+
+ try:
172+
+ if environ is None:
173+
+ return os.fsencode(os.environ[os.fsdecode(name)])
174+
+ else:
175+
+ return environ[name]
176+
+ except KeyError:
177+
+ return m[0]
178+
+
179+
+ return sub(repl, path)
180+
181+
182+
# Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A\B.
183+
diff --git a/Lib/posixpath.py b/Lib/posixpath.py
184+
index de2b90c..5daa6ef 100644
185+
--- a/Lib/posixpath.py
186+
+++ b/Lib/posixpath.py
187+
@@ -275,42 +275,41 @@ def expanduser(path):
188+
# This expands the forms $variable and ${variable} only.
189+
# Non-existent variables are left unchanged.
190+
191+
-_varprog = None
192+
-_varprogb = None
193+
+_varpattern = r'\$(\w+|\{[^}]*\}?)'
194+
+_varsub = None
195+
+_varsubb = None
196+
197+
def expandvars(path):
198+
"""Expand shell variables of form $var and ${var}. Unknown variables
199+
are left unchanged."""
200+
path = os.fspath(path)
201+
- global _varprog, _varprogb
202+
+ global _varsub, _varsubb
203+
if isinstance(path, bytes):
204+
if b'$' not in path:
205+
return path
206+
- if not _varprogb:
207+
+ if not _varsubb:
208+
import re
209+
- _varprogb = re.compile(br'\$(\w+|\{[^}]*\})', re.ASCII)
210+
- search = _varprogb.search
211+
+ _varsubb = re.compile(_varpattern.encode(), re.ASCII).sub
212+
+ sub = _varsubb
213+
start = b'{'
214+
end = b'}'
215+
environ = getattr(os, 'environb', None)
216+
else:
217+
if '$' not in path:
218+
return path
219+
- if not _varprog:
220+
+ if not _varsub:
221+
import re
222+
- _varprog = re.compile(r'\$(\w+|\{[^}]*\})', re.ASCII)
223+
- search = _varprog.search
224+
+ _varsub = re.compile(_varpattern, re.ASCII).sub
225+
+ sub = _varsub
226+
start = '{'
227+
end = '}'
228+
environ = os.environ
229+
- i = 0
230+
- while True:
231+
- m = search(path, i)
232+
- if not m:
233+
- break
234+
- i, j = m.span(0)
235+
- name = m.group(1)
236+
- if name.startswith(start) and name.endswith(end):
237+
+
238+
+ def repl(m):
239+
+ name = m[1]
240+
+ if name.startswith(start):
241+
+ if not name.endswith(end):
242+
+ return m[0]
243+
name = name[1:-1]
244+
try:
245+
if environ is None:
246+
@@ -318,13 +317,11 @@ def expandvars(path):
247+
else:
248+
value = environ[name]
249+
except KeyError:
250+
- i = j
251+
+ return m[0]
252+
else:
253+
- tail = path[j:]
254+
- path = path[:i] + value
255+
- i = len(path)
256+
- path += tail
257+
- return path
258+
+ return value
259+
+
260+
+ return sub(repl, path)
261+
262+
263+
# Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A/B.
264+
diff --git a/Lib/test/test_genericpath.py b/Lib/test/test_genericpath.py
265+
index e7acbcd..e53bb20 100644
266+
--- a/Lib/test/test_genericpath.py
267+
+++ b/Lib/test/test_genericpath.py
268+
@@ -9,7 +9,7 @@ import unittest
269+
import warnings
270+
from test import support
271+
from test.support.script_helper import assert_python_ok
272+
-from test.support import FakePath
273+
+from test.support import FakePath, EnvironmentVarGuard
274+
275+
276+
def create_file(filename, data=b'foo'):
277+
@@ -374,7 +374,7 @@ class CommonTest(GenericTest):
278+
279+
def test_expandvars(self):
280+
expandvars = self.pathmodule.expandvars
281+
- with support.EnvironmentVarGuard() as env:
282+
+ with EnvironmentVarGuard() as env:
283+
env.clear()
284+
env["foo"] = "bar"
285+
env["{foo"] = "baz1"
286+
@@ -408,7 +408,7 @@ class CommonTest(GenericTest):
287+
expandvars = self.pathmodule.expandvars
288+
def check(value, expected):
289+
self.assertEqual(expandvars(value), expected)
290+
- with support.EnvironmentVarGuard() as env:
291+
+ with EnvironmentVarGuard() as env:
292+
env.clear()
293+
nonascii = support.FS_NONASCII
294+
env['spam'] = nonascii
295+
@@ -429,6 +429,19 @@ class CommonTest(GenericTest):
296+
os.fsencode('$bar%s bar' % nonascii))
297+
check(b'$spam}bar', os.fsencode('%s}bar' % nonascii))
298+
299+
+ @support.requires_resource('cpu')
300+
+ def test_expandvars_large(self):
301+
+ expandvars = self.pathmodule.expandvars
302+
+ with EnvironmentVarGuard() as env:
303+
+ env.clear()
304+
+ env["A"] = "B"
305+
+ n = 100_000
306+
+ self.assertEqual(expandvars('$A'*n), 'B'*n)
307+
+ self.assertEqual(expandvars('${A}'*n), 'B'*n)
308+
+ self.assertEqual(expandvars('$A!'*n), 'B!'*n)
309+
+ self.assertEqual(expandvars('${A}A'*n), 'BA'*n)
310+
+ self.assertEqual(expandvars('${'*10*n), '${'*10*n)
311+
+
312+
def test_abspath(self):
313+
self.assertIn("foo", self.pathmodule.abspath("foo"))
314+
with warnings.catch_warnings():
315+
diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py
316+
index 8f07d18..9a8a44b 100644
317+
--- a/Lib/test/test_ntpath.py
318+
+++ b/Lib/test/test_ntpath.py
319+
@@ -1,11 +1,10 @@
320+
import ntpath
321+
import os
322+
-import subprocess
323+
import sys
324+
import unittest
325+
import warnings
326+
from ntpath import ALLOW_MISSING
327+
-from test.support import TestFailed, FakePath
328+
+from test.support import TestFailed, FakePath, EnvironmentVarGuard
329+
from test import support, test_genericpath
330+
from tempfile import TemporaryFile
331+
332+
@@ -642,7 +641,7 @@ class TestNtpath(NtpathTestCase):
333+
ntpath.realpath("file.txt", **kwargs))
334+
335+
def test_expandvars(self):
336+
- with support.EnvironmentVarGuard() as env:
337+
+ with EnvironmentVarGuard() as env:
338+
env.clear()
339+
env["foo"] = "bar"
340+
env["{foo"] = "baz1"
341+
@@ -671,7 +670,7 @@ class TestNtpath(NtpathTestCase):
342+
def test_expandvars_nonascii(self):
343+
def check(value, expected):
344+
tester('ntpath.expandvars(%r)' % value, expected)
345+
- with support.EnvironmentVarGuard() as env:
346+
+ with EnvironmentVarGuard() as env:
347+
env.clear()
348+
nonascii = support.FS_NONASCII
349+
env['spam'] = nonascii
350+
@@ -687,10 +686,23 @@ class TestNtpath(NtpathTestCase):
351+
check('%spam%bar', '%sbar' % nonascii)
352+
check('%{}%bar'.format(nonascii), 'ham%sbar' % nonascii)
353+
354+
+ @support.requires_resource('cpu')
355+
+ def test_expandvars_large(self):
356+
+ expandvars = ntpath.expandvars
357+
+ with EnvironmentVarGuard() as env:
358+
+ env.clear()
359+
+ env["A"] = "B"
360+
+ n = 100_000
361+
+ self.assertEqual(expandvars('%A%'*n), 'B'*n)
362+
+ self.assertEqual(expandvars('%A%A'*n), 'BA'*n)
363+
+ self.assertEqual(expandvars("''"*n + '%%'), "''"*n + '%')
364+
+ self.assertEqual(expandvars("%%"*n), "%"*n)
365+
+ self.assertEqual(expandvars("$$"*n), "$"*n)
366+
+
367+
def test_expanduser(self):
368+
tester('ntpath.expanduser("test")', 'test')
369+
370+
- with support.EnvironmentVarGuard() as env:
371+
+ with EnvironmentVarGuard() as env:
372+
env.clear()
373+
tester('ntpath.expanduser("~test")', '~test')
374+
375+
@@ -908,6 +920,7 @@ class TestNtpath(NtpathTestCase):
376+
self.assertIsInstance(b_final_path, bytes)
377+
self.assertGreater(len(b_final_path), 0)
378+
379+
+
380+
class NtCommonTest(test_genericpath.CommonTest, unittest.TestCase):
381+
pathmodule = ntpath
382+
attributes = ['relpath']
383+
diff --git a/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst b/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst
384+
new file mode 100644
385+
index 0000000..1d152bb
386+
--- /dev/null
387+
+++ b/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst
388+
@@ -0,0 +1 @@
389+
+Fix quadratic complexity in :func:`os.path.expandvars`.
390+
--
391+
2.45.4
392+

0 commit comments

Comments
 (0)