Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitbf2865f

Browse files
authored
[3.14]gh-90949: add Expat API to prevent XML deadly allocations (CVE-2025-59375) (GH-139234) (#139359)
* [3.14]gh-90949: add Expat API to prevent XML deadly allocations (CVE-2025-59375) (GH-139234)Expose the XML Expat 2.7.2 mitigation APIs to disallow use ofdisproportional amounts of dynamic memory from within an Expatparser (seeCVE-2025-59375 for instance).The exposed APIs are available on Expat parsers, that is,parsers created by `xml.parsers.expat.ParserCreate()`, as:- `parser.SetAllocTrackerActivationThreshold(threshold)`, and- `parser.SetAllocTrackerMaximumAmplification(max_factor)`.(cherry picked from commitf04bea4)(cherry picked from commit68a1778)
1 parent4d7fab9 commitbf2865f

File tree

7 files changed

+583
-29
lines changed

7 files changed

+583
-29
lines changed

‎Doc/library/pyexpat.rst‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ The :mod:`xml.parsers.expat` module contains two functions:
7272
*encoding* [1]_ is given it will override the implicit or explicit encoding of the
7373
document.
7474

75+
.. _xmlparser-non-root:
76+
77+
Parsers created through:func:`!ParserCreate` are called "root" parsers,
78+
in the sense that they do not have any parent parser attached. Non-root
79+
parsers are created by:meth:`parser.ExternalEntityParserCreate
80+
<xmlparser.ExternalEntityParserCreate>`.
81+
7582
Expat can optionally do XML namespace processing for you, enabled by providing a
7683
value for *namespace_separator*. The value must be a one-character string; a
7784
:exc:`ValueError` will be raised if the string has an illegal length (``None``
@@ -231,6 +238,55 @@ XMLParser Objects
231238
..versionadded::3.13
232239

233240

241+
:class:`!xmlparser` objects have the following methods to mitigate some
242+
common XML vulnerabilities.
243+
244+
..method::xmlparser.SetAllocTrackerActivationThreshold(threshold, /)
245+
246+
Sets the number of allocated bytes of dynamic memory needed to activate
247+
protection against disproportionate use of RAM.
248+
249+
By default, parser objects have an allocation activation threshold of 64 MiB,
250+
or equivalently 67,108,864 bytes.
251+
252+
An:exc:`ExpatError` is raised if this method is called on a
253+
|xml-non-root-parser| parser.
254+
The corresponding:attr:`~ExpatError.lineno` and:attr:`~ExpatError.offset`
255+
should not be used as they may have no special meaning.
256+
257+
..versionadded::next
258+
259+
..method::xmlparser.SetAllocTrackerMaximumAmplification(max_factor, /)
260+
261+
Sets the maximum amplification factor between direct input and bytes
262+
of dynamic memory allocated.
263+
264+
The amplification factor is calculated as ``allocated / direct``
265+
while parsing, where ``direct`` is the number of bytes read from
266+
the primary document in parsing and ``allocated`` is the number
267+
of bytes of dynamic memory allocated in the parser hierarchy.
268+
269+
The *max_factor* value must be a non-NaN:class:`float` value greater than
270+
or equal to 1.0. Amplification factors greater than 100.0 can be observed
271+
near the start of parsing even with benign files in practice. In particular,
272+
the activation threshold should be carefully chosen to avoid false positives.
273+
274+
By default, parser objects have a maximum amplification factor of 100.0.
275+
276+
An:exc:`ExpatError` is raised if this method is called on a
277+
|xml-non-root-parser| parser or if *max_factor* is outside the valid range.
278+
The corresponding:attr:`~ExpatError.lineno` and:attr:`~ExpatError.offset`
279+
should not be used as they may have no special meaning.
280+
281+
..note::
282+
283+
The maximum amplification factor is only considered if the threshold
284+
that can be adjusted by:meth:`.SetAllocTrackerActivationThreshold`
285+
is exceeded.
286+
287+
..versionadded::next
288+
289+
234290
:class:`xmlparser` objects have the following attributes:
235291

236292

@@ -954,3 +1010,4 @@ The ``errors`` module has the following attributes:
9541010
not. See https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-EncodingDecl
9551011
and https://www.iana.org/assignments/character-sets/character-sets.xhtml.
9561012
1013+
.. |xml-non-root-parser|replace:::ref:`non-root<xmlparser-non-root>`

‎Include/pyexpat.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ struct PyExpat_CAPI
5252
int (*SetHashSalt)(XML_Parserparser,unsigned longhash_salt);
5353
/* might be NULL for expat < 2.6.0 */
5454
XML_Bool (*SetReparseDeferralEnabled)(XML_Parserparser,XML_Boolenabled);
55+
/* might be NULL for expat < 2.7.2 */
56+
XML_Bool (*SetAllocTrackerActivationThreshold)(
57+
XML_Parserparser,unsigned long longactivationThresholdBytes);
58+
XML_Bool (*SetAllocTrackerMaximumAmplification)(
59+
XML_Parserparser,floatmaxAmplificationFactor);
5560
/* always add new stuff to the end! */
5661
};
5762

‎Lib/test/test_pyexpat.py‎

Lines changed: 198 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
# XXX TypeErrors on calling handlers, or on bad return values from a
22
# handler, are obscure and unhelpful.
33

4+
importabc
5+
importfunctools
46
importos
7+
importre
58
importsys
69
importsysconfig
710
importtextwrap
811
importunittest
912
importtraceback
1013
fromioimportBytesIO
1114
fromtestimportsupport
12-
fromtest.supportimportos_helper
15+
fromtest.supportimportimport_helper,os_helper
1316

1417
fromxml.parsersimportexpat
1518
fromxml.parsers.expatimporterrors
@@ -863,5 +866,199 @@ def start_element(name, _):
863866
self.assertEqual(started, ['doc'])
864867

865868

869+
classAttackProtectionTestBase(abc.ABC):
870+
"""
871+
Base class for testing protections against XML payloads with
872+
disproportionate amplification.
873+
874+
The protections being tested should detect and prevent attacks
875+
that leverage disproportionate amplification from small inputs.
876+
"""
877+
878+
@staticmethod
879+
defexponential_expansion_payload(*,nrows,ncols,text='.'):
880+
"""Create a billion laughs attack payload.
881+
882+
Be careful: the number of total items is pow(n, k), thereby
883+
requiring at least pow(ncols, nrows) * sizeof(text) memory!
884+
"""
885+
template=textwrap.dedent(f"""\
886+
<?xml version="1.0"?>
887+
<!DOCTYPE doc [
888+
<!ENTITY row0 "{text}">
889+
<!ELEMENT doc (#PCDATA)>
890+
{{body}}
891+
]>
892+
<doc>&row{nrows};</doc>
893+
""").rstrip()
894+
895+
body='\n'.join(
896+
f'<!ENTITY row{i+1} "{f"&row{i};"*ncols}">'
897+
foriinrange(nrows)
898+
)
899+
body=textwrap.indent(body,' '*4)
900+
returntemplate.format(body=body)
901+
902+
deftest_payload_generation(self):
903+
# self-test for exponential_expansion_payload()
904+
payload=self.exponential_expansion_payload(nrows=2,ncols=3)
905+
self.assertEqual(payload,textwrap.dedent("""\
906+
<?xml version="1.0"?>
907+
<!DOCTYPE doc [
908+
<!ENTITY row0 ".">
909+
<!ELEMENT doc (#PCDATA)>
910+
<!ENTITY row1 "&row0;&row0;&row0;">
911+
<!ENTITY row2 "&row1;&row1;&row1;">
912+
]>
913+
<doc>&row2;</doc>
914+
""").rstrip())
915+
916+
defassert_root_parser_failure(self,func,/,*args,**kwargs):
917+
"""Check that func(*args, **kwargs) is invalid for a sub-parser."""
918+
msg="parser must be a root parser"
919+
self.assertRaisesRegex(expat.ExpatError,msg,func,*args,**kwargs)
920+
921+
@abc.abstractmethod
922+
defassert_rejected(self,func,/,*args,**kwargs):
923+
"""Assert that func(*args, **kwargs) triggers the attack protection.
924+
925+
Note: this method must ensure that the attack protection being tested
926+
is the one that is actually triggered at runtime, e.g., by matching
927+
the exact error message.
928+
"""
929+
930+
@abc.abstractmethod
931+
defset_activation_threshold(self,parser,threshold):
932+
"""Set the activation threshold for the tested protection."""
933+
934+
@abc.abstractmethod
935+
defset_maximum_amplification(self,parser,max_factor):
936+
"""Set the maximum amplification factor for the tested protection."""
937+
938+
@abc.abstractmethod
939+
deftest_set_activation_threshold__threshold_reached(self):
940+
"""Test when the activation threshold is exceeded."""
941+
942+
@abc.abstractmethod
943+
deftest_set_activation_threshold__threshold_not_reached(self):
944+
"""Test when the activation threshold is not exceeded."""
945+
946+
deftest_set_activation_threshold__invalid_threshold_type(self):
947+
parser=expat.ParserCreate()
948+
setter=functools.partial(self.set_activation_threshold,parser)
949+
950+
self.assertRaises(TypeError,setter,1.0)
951+
self.assertRaises(TypeError,setter,-1.5)
952+
self.assertRaises(ValueError,setter,-5)
953+
954+
deftest_set_activation_threshold__invalid_threshold_range(self):
955+
_testcapi=import_helper.import_module("_testcapi")
956+
parser=expat.ParserCreate()
957+
setter=functools.partial(self.set_activation_threshold,parser)
958+
959+
self.assertRaises(OverflowError,setter,_testcapi.ULLONG_MAX+1)
960+
961+
deftest_set_activation_threshold__fail_for_subparser(self):
962+
parser=expat.ParserCreate()
963+
subparser=parser.ExternalEntityParserCreate(None)
964+
setter=functools.partial(self.set_activation_threshold,subparser)
965+
self.assert_root_parser_failure(setter,12345)
966+
967+
@abc.abstractmethod
968+
deftest_set_maximum_amplification__amplification_exceeded(self):
969+
"""Test when the amplification factor is exceeded."""
970+
971+
@abc.abstractmethod
972+
deftest_set_maximum_amplification__amplification_not_exceeded(self):
973+
"""Test when the amplification factor is not exceeded."""
974+
975+
deftest_set_maximum_amplification__infinity(self):
976+
inf=float('inf')# an 'inf' threshold is allowed by Expat
977+
parser=expat.ParserCreate()
978+
self.assertIsNone(self.set_maximum_amplification(parser,inf))
979+
980+
deftest_set_maximum_amplification__invalid_max_factor_type(self):
981+
parser=expat.ParserCreate()
982+
setter=functools.partial(self.set_maximum_amplification,parser)
983+
984+
self.assertRaises(TypeError,setter,None)
985+
self.assertRaises(TypeError,setter,'abc')
986+
987+
deftest_set_maximum_amplification__invalid_max_factor_range(self):
988+
parser=expat.ParserCreate()
989+
setter=functools.partial(self.set_maximum_amplification,parser)
990+
991+
msg=re.escape("'max_factor' must be at least 1.0")
992+
self.assertRaisesRegex(expat.ExpatError,msg,setter,float('nan'))
993+
self.assertRaisesRegex(expat.ExpatError,msg,setter,0.99)
994+
995+
deftest_set_maximum_amplification__fail_for_subparser(self):
996+
parser=expat.ParserCreate()
997+
subparser=parser.ExternalEntityParserCreate(None)
998+
setter=functools.partial(self.set_maximum_amplification,subparser)
999+
self.assert_root_parser_failure(setter,123.45)
1000+
1001+
1002+
@unittest.skipIf(expat.version_info< (2,7,2),"requires Expat >= 2.7.2")
1003+
classMemoryProtectionTest(AttackProtectionTestBase,unittest.TestCase):
1004+
1005+
# NOTE: with the default Expat configuration, the billion laughs protection
1006+
# may hit before the allocation limiter if exponential_expansion_payload()
1007+
# is not carefully parametrized. As such, the payloads should be chosen so
1008+
# that either the allocation limiter is hit before other protections are
1009+
# triggered or no protection at all is triggered.
1010+
1011+
defassert_rejected(self,func,/,*args,**kwargs):
1012+
"""Check that func(*args, **kwargs) hits the allocation limit."""
1013+
msg=r"out of memory: line \d+, column \d+"
1014+
self.assertRaisesRegex(expat.ExpatError,msg,func,*args,**kwargs)
1015+
1016+
defset_activation_threshold(self,parser,threshold):
1017+
returnparser.SetAllocTrackerActivationThreshold(threshold)
1018+
1019+
defset_maximum_amplification(self,parser,max_factor):
1020+
returnparser.SetAllocTrackerMaximumAmplification(max_factor)
1021+
1022+
deftest_set_activation_threshold__threshold_reached(self):
1023+
parser=expat.ParserCreate()
1024+
# Choose a threshold expected to be always reached.
1025+
self.set_activation_threshold(parser,3)
1026+
# Check that the threshold is reached by choosing a small factor
1027+
# and a payload whose peak amplification factor exceeds it.
1028+
self.assertIsNone(self.set_maximum_amplification(parser,1.0))
1029+
payload=self.exponential_expansion_payload(ncols=10,nrows=4)
1030+
self.assert_rejected(parser.Parse,payload,True)
1031+
1032+
deftest_set_activation_threshold__threshold_not_reached(self):
1033+
parser=expat.ParserCreate()
1034+
# Choose a threshold expected to be never reached.
1035+
self.set_activation_threshold(parser,pow(10,5))
1036+
# Check that the threshold is reached by choosing a small factor
1037+
# and a payload whose peak amplification factor exceeds it.
1038+
self.assertIsNone(self.set_maximum_amplification(parser,1.0))
1039+
payload=self.exponential_expansion_payload(ncols=10,nrows=4)
1040+
self.assertIsNotNone(parser.Parse(payload,True))
1041+
1042+
deftest_set_maximum_amplification__amplification_exceeded(self):
1043+
parser=expat.ParserCreate()
1044+
# Unconditionally enable maximum activation factor.
1045+
self.set_activation_threshold(parser,0)
1046+
# Choose a max amplification factor expected to always be exceeded.
1047+
self.assertIsNone(self.set_maximum_amplification(parser,1.0))
1048+
# Craft a payload for which the peak amplification factor is > 1.0.
1049+
payload=self.exponential_expansion_payload(ncols=1,nrows=2)
1050+
self.assert_rejected(parser.Parse,payload,True)
1051+
1052+
deftest_set_maximum_amplification__amplification_not_exceeded(self):
1053+
parser=expat.ParserCreate()
1054+
# Unconditionally enable maximum activation factor.
1055+
self.set_activation_threshold(parser,0)
1056+
# Choose a max amplification factor expected to never be exceeded.
1057+
self.assertIsNone(self.set_maximum_amplification(parser,1e4))
1058+
# Craft a payload for which the peak amplification factor is < 1e4.
1059+
payload=self.exponential_expansion_payload(ncols=1,nrows=2)
1060+
self.assertIsNotNone(parser.Parse(payload,True))
1061+
1062+
8661063
if__name__=="__main__":
8671064
unittest.main()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Add:meth:`~xml.parsers.expat.xmlparser.SetAllocTrackerActivationThreshold`
2+
and:meth:`~xml.parsers.expat.xmlparser.SetAllocTrackerMaximumAmplification`
3+
to:ref:`xmlparser<xmlparser-objects>` objects to prevent use of
4+
disproportional amounts of dynamic memory from within an Expat parser.
5+
Patch by Bénédikt Tran.

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp