Skip to content

fix(core): reparse current markup in sanitizer stabilization loop#69836

Open
arshsmith1 wants to merge 1 commit into
angular:mainfrom
arshsmith1:sanitizer-stabilization-reparse
Open

fix(core): reparse current markup in sanitizer stabilization loop#69836
arshsmith1 wants to merge 1 commit into
angular:mainfrom
arshsmith1:sanitizer-stabilization-reparse

Conversation

@arshsmith1

Copy link
Copy Markdown
Contributor

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • angular.dev application / infrastructure changes
  • Other... Please describe:

What is the current behavior?

Issue Number: N/A

  1. In _sanitizeHtml, parsedHtml is read from an inertBodyElement that was parsed from the previous iteration's string. From the second iteration onward it recomputes the value just assigned to unsafeHtml, so while (unsafeHtml !== parsedHtml) compares a value against itself and the loop always exits after exactly two iterations, whether or not the markup reached a parse fixed point. A consequence worth calling out: mXSSAttempts can never reach 0, so the input is unstable throw is unreachable today.

  2. DOMParserHelper.getInertBodyElement reassigns html to the <body><remove></remove> prefixed form and then passes that prefixed string to the InertDocumentHelper fallback, which never strips the sentinel. Each call adds another <remove>, so the fallback path has no fixed point either. The broken loop above was masking this.

<plaintext>a shows the first one: the tokenizer never leaves the PLAINTEXT state, so each reparse appends another literal </plaintext> and the markup never stabilizes, but sanitization currently reports it as stable and serializes a tree it never re-checked.

What is the new behavior?

The loop reparses the current string before comparing, so stabilization is measured against the value actually being tested, and the attempt guard becomes reachable. The fallback keeps the prefix in a separate local and receives the original markup.

I checked the blast radius before/after over 20k generated tag-soup inputs against the real sanitizer: for every input that stabilizes, output is byte-identical. The only inputs whose behavior changes are ones that genuinely have no fixed point (<plaintext>), which now hit the guard instead of silently returning. Excluding <plaintext> from the generator, 20000/20000 are identical. Existing html_sanitizer_spec and url_sanitizer_spec pass unchanged; the added spec fails on main and passes here.

Happy to drop the added <plaintext> spec if you would rather not pin that behavior, since it is the one case where callers see a throw they did not see before.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

The mXSS stabilization loop read `parsedHtml` from an inert body element
that was parsed from the previous iteration's string, so from the second
iteration onward it recomputed the value it had just assigned to
`unsafeHtml` and the comparison was always trivially equal. The loop
exited after two iterations whether or not the markup reached a parse
fixed point, which also left the `mXSSAttempts` guard unreachable.
Reparse the current string inside the loop before comparing it.

`DOMParserHelper` separately handed the `<body><remove></remove>` prefixed
markup to the `InertDocumentHelper` fallback, which does not strip that
sentinel, so the fallback accumulated another `<remove>` on every call and
could never reach a fixed point. Keep the prefix in a separate local so
the fallback receives the original markup.
@angular-robot angular-robot Bot added the area: core Issues related to the framework runtime label Jul 18, 2026
@ngbot ngbot Bot added this to the Backlog milestone Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: core Issues related to the framework runtime

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant