Skip to content

Integrate Trusted Types enforcement into attribute handling #1268

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

lukewarlow
Copy link
Member

@lukewarlow lukewarlow commented Mar 27, 2024

This updates the DOM spec to add the neccessary integration with the Trusted Types spec to ensure attribute values are protected by TT enforcement.

The Element.setAttribute() and Element.setAttributeNs() method steps, along with the "set an attribute" algorithm (which is used by Element.setAttributeNode(), Element.setAttributeNodeNs(), NamedNodeMap.setNamedItem() and NamedNodeMap.setNamedItemNs()), and the "set an existing attribute value" algorithm (which is used by Attr.value, Attr.nodeValue, and Attr.textContent), are all updated to allow Trusted Type's to verify the updated attribute value before it's set.

The IDL definitions for Element.setAttribute() and Element.setAttributeNs() are both updated to take a TrustedType object (can be all 3 types) in addition to DOMString, these two methods are the only "blessed" way to update an attribute value when Trusted Types is enforced.

See and #789. Supercedes #809 and #1247

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


Preview | Diff

@lukewarlow
Copy link
Member Author

This is a clone of #1247 where I will finish any outstanding work to get this across the line

@lukewarlow lukewarlow force-pushed the trusted-types-attributes branch 2 times, most recently from 3562fcb to ac5b4aa Compare April 10, 2024 15:52
@lukewarlow lukewarlow force-pushed the trusted-types-attributes branch from 524d8cd to f8877b6 Compare April 11, 2024 12:18
@lukewarlow lukewarlow marked this pull request as ready for review April 11, 2024 12:21
@lukewarlow
Copy link
Member Author

See also #1258 which is another integration point with the DOM spec that we need for TT.

@lukewarlow lukewarlow marked this pull request as draft April 15, 2024 10:22
@lukewarlow lukewarlow force-pushed the trusted-types-attributes branch from f8877b6 to ee9915e Compare April 15, 2024 11:45
dom.bs Outdated
<li><p><a>Validate and set attribute value</a> <var>attr</var>'s <a for="Attr">value</a> for
<var>attr</var> with <var>element</var>.

<li><p>If <var>element</var> <a lt="has an attribute">has</a> an <a>attribute</a>

Choose a reason for hiding this comment

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

I still find it difficult to wrap my head around this logic. I think conceptually, we want to have the old value -- from before the call, and thus before a default policy might have mucked with it. That's what should go into the change attribute logic. But once we have that, I'm not sure why we'd need to throw an exception here. I'm not sure why we'd have to care whether the default policy does anything funny with the attribute in the mean time.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've read through this spec path and I believe that, if the default policy removes the existing attribute node, then this will call replace an attribute, which in turn calls replace a list item, which results in a no-op because the old value no longer exists to be replaced.

So I think in this situation you're probably right the spec doesn't need to do anything, will make that change.

Copy link
Member Author

Choose a reason for hiding this comment

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

Having said that I'm slightly uneasy about stuff like attribute change steps still firing and how that all works spec wise.

Both Chromium and WebKit don't actually follow the spec 100% here and so I think would actually result in a different behaviour to this spec (the index lookup happens after the TT call) so that's also not ideal.

@lukewarlow lukewarlow marked this pull request as ready for review April 22, 2024 12:53
@lukewarlow lukewarlow requested review from annevk and smaug---- April 22, 2024 13:13
@lukewarlow
Copy link
Member Author

I think I've addressed all the comments from #1247.

I do want to point out Chrome and WebKit don't (or at least not in a way obvious to me) 100% follow the flow of the spec and as a result this may result in differences specifically in weird cases with attribute mutation.

So that bit especially it would be good to get feedback on.

It's also worth being aware that like Chromium's implementation this spec means that certain ways to update a nodes value don't work with a trusted type object as a user might expect. (e.g. iframe.getAttributeNode('srcdoc').value = trustedHTMLObj; will throw unless allowed by a default policy). I think in pratice this will be fine, and in some cases is the only real option we have without nodes keeping track of whether they're trusted or not (which would add lots of complexity)

Copy link
Member

@annevk annevk left a comment

Choose a reason for hiding this comment

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

This change is either incomplete or makes many cosmetic changes that would be best proposed separately as they confuse me quite a bit.

@lukewarlow lukewarlow force-pushed the trusted-types-attributes branch from 370f5c9 to 4421d50 Compare April 22, 2024 15:46
@lukewarlow
Copy link
Member Author

This change is either incomplete or makes many cosmetic changes that would be best proposed separately as they confuse me quite a bit.

Apologies I thought I'd reverted all of these changes but I missed a few, it's because I changed stuff and then changed it back and this led to some wonky diffs. Have hopefully reverted all of these unnecessary changes

@lukewarlow lukewarlow force-pushed the trusted-types-attributes branch 2 times, most recently from 1d42460 to 1eeaf00 Compare April 22, 2024 15:52
dom.bs Outdated
<li><p>If <var>attribute</var>'s <a for=Attr>element</a> <a lt="has an attribute">has</a>
an <a>attribute</a> <var>attribute</var>, then <a>handle attribute changes</a> for
<var>attribute</var> with <var>attribute</var>'s <a for=Attr>element</a>, <var>oldValue</var>, and
<var>value</var>.
Copy link
Member

Choose a reason for hiding this comment

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

Should this be value or attribute's value? They can be different, right?

@lukewarlow
Copy link
Member Author

lukewarlow commented May 7, 2024

@otherdaniel, @annevk , and @smaug---- regarding the case where the default policy changes assumptions about the existence of an attribute mid-way through what would you prefer the spec say to do? Currently I've specced to throw, but Chromium currently re-looks up the index (spec doesn't explicitly work on an index basis but Chromium and WebKit do)

@annevk
Copy link
Member

annevk commented May 13, 2024

Attributes are stored in a list and those do have indices per Infra. What am I missing?

@lukewarlow
Copy link
Member Author

Sorry I mean algorithmically the spec and implementations don't follow the same flow. So it's trickier to reason between the spec and implementation. This might just be my lack of familiarity with these APIs too.

@annevk
Copy link
Member

annevk commented May 13, 2024

The path of least resistance is prolly matching Chromium. Introducing new paths that throw is always risky. If you are looking for guidance as to how, I'd need quite a bit more context to provide helpful suggestions.

@lukewarlow lukewarlow force-pushed the trusted-types-attributes branch 2 times, most recently from 4089eaf to 02db8c7 Compare May 16, 2024 15:41
@lukewarlow lukewarlow requested a review from otherdaniel May 16, 2024 15:43
@fred-wang
Copy link

Just to be more explicit, I believe we have three categories of APIs:

  • Element.setAttribute/Element.setAttributeNS: It seems browsers agree to always successfully set the attribute in any case. If the attribute node is moved to another element in the default's policy callback, we end up with the attribute set on two different elements.

  • Element.setAttributeNode, Element.setAttributeNodeNS, NamedNodeMap.setNamedItem, NamedNodeMap.setNamedItemNS: If the attribute node is moved to another element in the default's policy callback, WebKit and Chromium may end up associating the attribute node to two different element's attribute maps. My proposal is to throw an InUseAttributeError error if the attribute node was moved to another element (which is what would be happening if we re-run https://dom.spec.whatwg.org/#concept-element-attributes-set without TT check).

  • Attr.value, Attr.nodeValue, Attr.textContent: These APIs essentially operate on an attribute. If the attribute's element is non-null, then WebKit and Chromium just calls setAttributeNS on that element and no new issue arises. The spec instead performs the TT check before setting the element's attribute but does not say what to do when the attribute's element changes (i.e. attribute is detached from the element or moved to another element). My proposal is again to just re-run current https://dom.spec.whatwg.org/#set-an-existing-attribute-value without TT check. This would differ from WebKit/Chromium's current implementation.

I updated https://phabricator.services.mozilla.com/D236161 to match this proposal ; and https://phabricator.services.mozilla.com/D236107 makes all these new tests pass. I'm happy to change to something else depending on what we decide at the end.

@otherdaniel
Copy link

otherdaniel commented Feb 13, 2025

The solution outlined here is correct & self-consistent, and if all implementations agree I'll be happy.

I still think we can be simpler, though: The default policy introduces a seemingly concurrent operation. All operations, on all three categories of APIs outlined above, can be explained by serializing these operations.

Consider: "To set an attribute given an attribute and an element":
As spec'ed here: First check structure; then run TT check; then re-check structure.
- TT error => throws TypeException
- structure error but no TT error => DOMException AttributeInUseError
- structure error created in TT callback => DOMException AttributeInUseError
- TT error and structure error =>TypeException

If instead, we spec it like so:

  1. Let verifiedValue be value.
  2. If verify is true: [... TT check; assign to verifiedValue ...]
  3. If attr’s element is neither null nor element, throw an "InUseAttributeError" DOMException
  4. [... old spec, but verifiedValue instead of value ...]

I think this would get us all of the same error conditions, but fewer operations. And IMHO easier to understand. And I like it, because explaining this behaviour in terms of serializing the checks and running the TT check gives us a guideline which we can apply to all similar cases.

There is one observable difference I can think of: If we have a structure error, and if the TT policy itself throws an exception, then in the first case we'd get AttributeInUseError, and in the second we'd get TypeException. I think that's okay.

@annevk
Copy link
Member

annevk commented Feb 18, 2025

Any updates here? I rather like @otherdaniel's suggestion.

@lukewarlow
Copy link
Member Author

If others are happy with that idea I'm happy to look into updating this PR to match.

@smaug----
Copy link
Collaborator

I think that would work for setNameItem* and setAttributeNode*.
Current code in Gecko does "If attr’s element is neither null nor element" before and after, and maybe doing it only after is good enough. right @fred-wang ?

Attr.value/.nodeValue/.textContent should do validation (if there is element) too before modifying the value.
(chromium and webkit seem to do something else, assuming I'm reading the code correctly)

Element.setAttribute/Element.setAttributeNS in the PR needs some tweaking too. Implementations don't necessarily create Attr nodes when those APIs are used, but if they do, what happens if TT callback moves the Attr away. As fred-wang mentioned earlier, implementations already just create a new attribute on the element.

@lukewarlow lukewarlow force-pushed the trusted-types-attributes branch from 622f78d to b047352 Compare February 20, 2025 14:47
@lukewarlow
Copy link
Member Author

lukewarlow commented Feb 20, 2025

I've pushed up the change to "set an attribute", and to "set an existing attribute value".

I'm looking into the setAttribute methods now.

Attr.value/.nodeValue/.textContent should do validation (if there is element) too before modifying the value.
(chromium and webkit seem to do something else, assuming I'm reading the code correctly)

WebKit at least calls element->setAttribute so the TT check happens slightly later and this can lead to the logic running on the wrong element.

I've updated the spec to make it clearer what should happen here. I believe this is slightly different to Firefox's recently implementation so will need test changes.

Element.setAttribute/Element.setAttributeNS in the PR needs some tweaking too. Implementations don't necessarily create Attr nodes when those APIs are used, but if they do, what happens if TT callback moves the Attr away. As fred-wang mentioned earlier, implementations already just create a new attribute on the element.

I'll investigate what Firefox is doing here and see what the best approach to take is. Generally it seems we want to move TT earlier in the process if possible so I'll try to achieve that.

image

Perhaps we can move step 6 higher up, but it would have to be after step 3 at the earliest.

Copy link
Member

@annevk annevk left a comment

Choose a reason for hiding this comment

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

I did a quick editorial review. Please double check the comments throughout as some apply several times.

aarongable pushed a commit to chromium/chromium that referenced this pull request Mar 12, 2025
Move the Trusted Types check -- which can modify the DOM -- *before* the
structure checks, to prevent inconsistencies from concurrent manipulation of the same attribute.

This fixes a number of WPT subtests in trusted-types/modify-attributes-in-callback.html and trusted-types/set-attributes-mutations-in-callback.tentative.html

Spec: whatwg/dom#1268
Bug: 394760815, 330516530
Change-Id: I9a394a75348598cc68e5b073d8769c826cd0605a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6243963
Commit-Queue: Daniel Vogelheim <vogelheim@chromium.org>
Reviewed-by: Joey Arhar <jarhar@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1431595}
lukewarlow added a commit to web-platform-tests/wpt that referenced this pull request Mar 19, 2025
…Attr.value like setters

The draft spec PR now early returns in the case the element of the attribute changes rather than updating the value.

See whatwg/dom#1268
lukewarlow added a commit to web-platform-tests/wpt that referenced this pull request Mar 19, 2025
…Attr.value like setters

The draft spec PR now early returns in the case the element of the attribute changes rather than updating the value.

See whatwg/dom#1268
@lukewarlow lukewarlow force-pushed the trusted-types-attributes branch from 69967de to d86e02f Compare April 24, 2025 12:42
@lukewarlow lukewarlow requested a review from fred-wang April 24, 2025 13:15
Copy link
Member

@annevk annevk left a comment

Choose a reason for hiding this comment

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

I would love to have a more detailed description of what we are trying to accomplish here. I don't think the resulting algorithms are very clear. Have you looked at whether it's possible to abstract certain bits?

dom.bs Outdated
Comment on lines 7314 to 7316
<li><p>If <var>attribute</var> is null, then set <var>attribute</var> to an <a>attribute</a>
whose <a for=Attr>local name</a> is <var>qualifiedName</var>, <a for=Attr>value</a> is
<var>value</var>, and <a for=Node>node document</a> is <a>this</a>'s <a for=Node>node document</a>.
Copy link
Member

Choose a reason for hiding this comment

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

Is the idea here to create a dummy attribute that you don't append in order to calculate the verified value?

Why is that done this way? This is quite confusing to read through.

Copy link
Member Author

Choose a reason for hiding this comment

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

Apologies for the slow reply on here I thought I replied before but it must have got lost.

TLDR: I agree with you I can't really remember why it's done this way. I updated the TT spec to just take the local name and namespace directly rather than an attribute object. As part of doing this change I realised these algorithms were overly complicated for what was actually needed.

Hopefully now the integration points should be much cleaner and less invasive.

… from the various algorithms, also update call signature to match changes in TT.
@lukewarlow lukewarlow force-pushed the trusted-types-attributes branch from f635814 to 15e9f57 Compare July 3, 2025 08:55
dom.bs Outdated
@@ -7013,6 +7020,8 @@ string <var>namespace</var> (default null):</p>

<li><p>Otherwise, <a lt="append an attribute">append</a> <var>attr</var> to <var>element</var>.

<li><p>Set <var>attr</var>'s <a for=Attr>value</a> to <var>verifiedValue</var>.
Copy link
Member Author

Choose a reason for hiding this comment

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

Need to check if this is in the right place in the algorithm.

cc @fred-wang

Copy link
Collaborator

Choose a reason for hiding this comment

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

(1) So in case oldAttr == attr, its value isn't updated.

(2) 'append an attribute' calls 'Handle attribute changes', and verifiedValue is set to attr after that. That doesn't seem right. Shouldn't value be set before?

Copy link
Member Author

@lukewarlow lukewarlow Jul 3, 2025

Choose a reason for hiding this comment

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

  1. So in case oldAttr == attr, its value isn't updated.

Yes, I think that makes sense? If the attribute hasn't changed does it make sense to put it through Trusted Types default policy? I'll double check what browsers actually do in this case though.

  1. 'append an attribute' calls 'Handle attribute changes', and verifiedValue is set to attr after that. That doesn't seem right. Shouldn't value be set before?

Ah yeah, you're right, step 7 should be moved to step 5.

I've fixed 2 now.

Copy link
Member Author

Choose a reason for hiding this comment

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

So I double checked and for point 1 there's actually an interop bug here, WebKit (and Gecko) doesn't throw a TT error. Chromium currently does, so we can choose whichever behaviour we think is best here.

cc @annevk and @otherdaniel for your opinions

TLDR should doing

element.setAttributeNode(element.attributes[1]) - throw a TT error when TT is enforced (and where element.attributes[1] is something like an onclick attribute?

@lukewarlow lukewarlow changed the title Trusted types attributes Integrate Trusted Types enforcement into attribute handling Jul 3, 2025
@lukewarlow
Copy link
Member Author

@otherdaniel I believe this spec PR fully matches your proposals to call TT as early as possible now. If you've got time I'd be thankful for a review to get another pair of eyes on this.

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.

7 participants