Skip to content

Sanitize while parsing - #12756

Open
noamr wants to merge 35 commits into
noamr/positional-htmlfrom
noamr/streaming-sanitizer
Open

Sanitize while parsing#12756
noamr wants to merge 35 commits into
noamr/positional-htmlfrom
noamr/streaming-sanitizer

Conversation

@noamr

@noamr noamr commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Instead of parsing into a fragment and then statically sanitizing that fragment, sanitize as we parse.
This entails the following changes:

  • The sanitizer config is propagated to the parser and kept as a new parser flag ("parser sanitizer configuration").
  • The "safe" flavor of sanitization is kept as a boolean flag in the parser ("remove javascript navigation URLs"), as that is a special safety check not kept in the sanitizer configuration.
  • The fragment created to hold the result is created in the inert document when sanitizing to avoid creation-time side effects (see Nodes are made non-inert before sanitizing #12560).
  • Instead of a recursive "sanitize" algorithm operating on a node tree, we define a "sanitize" algorithm that operates on a single Element, along with two helper checks: "sanitizer config allows comments" and "sanitizer config allows processing instruction target" (other node types do not need to check the sanitizer).
  • For "replace with children", we replaced the parser's "root insertion target" flag with an "insertion target redirection map" (mapping a node to an insertion location). This map is used to redirect insertions from the root dummy element to the target DocumentFragment, and also to redirect elements to their nearest non-replaced ancestor when a child is stripped but its children are kept.

Note that this still leaves out important follow ups, where we need to sometimes sanitize before the element is even created (e.g. declarative shadow roots and is), and having a live sanitizer config while parsing gives us the infrastructure for that.

Closes #12560
Closes #12543

(See WHATWG Working Mode: Changes for more details.)


/dom.html ( diff )
/dynamic-markup-insertion.html ( diff )
/iframe-embed-object.html ( diff )
/infrastructure.html ( diff )
/parsing.html ( diff )
/timers-and-user-prompts.html ( diff )

@noamr noamr closed this Aug 4, 2026
@noamr noamr reopened this Aug 4, 2026
@noamr noamr mentioned this pull request Aug 4, 2026
5 tasks
@noamr
noamr force-pushed the noamr/streaming-sanitizer branch from 7b105e3 to e24c23b Compare August 5, 2026 09:31
@noamr
noamr force-pushed the noamr/streaming-sanitizer branch 3 times, most recently from 4ff3705 to 8551681 Compare August 7, 2026 20:17

@zcorpan zcorpan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Missing null check; nits.

Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Comment thread source
Comment thread source Outdated
Comment thread source Outdated
Comment thread source
Comment thread source Outdated
Comment thread source Outdated
Comment thread source
Comment thread source
@noamr
noamr force-pushed the noamr/streaming-sanitizer branch 2 times, most recently from 1d9e935 to 4453c2a Compare August 12, 2026 16:07
@noamr noamr closed this Aug 13, 2026
@noamr noamr reopened this Aug 13, 2026
Comment thread source Outdated
Comment on lines 150315 to 150323
<li><p>Create a new <span>HTML parser</span> whose <span>parser sanitizer configuration</span> is
<var>configuration</var>, <span>allow declarative shadow roots</span> is
<var>allowDeclarativeShadowRoots</var>, <span>scripting mode</span> is
<var>scriptingMode</var>, and <span>remove javascript navigation URLs</span>
is <var>safe</var>, and associate it with <var>document</var>.</p></li>

<li><p>If <var>contextDocument</var>'s <span data-x="concept-n-script">scripting is
disabled</span>, then set <var>scriptingMode</var> to <span
data-x="scripting-mode-disabled">Disabled</span>.</p></li>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

From chatgpt's re-review:

The context document’s scripting-disabled state is still applied only after constructing the parser. The following step mutates the local scriptingMode; it does not update the parser created in the previous step. Please calculate the final mode before parser construction, or explicitly update the parser’s stored mode afterward. This commit should be correct independently of #12758. A <noscript> fragment test with scripting disabled would catch the ordering error.

@noamr noamr Aug 14, 2026

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.

Switched the order, should be OK now (this is also changed in the next PR in the stack)

@noamr
noamr force-pushed the noamr/streaming-sanitizer branch from 4453c2a to 931141c Compare August 14, 2026 08:52
@noamr
noamr requested a review from zcorpan August 14, 2026 08:57
Comment thread source
Comment on lines +147523 to +147525
<li><p>If <var>lastNode</var> is non-null, insert whatever <var>lastNode</var> ended up being
in the previous step at the <span>adjusted insertion location</span> given
<var>insertionLocation</var>.</p></li>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this insert lastNode without a sanitizer check?

data:text/html,<!DOCTYPE html><body><div id=div></div><script>div.setHTMLUnsafe('<b><div><b>x', { sanitizer: {removeElements: ['div'] }});</script>

  1. <b> is pushed
  2. <div> is created, sanitize removes, so insert is aborted but its still pushed
  3. </b> runs AAA, furthestBlock is removed div, commonAncestor is b.
  4. Inner loop breaks, lastNode is furthestBlock
  5. This step insets removed div into commonAncestor.

Perhaps worth a test at least.

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.

Good catch! This is fixed now (Adding tests as part of https://chromium-review.git.corp.google.com/c/chromium/src/+/8250959, once they're in I'll add them to the un-tentative PR)

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.

Added multiple test cases for this: web-platform-tests/wpt@4409cc2

(They're not tentative as they should be sanitized with the post-processing sanitizer as well)

Comment thread source
Comment on lines +144993 to +144995
<li><p>If the parser's <span>insertion target redirection map</span>[<var>targetParent</var>]
does not <span data-x="map exists">exist</span>, then return
<var>adjustedInsertionLocation</var>.</p></li>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So here, as removed elements never end up in the map, this can return early and - as the adjusted insertion location becomes a detached element - all children end up being added to that. While it's unobservable, it's a bit slow - but if we added a map of removed elements we could do something like:

If the parser's sanitizer-removed elements contains targetParent, then return null.

To no-op sooner, when targetParent is detatched. Of course implementations could do this and still be conformant but it may be nice to either specify or at least comment & hint at it.

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.

I don't know about adding more non-observable optimizations to the spec just for the purpose of AAA... seems like an additional potential point of failure.

@noamr
noamr force-pushed the noamr/streaming-sanitizer branch from 32e5b9a to 1b77b8d Compare August 15, 2026 10:04
brave-builds pushed a commit to brave/chromium that referenced this pull request Aug 17, 2026
Sanitization should take place while performing the adoption agency
algorithm, otherwise some nodes can be missed.

Since adoption agency often adds an intermediate element,
the ReplaceWithChildren directive essentially negates it,
while Remove reparents the children into the intermediate
element and removes all of them.

This is in line with spec changes made as part of
whatwg/html#12756

Bug: 498272014
Change-Id: Ieb755555635e197f8ed7daf06a08679a0f2d38c3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8250959
Commit-Queue: Noam Rosenthal <nrosenthal@google.com>
Reviewed-by: Daniel Vogelheim <vogelheim@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1680770}
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this pull request Aug 17, 2026
Sanitization should take place while performing the adoption agency
algorithm, otherwise some nodes can be missed.

Since adoption agency often adds an intermediate element,
the ReplaceWithChildren directive essentially negates it,
while Remove reparents the children into the intermediate
element and removes all of them.

This is in line with spec changes made as part of
whatwg/html#12756

Bug: 498272014
Change-Id: Ieb755555635e197f8ed7daf06a08679a0f2d38c3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8250959
Commit-Queue: Noam Rosenthal <nrosenthal@google.com>
Reviewed-by: Daniel Vogelheim <vogelheim@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1680770}
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this pull request Aug 17, 2026
Sanitization should take place while performing the adoption agency
algorithm, otherwise some nodes can be missed.

Since adoption agency often adds an intermediate element,
the ReplaceWithChildren directive essentially negates it,
while Remove reparents the children into the intermediate
element and removes all of them.

This is in line with spec changes made as part of
whatwg/html#12756

Bug: 498272014
Change-Id: Ieb755555635e197f8ed7daf06a08679a0f2d38c3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8250959
Commit-Queue: Noam Rosenthal <nrosenthal@google.com>
Reviewed-by: Daniel Vogelheim <vogelheim@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1680770}
This behavior was never shipped, due to compatibility issues. Effectively reverts 5f626a1 & 7c8fb86.
@noamr
noamr force-pushed the noamr/streaming-sanitizer branch from 1b77b8d to 2eb51b7 Compare August 19, 2026 16:22
lando-worker Bot pushed a commit to mozilla-firefox/firefox that referenced this pull request Aug 21, 2026
…ency edge cases, a=testonly

Automatic update from web-platform-tests
Fix some streaming-sanitizer/adoption-agency edge cases

Sanitization should take place while performing the adoption agency
algorithm, otherwise some nodes can be missed.

Since adoption agency often adds an intermediate element,
the ReplaceWithChildren directive essentially negates it,
while Remove reparents the children into the intermediate
element and removes all of them.

This is in line with spec changes made as part of
whatwg/html#12756

Bug: 498272014
Change-Id: Ieb755555635e197f8ed7daf06a08679a0f2d38c3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8250959
Commit-Queue: Noam Rosenthal <nrosenthal@google.com>
Reviewed-by: Daniel Vogelheim <vogelheim@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1680770}

--

wpt-commits: 4409cc231d390712ee52ffb5529177ebb0cd0da3
wpt-pr: 61987
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Nodes are made non-inert before sanitizing Streaming sanitizer support

3 participants