Skip to content

Integrate fragment parser options with trusted types - #12583

Open
noamr wants to merge 12 commits into
mainfrom
noamr/cpo
Open

Integrate fragment parser options with trusted types#12583
noamr wants to merge 12 commits into
mainfrom
noamr/cpo

Conversation

@noamr

@noamr noamr commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

In almost* all of the places where where TrustedTypes' createHTML is called,
we now also check for createParserOptions and call that if it's available, by calling "get trusted types compliant input" instead of "get trusted types compliant string".

The exceptions to this are srcdoc, document.write(), and the insertHTML execCommand. We can resolve separately as to whether these should also be sanitized with createParserOptions.

This now allows us to use the sanitizer in legacy markup insertion methods like the innerHTML setter, and to
disallow running scripts in createContextualFragment.

Since all markup insertion methods can now include a sanitizer, most of the steps from "set and filter HTML" are folded into the "fragment parsing algorithm steps", and it's assumed that there is always a sanitizer (though sometimes with an empty configuration).

Together with w3c/trusted-types#606

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


/dom.html ( diff )
/dynamic-markup-insertion.html ( diff )
/infrastructure.html ( diff )

@noamr noamr changed the title Integrate createParserOptions (draft) Integrate createParserOptions Jul 13, 2026
@noamr noamr changed the title Integrate createParserOptions Integrate fragment parser options with trusted types Jul 13, 2026
@noamr noamr closed this Jul 13, 2026
@noamr noamr reopened this Jul 13, 2026
@noamr
noamr marked this pull request as ready for review July 13, 2026 19:43
@noamr
noamr requested review from annevk, lukewarlow and zcorpan July 13, 2026 19:46
@noamr noamr added the agenda+ To be discussed at a triage meeting label Jul 15, 2026
@lukewarlow

Copy link
Copy Markdown
Member

Two things for the top comment, I don't think you do fold set and filter html anymore?

Also this also doesn't touch execCommand with the insertHTML command. (This is probably the right move because that isnt specced correctly to start with. But worth calling out probably).

Comment thread source
@noamr

noamr commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Two things for the top comment, I don't think you do fold set and filter html anymore?

Also this also doesn't touch execCommand with the insertHTML command. (This is probably the right move because that isnt specced correctly to start with. But worth calling out probably).

Thanks, OP updated.

@lukewarlow

Copy link
Copy Markdown
Member

Did we decide it was okay to not enforce the sanitizing if the document was XML?

@noamr

noamr commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Did we decide it was okay to not enforce the sanitizing if the document was XML?

That's what I understood but @mozfreddyb, @evilpie or @otherdaniel would know more. Sanitization is not specified for the XML parser.
Note that this is the status quo that is not changed here - what this does is allows you to sanitize when setting HTML with one of the old methods (and soon with streaming).

@otherdaniel

Copy link
Copy Markdown
Contributor

Did we decide it was okay to not enforce the sanitizing if the document was XML?

That's what I understood but @mozfreddyb, @evilpie or @otherdaniel would know more. Sanitization is not specified for the XML parser. Note that this is the status quo that is not changed here - what this does is allows you to sanitize when setting HTML with one of the old methods (and soon with streaming).

What I remember is that we specified Sanitizer API only for methods that would inherently only support HTML syntax. setHTML, setHTMLUnsafe, parseHTML, and parseHTMLUnsafe were all new methods, that simply don't support XML syntax. It's right there in the names. We didn't say what to do for XML syntax because we didn't have to.

This is probably best articulated in the hopelessly outdated explainer

Nearly all interesting bits are specified in terms of DOM & DOM operations, so I'd expect this to be easy to adapt to XML. But IMHO, application to XML-parser parse trees requires a second look, since it invalidates one of the assumptions we had when specifying any of this.


A silly example, but the only one I can think of: CDataSection in https://wicg.github.io/sanitizer-api/#sanitize-core step 1.1. That shouldn't be difficult to fix; but at least for now Sanitizer would assert-fail on (some) XML parse trees. In our implementation, there's a runtime assert there.

@lukewarlow

Copy link
Copy Markdown
Member

I guess my main concern is people defining a trusted types policy with this new function thinking it protects them and then it doesn't because they're in XHTML or something?

Assuming I'm reading this right you'd end up with a default policy explicitly setup to remove unsafe and then it actually no-ops when it's called by a legacy sync in XML.

@noamr

noamr commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

I guess my main concern is people defining a trusted types policy with this new function thinking it protects them and then it doesn't because they're in XHTML or something?
Assuming I'm reading this right you'd end up with a default policy explicitly setup to remove unsafe and then it actually no-ops when it's called by a legacy sync in XML.

You mean sink?

Yea it's limited in that way. But createHTML is still there... until we have some solution for this people should probably still use both in their policy or protect XML in other means.

@noamr

noamr commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

I guess my main concern is people defining a trusted types policy with this new function thinking it protects them and then it doesn't because they're in XHTML or something?
Assuming I'm reading this right you'd end up with a default policy explicitly setup to remove unsafe and then it actually no-ops when it's called by a legacy sync in XML.

You mean sink?

Yea it's limited in that way. But createHTML is still there... until we have some solution for this people should probably still use both in their policy or protect XML in other means.

I think that the specific guidance to developers to be to check the type of document when they create the default policy, use createHTML with the appropriate userland sanitizer if either this is an XML document or TrustedParserOptions is not supported, and createParserOptions otherwise

@lukewarlow

Copy link
Copy Markdown
Member

Non-authoratative LGTM. I'm still slightly unsure about the XML case mentioned above but if the consensus is that it's fine then I buy that.

@noamr noamr closed this Jul 23, 2026
@noamr noamr reopened this Jul 23, 2026
@noamr noamr removed the agenda+ To be discussed at a triage meeting label Jul 23, 2026
@zcorpan

zcorpan commented Aug 5, 2026

Copy link
Copy Markdown
Member

I asked chatgpt pro to help review the stack of 4 PRs. Sharing the response below; I haven't yet read through the PRs or this response in detail myself.

Prompt:

Help me review this stack of PRs:

#12583
#12753
#12756
#12758

Response

Review verdict

I would request changes on all four PRs in their current state. The stack itself is arranged correctly—#12583 targets main, and each subsequent PR targets the previous branch—but there are blocking correctness and security issues at every layer. Review the PRs commit-by-commit rather than comparing each one with main. ([GitHub]1)

The most serious stack-wide interaction is between #12756 and #12758:

  • Sanitize while parsing #12756’s normal element-insertion path invokes the sanitizer, but the parser’s special <script> path still inserts its newly created element directly.
  • Stream HTML into element or ShadowRoot #12758 initializes every streaming parser in Normal scripting mode, without consulting runScripts.
  • The accompanying tests expect runScripts: false and safe streaming methods not to execute scripts.

Taken together, the safe streaming path can preserve a <script> that bypassed sanitization and process it in an execution-enabled parser mode. That is the first issue I would raise. ([What PR]2)


#12583 — Trusted Types parser options

1. Blocking: template.innerHTML targets the wrong tree

setHTML() and setHTMLUnsafe() explicitly redirect a <template> target to its template contents. The new innerHTML setter instead invokes “set and filter HTML” with this, so it would replace the template element’s ordinary child list rather than template.content. ([What PR]3)

Suggested review comment:

This loses the required <template> redirection. The innerHTML setter invokes “set and filter HTML” with this, whereas for an HTMLTemplateElement the target needs to be this’s template contents, as the setHTML() and setHTMLUnsafe() algorithms already do. Otherwise template.innerHTML = ... operates on the wrong node tree. Please share a single “HTML insertion target” step and add a regression test that checks template.content.

2. Cross-spec blocker: the Trusted Types throwIfMissing condition appears reversed

The Trusted Types dependency currently sets throwIfMissing to true for a non-stream input and false for the Stream sentinel. The stated intent in the PR discussion is the opposite: stream methods need createParserOptions, whereas non-stream methods can already fall back through createHTML. This needs to be resolved before the stack lands. ([pr-preview.s3.amazonaws.com]4)

Suggested comment for the Trusted Types dependency:

The throwIfMissing polarity appears opposite to the stated intent. The algorithm currently uses true for non-Stream input and false for Stream, but the PR description says streaming sinks should throw when the default policy lacks createParserOptions, while non-stream sinks can rely on createHTML. Should step 3 be input is a Stream instead?

3. Security-contract question: parser options silently do not sanitize XML

The algorithm obtains a sanitizer, but the Legacy/XML branch returns the XML fragment-parser result before invoking sanitization. The discussion acknowledges this and advises policies to inspect the document type and sanitize XML themselves. The clarification is useful, but this remains an easy policy-authoring footgun: createParserOptions can appear to have protected a sink even though its sanitizer was ignored. ([What PR]3)

I would ask for:

  • A clear normative or security-considerations statement that sanitizer options are ignored by the XML fragment parser.
  • A WPT covering innerHTML in an XHTML/XML document under a default policy with createParserOptions.
  • Agreement on whether the policy should be invoked at all when its result cannot be applied.

4. Editorial bug

The insertAdjacentHTML(position, string) algorithm sends “the given value” to the Trusted Types algorithm, but this method’s argument is named string. ([What PR]3)

Nit: step 1 should pass the string argument, rather than “the given value”.


#12753 — Positional HTML methods

1. Blocking: helper contracts and call sites do not agree

The “filter and set HTML” helper is defined as (target, html, options, mode), but safe setHTML() passes (html, this, options, true), while unsafe setHTMLUnsafe() passes (compliantHTML, this, compliantOptions) and therefore defaults to Normal. Separately, the pre-insert and replace helpers forward a boolean into a parameter whose values are the Legacy, Unsafe, and Normal modes. ([What PR]5)

This is not only editorially ill-typed. If the algorithms are followed literally, true is not Normal, so the fragment-parser algorithm derives safe = false for the supposedly safe positional APIs.

Suggested review comment:

The helper contract and its call sites do not line up. “Filter and set HTML” is (target, html, options, mode), but safe setHTML() passes (html, this, options, true) and unsafe setHTMLUnsafe() passes (compliantHTML, this, compliantOptions). The pre-insert and replace helpers also pass a boolean where the fragment parser expects a Legacy/Unsafe/Normal mode. As written, the safe calls do not select Normal, while unsafe setHTMLUnsafe() defaults to Normal. Please make all three helpers consistently mode-based and add tests covering every safe/unsafe call site.

2. Blocking: replacement uses the replaced child as parser context

“Filter and replace with HTML” invokes fragment parsing with target, although the API prose says the child’s parent provides parser context. This matters for table, select, and other context-sensitive fragment parsing. It is also type-invalid for methods invoked on Text, Comment, or DocumentType: the helper says target must be an Element or DocumentFragment, but ChildNode applies to more node types. ([What PR]5)

Suggested review comment:

“Filter and replace with HTML” should parse against parent, not the node being replaced. ChildNode can be a Text, Comment, or DocumentType, none of which is a valid fragment-parser target, and the API prose explicitly says the parent supplies parser context. Please type the replacement target as Node, parse using the insertion parent, then replace the original node with the resulting fragment. A <tr>, <tbody>, and <select> context test would catch this.

3. Blocking: a Document parent is treated like no parent

“Get the parent for HTML insertion” only returns an Element or DocumentFragment. A node whose real parent is a Document is therefore mapped to null, and the positional method silently returns. That contradicts the PR’s own prose that invalid tree insertion throws HierarchyRequestError, as well as the associated WPT expectations. ([What PR]5)

Suggested review comment:

This helper conflates “disconnected” with “has a parent that is not an Element or DocumentFragment”. For example, calling a positional method on document.documentElement silently returns because its parent is a Document, instead of reaching the DOM insertion constraints and throwing HierarchyRequestError. Null should represent only a genuinely absent parent; the Document-parent case needs explicit behavior consistent with the prose and WPT.

4. <template>.setHTML() needs preservation through the refactor

The new “filter and set HTML” helper does not perform template-content redirection. After correcting the argument order, HTMLTemplateElement.setHTML() and setHTMLUnsafe() would still need to target the template contents rather than the element itself. The pre-insert helper does perform such a redirection, so the helpers are currently inconsistent. ([What PR]5)

Please move template-content target selection into a shared helper used by set, append, prepend, and eventually streaming methods. Handling it independently at call sites is already producing regressions elsewhere in the stack.


#12756 — Sanitize while parsing

1. Blocking: not every parser mutation passes through the sanitizer

The new generic element-insertion algorithm sanitizes the created element before insertion, but several special parser paths bypass it:

  • The special <script> branch creates and inserts the element directly.
  • The initial explicit and implicit <html> element paths append directly.
  • Duplicate <html> and <body> start tags merge attributes directly into existing elements.

That means safe parsing can retain a <script> or unsafe attributes such as event-handler attributes on the document element or body. ([What PR]2)

Suggested comment on the script branch:

The special <script> start-tag branch still directly inserts the newly created element rather than using the sanitizer-aware element-insertion algorithm. A sanitizer configured to remove <script> therefore never sees this node. Please route this path through the sanitizer and explicitly define what removal or replacement of the script means for tokenizer state, the stack of open elements, and subsequent script processing.

Suggested comment on document roots and attribute merging:

Sanitization is also bypassed by non-insertion mutations. The initial <html> element is created/appended directly, and duplicate <html>/<body> tokens merge attributes directly. Safe Document.parseHTML() can therefore retain attributes such as onclick or onload. Please add a sanitizer-aware operation for applying a token’s attributes to an existing element, and audit every tree-builder mutation path rather than only calls to the generic insertion algorithm.

2. The acknowledged is and declarative-shadow-root follow-ups are security-relevant

The PR explicitly says sanitization before element creation is still missing for declarative shadow roots and the is attribute. Those are not ordinary cleanup items: the parser can consult is during custom-element creation, and declarative shadow-root attributes can cause a shadow root to be attached before insertion-time sanitization gets a chance to remove them. ([GitHub]6)

Suggested review comment:

The PR describes pre-creation handling for declarative shadow roots and is as follow-up work, but these are precisely the cases where sanitizing only before DOM insertion is too late. A sanitizer decision needs to happen before custom-element lookup/creation and before shadow-root attachment. At minimum I would block the safe streaming APIs on those cases, with tests that observe custom-element construction and shadow-root creation.

Whether #12756 can land independently as parser infrastructure is a project judgment, but I would not allow #12758 to expose it as a “safe streaming” feature until those paths are resolved.

3. Observable text-node coalescing

The PR notes that sanitizing while parsing can change adjacent text-node coalescing. Since that is observable through childNodes, MutationObserver records, and later DOM operations, it deserves focused interoperability tests rather than only a release note in the PR body. ([GitHub]6)


#12758 — Streaming HTML insertion

1. Blocking: runScripts is never read

The common “stream HTML” algorithm always chooses Normal scripting mode and explicitly says this is to allow inline scripts to execute. It never examines options["runScripts"]. That contradicts the default runScripts: false contract and the accompanying tests, which expect false not to execute and true to execute. Safe streams use the same execution-enabled mode. ([What PR]7)

This is independently a problem for safe streaming into an existing <script> element: unlike the non-stream fragment-parser algorithm, the streaming helper has no early safe-mode rejection for a script context. The WPT explicitly permits a synchronous exception or a non-executing stream, but not execution. ([GitHub]8)

Suggested review comment:

stream HTML never reads options["runScripts"]; it unconditionally initializes the parser in Normal scripting mode. Safe streams and unsafe streams with the default/false option must not use an execution-enabled mode. Only unsafe streams with runScripts: true should do so. Please also carry over the safe-mode script-context guard from the ordinary fragment-parser algorithm. The current text contradicts both runScripts WPT and the safe-script-target WPT.

2. Blocking: the Trusted Types sentinel is passed as a literal string

The unsafe streaming methods call the Trusted Types algorithm with "Stream" in quotes. The Trusted Types dependency defines Stream as a special sentinel distinct from a string. Passing the string takes the non-stream branch, performs TrustedHTML conversion on the word “Stream”, and changes the throwIfMissing behavior. The returned compliant HTML component is then discarded. ([What PR]7)

Suggested review comment:

The Trusted Types dependency uses a special Stream sentinel, but these call sites pass the literal string "Stream". That takes the ordinary string-input path, including TrustedHTML conversion, and changes the parser-options fallback behavior. Please pass the actual sentinel and add a cross-spec WPT showing that createHTML is not called for streaming sinks and that missing createParserOptions has the intended result.

3. Blocking: all Element stream methods mishandle <template>

The methods use this directly as the insertion target. streamHTML() clears this, and streamPrependHTML() obtains this.firstChild; neither operation uses the template contents. The streaming helper itself has no template redirection. ([What PR]7)

Suggested review comment:

All Element streaming methods need to resolve the insertion target before clearing the target or computing referenceChild. For an HTMLTemplateElement, that target is the template contents. Currently streamHTML() clears the template element, while prepend reads template.firstChild, so both replacement and positioning operate on the wrong tree. Please reuse the target-selection algorithm from the non-stream setters and add coverage for every template stream method.

4. Blocking: destructive mutation happens before option validation

streamHTML() clears the destination before “stream HTML” validates or constructs the sanitizer. streamReplaceWithHTML() removes the original node before the same validation and parser initialization. Thus an invalid sanitizer configuration can synchronously throw after the existing DOM has already been destroyed. ([What PR]7)

Suggested review comment:

Please make synchronous failure atomic. streamHTML() currently clears the target, and streamReplaceWithHTML() removes the node, before sanitizer validation/parser construction. An invalid option can therefore throw after destructive mutation. Validate all options and construct the stream/parser first, then perform the initial DOM mutation.

5. Blocking: disconnected ChildNode behavior is undefined

The helper for locating an insertion parent can return null, but streamBeforeHTML() and streamAfterHTML() pass that null to “stream HTML”, which requires an Element or DocumentFragment. The replace methods attempt to remove the node from the null parent. Since these APIs return a WritableStream, the intended disconnected-node behavior needs an explicit design—silent no-op alone does not say what stream object should be returned. ([What PR]7)

Suggested review comment:

The ChildNode stream methods do not handle a missing insertion parent. Before/after pass null to an algorithm requiring an Element or DocumentFragment, and replace removes from null. Please define the observable disconnected-node behavior—e.g. a discarding/closed stream or a synchronous exception—and do so before any TT, sanitizer, parser, or DOM side effects.

6. Stream lifecycle needs specification

The WritableStream has writeAlgorithm and closeAlgorithm, but no abort algorithm. The reference child is validated on each write but not before EOF processing in closeAlgorithm; EOF can still cause tree-builder insertions after the reference child has been moved or removed. ([What PR]7)

Suggested comments:

referenceChild is checked on each write but not in closeAlgorithm. EOF processing can still generate tree-builder insertions, so moving or removing the reference child after the last write but before close leaves the redirect location stale. Please revalidate on close or define how this case is handled.

No abort algorithm is provided. Please define whether abort intentionally leaves already-inserted partial DOM, how parser state is terminated, and whether pending parser/script work is canceled. Add WPT for abort after partial tags, partial comments, and partial script input.

7. Fragment-parser status is underspecified

The streaming path calls “initialize an HTML fragment parser” directly. The parser specification still defines many “fragment case” branches in terms of a parser created for the purposes of the full HTML fragment parsing algorithm. The initializer creates a parser but does not explicitly mark it as such. This can affect custom-element construction and other document-versus-fragment branches. ([What PR]9)

Please introduce an explicit parser “fragment parser” flag, or normatively state that a parser returned by “initialize an HTML fragment parser” was created for the purposes of the fragment parsing algorithm. Relying on prose provenance becomes ambiguous now that streaming calls the initializer directly.

8. PR description needs replacement

The #12758 description currently repeats the positional-methods description, leaves the tests item empty, and contains a malformed checklist link. It does not explain stream semantics, incremental script execution, partial-DOM behavior, close/abort behavior, or its dependency on sanitize-while-parsing. ([GitHub]10)


Minimum test gate

Before promoting the relevant WPTs from tentative status, I would require focused coverage for:

  1. <template> for innerHTML, set, append, prepend, and every stream variant.
  2. Safe/unsafe × default/false/true runScripts, including scripts split across chunks.
  3. Safe parsing of <script>, root <html onclick>, and repeated <body onload> tokens.
  4. Text, Comment, and DocumentType replacement, plus table and select parser contexts.
  5. Child nodes under Element, DocumentFragment, Document, and no parent.
  6. Invalid sanitizer options proving the original DOM remains unchanged.
  7. Moving the stream reference child before a write and between the final write and close.
  8. Abort after partial markup.
  9. Customized built-ins using is and declarative shadow roots, with observable side-effect counters.
  10. Trusted Types policies containing only createHTML, only createParserOptions, both, or neither.

Recommended landing order

  1. Resolve the Trusted Types Add output of build-entities.sh #606 sentinel and throwIfMissing contract.
  2. Fix Integrate fragment parser options with trusted types #12583’s template regression and lock down XML behavior with tests.
  3. Repair Positional HTML methods #12753’s helper signatures, mode type, replacement context, and parent semantics.
  4. Audit every tree-builder mutation in Sanitize while parsing #12756 and settle pre-creation sanitization for is and declarative shadow roots.
  5. Land Stream HTML into element or ShadowRoot #12758 only after scripting-mode selection, template targets, atomic validation, disconnected nodes, close, and abort are fully specified.

The first comments I would post are the #12756 <script> sanitizer bypass, #12758 unconditional Normal scripting mode, and #12753 boolean-versus-mode mismatch. Those establish that the current safe-path guarantees are not yet upheld.

@zcorpan

zcorpan commented Aug 5, 2026

Copy link
Copy Markdown
Member
  1. Security-contract question: parser options silently do not sanitize XML

It seems reasonable to throw in this case.

@noamr

noamr commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

I asked chatgpt pro to help review the stack of 4 PRs. Sharing the response below; I haven't yet read through the PRs or this response in detail myself.

Prompt:

Help me review this stack of PRs:
#12583
#12753
#12756
#12758

Thanks! See below if you want to run it again. One of the issues is false, some are known limitations/existing issues, most are now fixed.

I'd be happy to learn if there are pending design issues while we work through the logical/editorial bits.

Response

#12583 — Trusted Types parser options

1. Blocking: template.innerHTML targets the wrong tree

Fixed

2. Cross-spec blocker: the Trusted Types throwIfMissing condition appears reversed

Fixed (together with w3c/trusted-types#606)

3. Security-contract question: parser options silently do not sanitize XML

Changed to throw when createParserOptions returns a sanitizer for XML.

4. Editorial bug

The insertAdjacentHTML(position, string) algorithm sends “the given value” to the Trusted Types algorithm, but this method’s argument is named string. ([What PR]3)

Fixed

Nit: step 1 should pass the string argument, rather than “the given value”.

Fixed

#12753 — Positional HTML methods

1. Blocking: helper contracts and call sites do not agree

Fixed

2. Blocking: replacement uses the replaced child as parser context

Fixed

3. Blocking: a Document parent is treated like no parent

“Get the parent for HTML insertion” only returns an Element or DocumentFragment. A node whose real parent is a Document is therefore mapped to null, and the positional method silently returns.

Fixed

4. <template>.setHTML() needs preservation through the refactor

Fixed

#12756 — Sanitize while parsing

1. Blocking: not every parser mutation passes through the sanitizer

The new generic element-insertion algorithm sanitizes the created element before insertion, but several special parser paths bypass it:

  • The special <script> branch creates and inserts the element directly.

I believe this is false. It goes through "insert node the adjusted insertion location" which sanitizes.

  • The initial explicit and implicit <html> element paths append directly.

Fixed

  • Duplicate <html> and <body> start tags merge attributes directly into existing elements.

Fixed

2. The acknowledged is and declarative-shadow-root follow-ups are security-relevant

Added an XXX note for now

3. Observable text-node coalescing

There are tests for this. I will point it out in the OP for streaming.

#12758 — Streaming HTML insertion

1. Blocking: runScripts is never read

Fixed

2. Blocking: the Trusted Types sentinel is passed as a literal string

Fixed

3. Blocking: all Element stream methods mishandle <template>

Fixed

4. Blocking: destructive mutation happens before option validation

Fixed

5. Blocking: disconnected ChildNode behavior is undefined

Fixed

6. Stream lifecycle needs specification

Fixed

7. Fragment-parser status is underspecified

Exisiting issue

8. PR description needs replacement

Ack

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.

4 participants