-
Notifications
You must be signed in to change notification settings - Fork 74
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
Alternative Options for Default Policy. #248
Comments
Default policy alternativesBackground: Trusted Types' Default PoliciesTrusted Types default policy is a mechanism that lets the web authors define a fallback behavior if a DOM XSS sink is called with a string (instead of a matching typed object). This happens when existing applications start migrating to Trusted Types (as the code does not use the types at all), and when using 3rd party code - for example, jQuery upon loading will write to innerHTML a few times at least. Example:
Further example of a typical Angular application default policy here. The fallback behavior is defined as a set of JS functions that accept a string and return a string, and perform various (synchronous) tasks, the most complex of which is likely to be HTML sanitization (e.g. running DOMPurify to remove JS payloads). Some sort of custom fallback behavior is crucial for successfully adopting TT in existing applications, as all existing code nowadays uses strings with DOM XSS sinks. A lack of a default policy (or an equivalent mechanism) would force sites into a "big bang" migration, where enforcement could only be enabled after all dependencies and third-party libraries have been converted. Experience suggests this oftentimes prevents adoption of a feature entirely. Problem StatementThe default policy - as presently specced and implemented in Chromium - runs a user-supplied JavaScript callback function. Since it runs user code during the operation, it might cause unexpected modifications to the DOM state. For example, a default policy might delete the DOM node that the ongoing operation was meant to operate on. The intended use of the policy is to apply a string-to-string mapping (or reject a value), but there is no way to safely restrict a user-supplied callback to being side-effect free. This is difficult to spec and implement in a sane fashion, and might cause interoperability issues, as different browsers, or different versions of the same browser, might have subtly different behaviour in some edge cases, security issues, as this allows to run a user-script on the DOM in an unexpected (and likely untested) intermediate state, or performance issues. Similar issues have occured with Mutation Events, which has led to a substantial string of problems with their implementation(s). This document explores the alternatives to the current shape of the default policy in the Trusted Types API. AlternativesAlternatives fall into three major categories:
Eliminate callbacks: No default policyNo default policy mechanism exists in TT. Assigning a string value to DOM XSS sink throws a TypeError. The fallback behavior can only be defined in JS code, by hijacking setters (similar to how TT polyfill does it). Advantages
Disadvantages
Eliminate callbacks: No default policy, but ship with a supplied polyfillThis is basically a variant of "No default policy", except that we ship a polyfill for the "default" policy from the start. Advantages
Disadvantages
Restrict execution: Running the policy out-of-bandFor each sink (e.g. IDL attribute setter), record whether it was set with a 'trusted' value or not. If a default policy is not configured, throw immediately for the untrusted case. Otherwise, apply the modifications (as without Trusted Types), but record the sink value as untrusted. Later, when the associated script is about to execute (for TrustedScript sinks) or loaded (for TrustedScriptURL sinks), run all 'un-trusted' values through the default policy. Advantages
Disadvantage:
Restrict execution: WorkletsThe default policy might be implemented in a Worklet (similar to AudioWorklets). This gives the default policy an isolated global to run in. Example: // index.js
await trustedTypes.defaultPolicyWorklet.addModule('default-policy.js');
// default-policy.js
class MyDefaultPolicy extends TrustedTypesDefaultPolicy {
constructor(domFragment) {
super(domFragment);
}
createHTML(input, sink) {
try {
sanitize(domFragment, input);
} catch (e) {
return null;
}
}
}
registerDefaultPolicy('default', MyDefaultPolicy); Advantages
Disadvantages
Tighten down specification: Running the default policy prior to all internal operationsThis requires distinguishing between two classes of DOM mutations: Attributes (and attribute nodes), and all others. For attributes, run the Trusted Types checks and the default policy before any modifications are applied, thus making it strictly equivalent to running the default policy and attribute setting in sequence. Advantages
Disadvantages
Tighten down specification: WebIDL type mappingThis is a variant of "Running the default policy [... early …]" above, but based on different spec mechanics: The idea is to specify all applicable sinks in WebIDL, and handling default policy execution in the mapping layer. Example:Currently (in Chromium) trusted types are defined as a union type:
The setter definion then differs from the canonical IDL. In HTMLScriptElement, we have:
An alternative would be to drop the union type and use an extended attribute:
Ideally, these could be upstreamed, where browsers not supporting trusted types could easily ignore the attribute. This would bring our IDLs closer to upstream; makes it easier to spot where trusted types are used; and makes it easier to test compatibility between browsers, both with and without trusted type support (e.g. in WPT tests). For the purpose of our "default policy" problem, the idea is that (in both spec and implementation) the WebIDL deals with trusted types, analogous to how it deals with any other non-String type: The value is either accepted as-is, or is converted (e.g. stringified), or is rejected. This is exactly what we need for trusted types: We either want to accept the value, or convert it to something acceptable (via default policy), or reject it. Conveniently, these checks always occur in a context that is already running user script, and before any actual DOM mutations (or somesuch) take place. So the idea is to strictly serialize user script -> trusted types -> DOM. (And thus guarantee that DOM mutation within another DOM mutation can happen.) For most intents and purposes, the downstream (DOM, HTML) would only "see" the converted values. If it's necessary to make decisions based on the trust-status, this could be spec'ed (and implemented) so that the type mapping for Advantages
Disadvantages
Eliminate callback: Mutation observersSanitize the values via MutationObservers (i.e. in a microtask after the DOM processing happened). Disadvantages
|
As I understand it, there are different enforcement points and for some a callback (default policy) can be invoked can for some this creates issues, as outlined above. I think it would be useful to distinguish the various callers of the callback more clearly to see what problems need solving where. E.g., we can probably run a callback before executing script. We can probably run a callback before running an event handler. The hard part is running a callback while mutating the tree. But is mutating the tree itself XSS? Or is it executing a script or running an event handler introduced as part of the mutation? Or both? Are there other callback callers that are potentially problematic in terms of timing? |
There's also a policy called upon a navigation to javascript:. The reason why we're running the default policy now (as part of Get Trusted Type compliant string) when setting attributes (IDL or content), is to be able for the JS statement to either:
or
IOW, we're not trying to catch the XSS at the latest possible moment, but error out early (with the default policy giving a way to silently recover from some error conditins). @annevk, can you clarify what tree mutations are problematic? I think we only have that for a script text nodes. I think it's OK for the default policy to delay the execution if e.g. a text node is appended to a script that later on gets executed. I was under the impression that running when the attributes change (say |
I think I understand the ergonomics issue, but I also want to ensure that if we decide to ignore it, we don't break other important functionality. |
Yes, and I don't think we should do that at all. We only did that inadvertently, and didn't realize the implications. Our implementation largely skirted around the issue, since it always runs the "default policy" callback before passing it on to the DOM code.
Currently (implemented):
None of those mutate the DOM while the DOM mutation is ongoing. (At least not intentionally; but I might be overlooking something.) We've tried to spec that, but I'm not sure we've been 100% successful in that. (I'm having a hard time parsing all the subtleties out of the spec text, to be honest.) ============== edit: There's a fifth enforcement point, that koto briefly mentioned above: In case of navigation to javascript:-URLs, the default policy is run at the time of navigation, just before executing the actual URL script. |
Oooh, this is a cool approach that very directly attacks the problem, and probably encourages good policies. I like it, if you think we could get away with it.
I'm not sure this fixes anything?
This seems similar in spirit to In particular I think the I believe you won't need the complex stack of queue of queues type system that custom element reactions use, since you only want to run one default policy callback? Or, hm, you might need something like it, because the side effects that that callback causes could themselves trigger another default policy callback. See the paragraphs following this definition for more on that. And yeah, you probably even need the backup element queue (or an analogue), since you want to run this default policy in cases like the user editing a |
@annevk, re: "describe the various callers of the callback" - the IDL index shows all the affected APIs - all the ones with a Apart from the script text/src property that we now have slot-based approach to guard, that's @domenic, re: Worklets and async. I'm not sure if |
I took a look at the Worklets route, and I believe it's a hard sell, but only for one reason - having to fetch a module with the default policy before the rest of the scripts run is problematic (also from a performance perspective). If we could create a separate realm synchronously, without having to Regarding the [CEReactions] route - we actually now have a similar setup via our extended attribute. [CEReactions] in HTML apply at IDL attributes level - and in DOM there's an explicit callback to them when content attributes change. Chrome's implementation of TT follows that model, but the TT reactions are not put on a queue, but executed immediately. The optimal behavior we are after is exactly what polyfill does, which is hijacking the setters, such that default policy happens before DOM gets to process the attribute mutation. Calling it when the browser algos themselves mutate the DOM (e.g. from within the parser) is a nice-to-have, but not crucial for XSS guarding. After whatwg/dom#805 there's a single place where the default policy would be called for DOM. That's probably very naive, but maybe asserting that the crucial objects still exist (or were not changed) after validation steps would let us sidestep the risk? |
There's also prior work in throw-on-dynamic-markup-insertion-counter that prevents specific JS code (custom elements) from calling dangerous API when called in a specific context. @annevk, do you think this approach might help here? I'm not sure what's the practical experience with implementing that counter, and whether this worked and prevented bugs in implementations. Is there an exhaustive list of "bad things" that a default policy should not do when run during content attribute mutation? |
Thanks all for the feedback! What I'm reading out of the replies so far is that the two top candidates are "Worklets" and "WebIDL type mapping". I think modelling the "type mapping" after (Personally, as an implementor, I'm in favour of "type mapping", although I find "Worklets" more appealing in theory.) |
+ @annevk, resurfacing this question:
I'm trying to see whether the approach taken when speccing custom element constructor behavior is feasible. For CE there are safeguards prohibiting constructors from |
I don't think so, the problem is mutating the tree and the mutating the tree has no existing guards and I don't think we should add them. Or at least not as a side effect of adding this feature. |
So.. I think we've figured this out. We'd mainly like to follow the "WebIDL" solution outlined above: WebIDL gets a TrustedType=... extended attribute. Attributes and method parameters will be declared with the (unmodified) string types and the extended attribute. In the common case, the TT check (meaning: unwrapping the trusted type wrapper in case of a trusted type, or running the string through a default policy in case of a plain string (or string-ified argument)) happens when the arguments are converted, just before an element's actual property or attribute setter are called. Thus, the JavaScript callback happens when the arguments are converted, and before handing off the string result to the DOM. This won't cover everything, though. The other cases are:
In terms of point in time, the "default policy" is executed mostly when the arguments are converted. The exceptions are script elements (prepareScript) and eval (before execution). In terms of implementation, the IDL compiler will end up doing the bulk of the work, and the TT check is mostly run in the WebIDL bindings. The exceptions are script elements (shadow slot), attribute node functions and setAttribute/setAttributeNS (which need to lookup whether a check needs to be made), and eval. |
(DOM|USV)String. This is to hook up the Trusted Types validation during the ES->IDL type conversion to avoid funky issues with its default policy. See w3c/trusted-types#248, w3c/trusted-types#176
Please don't call it a shadow slot, that's rather confusing with shadow trees and their slots, especially for a DOM feature. Otherwise I think the solutions for those cases make sense. |
In the spec they are simply called I'll followup on the DOM integration in whatwg/dom#789 (I think the 'Attr Node' case and 'setAttribute' changes still need speccing) and the specific mechanics of WebIDL in WebIDL PR, but it seems like we've resolved this issue. Please shout if not. Thanks all! |
(DOM|USV)String. This is to hook up the Trusted Types validation during the ES->IDL type conversion to avoid funky issues with its default policy. See w3c/trusted-types#248, w3c/trusted-types#176
(DOM|USV)String. This is to hook up the Trusted Types validation during the ES->IDL type conversion to avoid funky issues with its default policy. See w3c/trusted-types#248, w3c/trusted-types#176
The current definition of the 'default policy' (even after the changes in #236) has the problem that it's not clearly specced what happens when the default policy callback would modify the DOM.
For example, the default policy might delete the element whose attribute is being modified. This doesn't seem like a super sensible thing to do, but the spec certainly allows it. As this occurs during the DOM implementation, the DOM would be modified during another modification to the DOM. At the very least, this requires exact specification of the point in time in which this occurs. Moreover, similar issues apparently have occured with MutationObserver, which has apparently led to a painful string of interoperability & implementation problems in browsers, so that feedback was strongly negative on this approach in general.
On the other hand, the "default policy" is an essential mechanism to allow a reasonable migration. Without it, only "big bang" migrations including all dependencies and 3rd-party libraries would be possible, which would likely prevent a large number of developers from utilizing the feature at all.
This issue is to discuss alternative choices for the "default policy".
The text was updated successfully, but these errors were encountered: