Skip to content

[2.7.18.16] Give Expat parsers a 16-byte random hash salt (CVE-2026-7210) - #105

Merged
icanhasmath merged 1 commit into
2.7.18.16from
2.7.18.16-expat-hash-salt
Oct 28, 2026
Merged

icanhasmath merged 1 commit into
2.7.18.16from
2.7.18.16-expat-hash-salt

Conversation

@icanhasmath

Copy link
Copy Markdown

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 to 2.7.18.16 once #104 merges.

Problem

pyexpat and cElementTree salt every parser with XML_SetHashSalt(parser, _Py_HashSecret.prefix). On 2.7, _Py_HashSecret is all zeros unless -R is used. A salt of 0 makes Expat generate its own, and our build defines XML_POOR_ENTROPY, so that salt comes from the time and pid. With 2.7.18.15:

$ EXPAT_ENTROPY_DEBUG=1 python -c "import pyexpat; pyexpat.ParserCreate().Parse('<a/>', True)"
expat: Entropy: fallback(8) --> 0x7fffffffffc792e4 (8 bytes)     # run 1
expat: Entropy: fallback(8) --> 0xdfffffffffc7dc21 (8 bytes)     # run 2

Fix

With Expat ≥ 2.8.0, pyexpat and _elementtree each read 16 bytes from _PyOS_URandom() at import, and pass them to every parser through XML_SetHashSalt16Bytes():

expat: Entropy: explicit(16) --> [0xb59df9d834359beb, 0x334b37ed14f0f358] (16 bytes)
  • As upstream, SetHashSalt16Bytes is appended to PyExpat_CAPI. It is NULL for Expat < 2.8.0.
  • With an older Expat, or if the OS random source fails, the code falls back to the old XML_SetHashSalt() call.
  • 2.7 difference: upstream reads the salt from a new 16-byte expat member of _Py_HashSecret. 2.7's _Py_HashSecret_t is 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.pyd links the external AS libexpat.lib and 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)

  • New test_pyexpat.HashSaltTest (pyexpat and cElementTree) runs a child interpreter with EXPAT_ENTROPY_DEBUG=1 and requires every parser's salt to be explicit(16).
  • test_pyexpat, test_xml_etree, test_xml_etree_c, test_minidom, test_sax and test_xmlrpc pass.
  • Windows x64: not built here.

@ezequielp-activestate ezequielp-activestate left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 -R gives explicit(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.
  • Fallbacks: if _PyOS_URandom fails, the error is cleared and the old XML_SetHashSalt(_Py_HashSecret.prefix) path is used. Expat < 2.8.0 keeps the old path at compile time.
  • C API: SetHashSalt16Bytes is appended at the end of PyExpat_CAPI and is NULL for older Expat, as upstream does. _elementtree checks it before use.
  • Tests: test_pyexpat (including the new HashSaltTest for 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
icanhasmath force-pushed the 2.7.18.16-expat-hash-salt branch from 6dcc367 to fb66720 Compare October 28, 2026 16:22
@icanhasmath
icanhasmath changed the base branch from 2.7.18.16-expat-2.9.0 to 2.7.18.16 October 28, 2026 17:04
@icanhasmath
icanhasmath merged commit 834ab93 into 2.7.18.16 Oct 28, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants