Repository navigation
[2.7.18.16] Give Expat parsers a 16-byte random hash salt (CVE-2026-7210) - #105
Merged
Merged
Conversation
ezequielp-activestate
approved these changes
Oct 28, 2026
ezequielp-activestate
left a comment
There was a problem hiding this comment.
Approving. This is a correct port of upstream pythongh-149018 (CVE-2026-7210) to 2.7. It depends on the Expat 2.9.0 PR it's stacked on, which currently has a blocker in the Windows _elementtree build. That blocker is in the vendored-sources wiring, not in this change.
Checked (Linux, Python 2.7, bundled Expat 2.9.0)
- Salt source,
EXPAT_ENTROPY_DEBUG=1:- Without this change, every pyexpat and cElementTree parser gets
fallback(8)(time and pid), and-Rgivesexplicit(8). - With it, every parser gets
explicit(16)from 16 bytes of_PyOS_URandom. Parsers in the same process share one salt, and each process gets a new one, with or without-R.
- Without this change, every pyexpat and cElementTree parser gets
- Fallbacks: if
_PyOS_URandomfails, the error is cleared and the oldXML_SetHashSalt(_Py_HashSecret.prefix)path is used. Expat < 2.8.0 keeps the old path at compile time. - C API:
SetHashSalt16Bytesis appended at the end ofPyExpat_CAPIand is NULL for older Expat, as upstream does._elementtreechecks it before use. - Tests: test_pyexpat (including the new
HashSaltTestfor pyexpat and cElementTree), test_xml_etree, test_xml_etree_c, test_minidom, test_sax and test_xmlrpc pass. - Upstream: the only other commits for pythongh-149018 are the 3.11–3.14 backports of the same change.
Not assessed: Windows. pyexpat.pyd there calls XML_SetHashSalt16Bytes from the external AS libexpat.lib, so that library must be ≥ 2.8.0. The description says it's 2.8.4, which I didn't verify.
…026-7210) Backport of python/cpython pythongh-149018 (24b8f12; 3.10: ea70712) to 2.7.18.x. pyexpat and cElementTree seed every Expat parser's hash-flooding protection with XML_SetHashSalt(parser, _Py_HashSecret.prefix). On 2.7 _Py_HashSecret is all zeros unless hash randomization (-R) is on. A salt of 0 makes Expat generate its own, and our build defines XML_POOR_ENTROPY, so it comes from the time and pid. EXPAT_ENTROPY_DEBUG=1 shows "fallback(8)" salts whose upper bits are the same on every run. Even with -R the salt is at most one long. With Expat >= 2.8.0, pyexpat and _elementtree now read 16 bytes from _PyOS_URandom() once at import, and pass them to every parser through XML_SetHashSalt16Bytes(). As upstream, the function is added to the end of the PyExpat_CAPI struct, which is NULL for Expat < 2.8.0. If the OS random source fails, or with an older Expat, the old XML_SetHashSalt() path is used. 2.7.18.x: upstream reads the salt from a new 16-byte expat member of _Py_HashSecret. 2.7's _Py_HashSecret_t is two longs, and changing it would alter that exported struct, so each module keeps its own random salt instead. _elementtree uses the 16-byte path only through the pyexpat CAPI (USE_PYEXPAT_CAPI, which setup.py and the Windows project both define). Needs Expat >= 2.8.0 (the bundled copy is 2.9.0). On Windows, pyexpat links the external AS libexpat, which must then also be >= 2.8.0 (the current platform build uses 2.8.4). The new test_pyexpat.HashSaltTest runs pyexpat and cElementTree under EXPAT_ENTROPY_DEBUG=1 and requires every parser's salt to be "explicit(16)". With bundled Expat 2.9.0 and no fix, both tests fail with fallback(8). With the fix, both pass. Both are skipped for Expat < 2.8.0. Co-Authored-By: Claude Opus 5.5 <[email protected]>
icanhasmath
force-pushed
the
2.7.18.16-expat-hash-salt
branch
from
October 28, 2026 16:22
6dcc367 to
fb66720
Compare
icanhasmath
added a commit
that referenced
this pull request
Oct 28, 2026
Resolve the test_pyexpat.py conflict with #105: both add a test class after ForeignDTDTests and register it in test_main(); keep both ElementDeclHandlerTest and HashSaltTest. Modules/pyexpat.c merged cleanly.
This was referenced Oct 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backport of upstream pythongh-149018 (CVE-2026-7210): Expat's hash-flooding salt was too weak. Part of 2.7.18.16 (Linear PROD-1588).
Stacked on #104 (Expat 2.9.0). The base branch is
2.7.18.16-expat-2.9.0; retarget it to2.7.18.16once #104 merges.Problem
pyexpat and cElementTree salt every parser with
XML_SetHashSalt(parser, _Py_HashSecret.prefix). On 2.7,_Py_HashSecretis all zeros unless-Ris used. A salt of 0 makes Expat generate its own, and our build definesXML_POOR_ENTROPY, so that salt comes from the time and pid. With 2.7.18.15:Fix
With Expat ≥ 2.8.0, pyexpat and
_elementtreeeach read 16 bytes from_PyOS_URandom()at import, and pass them to every parser throughXML_SetHashSalt16Bytes():SetHashSalt16Bytesis appended toPyExpat_CAPI. It is NULL for Expat < 2.8.0.XML_SetHashSalt()call._Py_HashSecret. 2.7's_Py_HashSecret_tis an exported struct of two longs, so each module keeps its own process-wide random salt instead.Dependency
This needs Expat ≥ 2.8.0. The bundled copy is 2.9.0 after #104. On Windows,
pyexpat.pydlinks the external ASlibexpat.liband uses the bundled headers, so that library must be ≥ 2.8.0. The platform's current expat is 2.8.4, which meets that.Testing (Linux x86_64, prod OpenSSL 1.1.1w, bundled expat 2.9.0)
test_pyexpat.HashSaltTest(pyexpat and cElementTree) runs a child interpreter withEXPAT_ENTROPY_DEBUG=1and requires every parser's salt to beexplicit(16).fallback(8).