Repository navigation
2.7.18.16: HTMLParser incremental feed is quadratic (CVE-2026-15308) - #103
Merged
Merged
Conversation
…eeding (CVE-2026-15308) Backport of python/cpython pythongh-153030 (3.14 07efb08) to 2.7.18.x. HTMLParser.feed() concatenated each chunk onto rawdata and rescanned the whole unparsed buffer. While a construct stays unterminated (a comment, processing instruction, doctype, CDATA section, start tag or RAWTEXT element such as <script>), every feed() rescans everything buffered so far, so feeding attacker-controlled data in small chunks takes quadratic time. On 2.7.18.15, 10000 64-byte chunks inside <script> take 22s (5.6s for 5000). feed() now collects chunks in a list and only joins and parses once the pending data reaches a threshold: 1 byte after a call that made progress, otherwise the current buffer size, so the buffer must double before it is rescanned. close() flushes pending data. 200000 chunks now take 0.1-0.3s per construct. As upstream: when a feed() makes no progress, the events for data in the next small feeds may be reported later (once the buffer doubles, or at close()); no data is lost. 2.7.18.x: Lib/HTMLParser.py; del list[:] instead of list.clear(). The cpu-gated upstream test goes in a new IncrementalFeedTestCase, plus a test that char-by-char feeding still yields the same events and that close() delivers held-back data. sgmllib/htmllib keep their own feed() and are not changed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
ezequielp-activestate
approved these changes
Oct 28, 2026
ezequielp-activestate
left a comment
There was a problem hiding this comment.
No blockers. The change is upstream's pythongh-153030 fix (bcf98dd), with del lst[:] for 2.7, and Lib/html/parser.py hasn't changed upstream since. Mixed str/unicode chunks coerce in ''.join() the same way they did in rawdata + data.
-
Repro: an open construct followed by 10,000 64-byte
feed()calls, thenclose():construct base this branch <script>39.2 s 0.03 s <!--5.1 s 0.01 s <?x1.9 s 0.01 s -
Events: 400 random documents fed in random 1–8 character chunks give the same events as feeding each document whole, with adjacent data merged.
-
Tests:
test_htmlparser(with-ucpu),test_htmllibandtest_robotparserpass. Against the baseHTMLParser.py, the new cpu-gated test did not finish within 300 s.
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 python/cpython pythongh-153030 (CVE-2026-15308, PSF-2026-33) to 2.7.18.x.
Problem:
HTMLParser.feed()appended each chunk torawdataand rescanned everything that hadn't been parsed yet. If a construct stays open (a comment,<?…, doctype, CDATA, a start tag, or<script>), feeding small chunks takes quadratic time. On 2.7.18.15, 10,000 64-byte chunks inside<script>take 22s, versus 5.6s for 5,000.Fix:
feed()collects chunks in a list and only re-parses once enough has accumulated. If the last parse made no progress, it waits until the buffer doubles.close()flushes everything. With the fix, 200,000 chunks (12.8 MB) take 0.1–0.3s per construct.Behavior note (same as upstream): after a
feed()that makes no progress, events for the next small feeds can arrive later: when the buffer doubles, or atclose(). No data is lost.Tests:
IncrementalFeedTestCase.cpuresource, as upstream does.close()delivers held-back data.test_htmlparserpasses 43/43 with-ucpu, andtest_htmllib,test_sgmllib,test_robotparserandtest_pydocpass.sgmllib/htmllibhave their ownfeed()and are unchanged.Part of 2.7.18.16. It targets the
2.7.18.16integration branch.