Skip to content

2.7.18.16: ElementPath index predicates are O(n^2) (CVE-2026-6879) - #101

Merged
icanhasmath merged 1 commit into
2.7.18.16from
2.7.18.16-elementpath-index
Oct 28, 2026
Merged

icanhasmath merged 1 commit into
2.7.18.16from
2.7.18.16-elementpath-index

Conversation

@icanhasmath

Copy link
Copy Markdown

Backport of python/cpython pythongh-152674 (CVE-2026-6879, PSF-2026-34) to 2.7.18.x.

Problem: for index predicates ([1], [last()], [last()-N]), ElementPath called parent.findall(elem.tag) once for every candidate element. That makes findall() / iterfind() quadratic in the number of same-tag siblings.

Fix: cache the selected sibling for each (parent, tag) pair.

2.7 notes:

  • In 2.7, cElementTree's find()/findall() also go through this Python ElementPath, so the fix covers both implementations.
  • The test is skipped for cElementTree, because its Element is a factory function in 2.7 and can't be subclassed.

Test: test_find_xpath_index_no_quadratic_complexity.

  • Without the fix, findall calls grow as 1025/2049/4097 for 1024/2048/4096 siblings, so the test fails.
  • With the fix, the count stays constant, and test_xml_etree and test_xml_etree_c pass.

Part of 2.7.18.16. It targets the 2.7.18.16 integration branch.

…ates (CVE-2026-6879)

Backport of python/cpython pythongh-152674 (2ffab08) to 2.7.18.x.

For an index predicate ([1], [last()], [last()-N]) ElementPath called
parent.findall(elem.tag) once per candidate element, so findall() and a
fully consumed iterfind() over n same-tag siblings did O(n^2) work.  A
crafted document plus a query like ".//item[last()]" could burn CPU.
select() now caches the selected sibling per (parent, tag) pair.

2.7.18.x: cElementTree's find()/findall() also go through this Python
ElementPath module, so the fix covers both implementations.  The test
counts findall() calls through an Element subclass; cElementTree's
Element is a factory function in 2.7 and cannot be subclassed, so the
test is skipped there.  super(Class, self) and %-formatting replace the
3.x spellings.  Without the fix, findall() calls grow as 1025/2049/4097
for 1024/2048/4096 siblings; with it they are constant.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@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.

No blockers. The change is upstream's pythongh-152674 fix (2ffab08). The only difference is a harmless list(), and ElementPath.py hasn't changed upstream since.

  • Repro: findall with c[1], c[last()] and c[last()-3] over N same-tag siblings:

    N base this branch
    1,024 3,075 findall calls, 0.9 s 6 calls, 0.01 s
    4,096 12,291 calls, 22 s 6 calls, 0.04 s
    16,384 49,155 calls, 458 s 6 calls, 0.15 s
  • Correctness: on 300 random trees, index predicates ([1], [2], [last()], [last()-1], */b[1], .//*[1]) give the same elements as a naive reference, on both base and this branch.

  • Tests: test_xml_etree and test_xml_etree_c pass. The new test fails against the base ElementPath.py.

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