Skip to content

fix(parse): remove quadratic parsing paths and prototype-key corruption - #22

Merged
paustint merged 1 commit into
mainfrom
fix/parser-quadratic-and-proto-keys
Oct 31, 2026
Merged

paustint merged 1 commit into
mainfrom
fix/parser-quadratic-and-proto-keys

Conversation

@paustint

@paustint paustint commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes three issues in src/parse.ts found by a security scan of the repo. All three are reachable by anything that passes untrusted XML to parse().

  • Quadratic attribute regex (medium). /([^\s=]+)\s*=\s*(?:"…"|'…')/g retried from every offset inside a long run of name characters with no =. A tag of about 100 KB could block the event loop. Attributes are now read with one linear scan that accepts the same name = "value" / name = 'value' pairs.
  • Quadratic text flush (medium). At every <, the whole accumulated element text was re-trimmed and re-entity-decoded, so long text interrupted by many comments, PIs, CDATA sections or child tags cost O(text × markers). Only newly seen text is processed now.
  • Prototype-member names (low). Element or attribute names like __proto__, constructor and toString swapped the result's prototype, dropped the value, or wrapped the inherited member in an array (key in parent is true for inherited names). Keys are now stored with Object.defineProperty, and duplicates are detected with Object.hasOwn. Result objects keep the normal prototype, so consumers calling result.hasOwnProperty(...) are unaffected.

Behaviour change to review

Text that contained an entity-looking sequence after decoding was decoded twice if a < followed it. For example, <a>&amp;lt;<!---->x</a> gave <x, but <a>&amp;lt;x</a> gave &lt;x. Both now give &lt;x. This is noted under Fixed in the changelog.

A second, very narrow difference: when a numeric entity decodes to whitespace (such as &#32;) between two comments, that space is now kept.

Testing

  • Added tests for each fix, plus tests for attribute scanning edge cases (spaced =, single and double quotes, valueless names, unquoted values).
  • The six new regression tests fail on main (the long-tag test takes about 27 s there) and pass with this change.
  • npm run format:check, npm run lint, npm run typecheck and npm test (127 tests) pass.
  • [Unreleased] classifies as a patch bump.

- Scan attributes linearly instead of with a regex that backtracked from every offset in a long run of name characters
- Flush only newly seen text at each '<' instead of re-trimming and re-decoding the whole accumulated text, which also stops entities being decoded twice
- Store element and attribute names such as __proto__, constructor and toString as own data properties, and detect duplicates with Object.hasOwn

@paustint paustint left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🟢 Ready to approve

No issues found at confidence >= 8. The three fixes in src/parse.ts match what the PR and changelog describe, and I found no regression in the paths I traced.

Reviewed: 8554e88 — 3 files, +151/-29. Focus: the linear attribute scanner against the regex it replaces, the pending-text flush against the old flush-at-every-< behavior, and the setOwn / Object.hasOwn handling of prototype-member keys.

Checked and clean: attribute scanner accepts the same pairs as the old regex (a failed name fails identically from every suffix, so resuming after the name is equivalent; unterminated quotes, valueless names, unquoted values and a==b="c" all behave the same); text trim/decode semantics are unchanged except for the documented double-decode fix; leaf, mixed-content (#text), CDATA and trimValues: false paths; setOwn covers every assignment of a document-controlled key (child names, attribute names, #text); no existing review threads to dedupe; changelog heading classifies as a patch bump and the repo's formatting and commit conventions are followed. This was a static review: nothing was installed, built or run.


🤖 Final-gate review via Claude Code /pr-gatekeeper

@paustint
paustint merged commit 1ca6fa6 into main Oct 31, 2026
8 checks passed
@paustint
paustint deleted the fix/parser-quadratic-and-proto-keys branch October 31, 2026 18:05
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.

1 participant