-
Notifications
You must be signed in to change notification settings - Fork 191
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
Allow to render TrustedHTML objects similar to SafeString #1567
base: main
Are you sure you want to change the base?
Conversation
Thanks for submitting this, this is neat! |
Nice, "All checks have passed" |
Of note,
|
@@ -43,6 +44,18 @@ export function isEmpty(value: unknown): boolean { | |||
return value === null || value === undefined || typeof (value as Dict).toString !== 'function'; | |||
} | |||
|
|||
let isHTML: ((value: unknown) => boolean) | undefined; | |||
if (typeof window !== 'undefined') { | |||
let trustedTypes = (window as unknown as TrustedTypesWindow).trustedTypes; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a good way to progressively allow the feature for browsers that have it and not break in browsers that don't
let value = isEmpty(rawValue) | ||
? '' | ||
: isTrustedHTML(rawValue) | ||
? (rawValue as string) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This cast seems suspect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would think, if we made it here, we should have already checked and unwrapped the value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can modify whole chain of functions to accept string | TrustedHTML
type, but I thought we should keep it when decide to fully migrate Glimmer to Trusted Types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added TrustedHTML for appendHTML methods, let's see if we have any issues
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it goes down to
@simple-dom/interface that doesn't support Trusted Types
insertHTMLBefore(parent: SimpleElement, nextSibling: Nullable<SimpleNode>, html: string): Bounds { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we definitely still have to plumb this through to simple-dom. I am actually not sure why TypeScript is not catching the TypeError, but essentially the problem is you changed the interface without changing the implementations, so for example, in places like these, it's all going to be problematic:
glimmer-vm/packages/@glimmer/node/lib/node-dom-helper.ts
Lines 23 to 25 in 1cb493b
html: string | |
): Bounds { | |
let raw = this.document.createRawHTMLSection!(html); |
The code is a bit odd, because the primary implementation of insertHTMLBefore
is on a class that doesn't claim to implement the interface you modified, so no type error there. But this extends and implement it, so it should be caught there, but it wasn't: https://github.com/glimmerjs/glimmer-vm/blob/main/packages/%40glimmer/runtime/lib/dom/api.ts#L18
Anyway missing type errors aside, there are some real, a bit tedious but solvable problems, there are also a few more things to think through around attributes (and then the grunt work to actually plumb it through, get simple-dom released, etc), so it may be a while before we can land this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've changed it back to not change interface of insertHTMLBefore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you think it will take a while, I suggest to taking small steps and in this PR just stringify TrusteHTML, but render it as HTML.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
more things to think through around attributes
Let me explain why I'm against error on rendering TrustedHTML in attribute context.
I've tested LinkedIn where we use SafeString in attribute context, almost everywhere. It's related to our i18n system. Every i18n string is like small HTML template where all params HTML encoded, not every i18n string contains HTML tags, but potentially every string can contain HTML entities, like
. For this reason i18n helper uses htmlSafe for every i18n string. And obviously same SafeStrings used in attribute context. If we are going to replace SafeString with TrustedHTML it will not change anything. Yes, we have some rare double encoding issues, but throwing errors would completely prevent migration to TrustedHTML.
Another point is DOM API, some attributes require TrustedHTML to work, for example iframe.srcdoc
trustedTypes.getAttributeType('iframe', 'srcdoc') // TrustedHTML
https://w3c.github.io/trusted-types/dist/spec/#validate-attribute-mutation
For this reason we should keep TrusteHTML and other trusted types unmodified for setAttribute, decoding can cause an injection, stringifying can cause default policy triggering.
https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicyFactory/getAttributeType
I would recommend to keep same behavior as SafeString, and in best case don't stringify TrustedHTML object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for walking through those use cases. That's helpful, and I'm fine with not throwing
I discussed this with @wycats and here is a summary of the things we discussed. In general:
Research:
For content position:
For attributes:
So overall, we think this is certainly a worthwhile feature to pursue, it's a bit of a project to get it implemented correctly, but a fairly bounded one. If you think it would help enough to land a version where we simply stringify, and if you think that helps enough with your use case (keep in mind that it will still trigger CSP warnings because it won't make it all the way to the DOM APIs), we can certainly explore that. However, I'm not sure that would be much less work than doing everything correctly – the PR as it stands is still problematic, because we can't just rely on accessing the trusted types on the global object in SSR/fastboot, and if we are going to fix that we may as well do everything else? Does LinkedIn not need this to work in SSR/fastboot? |
It will take significant amount of time until we will be able to enable trusted-types CSP, but replacement SafeString with TrustedHTML already will be hugely beneficial in preventing HTML injections for one simple reason – developers no longer will require to call htmlSafe manually, because systems that not dependent on Ember.js could return TrustedHTML objects instead of strings. To summarize, migration to TrustedHTML will take time, and simple rendering it similar to SafeString would help a lot even if we couldn't utilize CSP.
Internally we will be using object similar to SafeString or Trusted Types polyfill, I don't see issues with SSR. We will make sure that it works. |
I don't think you can. FastBoot/SSR runs in a sandboxed environment, so as it stand, what would happen is that there will be no |
in fastboot's sandboxed environment, you could provide an implementation for is this feasible? or not worth it? 🤷 ./config/fastboot.js
module.exports = function () {
return {
buildSandboxGlobals(defaultGlobals) {
return {
...defaultGlobals,
fetch,
AbortController,
ReadableStream: typeof ReadableStream !== 'undefined'
? ReadableStream
: require('node:stream/web').ReadableStream,
WritableStream: typeof WritableStream !== 'undefined'
? WritableStream
: require('node:stream/web').WritableStream,
TransformStream: typeof TransformStream !== 'undefined'
? TransformStream
: require('node:stream/web').TransformStream,
Headers: typeof Headers !== 'undefined' ? Headers : undefined,
// custom stuff here
TrustedHTML: YourImplementationHere
}
}
}
}; |
I just planned to use this code to generate TrustedHTML objects: let createTrustedHTML: (s: string) => TrustedHTML;
let isTrustedHTML: (s: unknown) => boolean;
const trustedTypes = window.trustedTypes;
if (trustedTypes?.createPolicy && trustedTypes?.isHTML) {
const policy = trustedTypes.createPolicy('jSecure', {
createHTML: (s: string) => s
});
createTrustedHTML = (s: string) => policy.createHTML(s);
isTrustedHTML = (value: unknown) => trustedTypes.isHTML(value);
} else {
// if Trusted Types are not available, create object similar to Ember.js SafeString
class HTML extends String {
toHTML(): string {
return this.toString();
}
}
createTrustedHTML = (s: string) => new HTML(s) as unknown as TrustedHTML;
isTrustedHTML = (value: unknown): boolean => {
return (
value !== null &&
typeof value === 'object' &&
'toHTML' in value &&
typeof (value as HTML).toHTML === 'function'
);
};
} |
It think it should probably get passed here: https://github.com/ember-fastboot/ember-cli-fastboot/blob/242683f4137ca3dbcd53fdc0d8cc65f4055e3b57/packages/fastboot/src/ember-app.js#L362-L374 Along side the |
It's not that different from the plan I laid out, the main difference is that we would only define a strict subset in Which is the same reason why you don't just set In this case, you only really need to implement |
Asking some experts about CSS exfiltration, I believe style attribute is not that bad. And htmlSafe doesn't provide security benefits there. |
That would be a pretty strong claim with a pretty high bar to clear, and there are additional considerations for client side JS apps. Off the top of my head, a scenario would be that you have Besides exfiltration, it is also a phishing vector, e.g. re-positioning a comment/message box directly over a password field, hiding important warning text, that kind of stuff. We can certainly discuss whether we originally meant to protect users against that kind of threat, or was it just meant for XSS because expressions were a thing back then. My recollection was that it was for both, and it was added in response to (or at least around the same time) industry experts calling out this kind of vectors in Angular/other frameworks. But in any case, that is not a small change that we are just going to make in a PR without an RFC. |
I'm checking Discord conversation
Inline script inserted with innerHTML doesn't work in any browser: I guess you meant For this specific reason CSP have additional directives:
I absolutely agree with all that, but these issues happen during HTML injection e.g. when attacker bypassed HTML sanitizer or able to inject attribute. Injection in style attribute itself is very rare, and that fact that developer warned about usage of htmlSafe will not prevent it. Gareth replied: https://twitter.com/garethheyes/status/1761176827492552748
|
Some big-picture thoughts: The Status QuoToday, Ember attempts to address certain potential security risks via the XSS MitigationsThe recommended approach for XSS is:
This approach maps onto security best practices that predate trusted types. It's not a confusion-free approach, but it's straight-forward once you understand what's going on, and it's pretty straight-forward for npm packages to use the approach to create helpers that produce safe HTML. Exfiltration and ClickjackingEmber uses the Concatenating malicious user content into the This mitigation is less effective than the mitigation against XSS because:
We may want to reconsider this mitigation, but it would be inappropriate (in my opinion) to weaken a Defense in Depth strategy simply because we're strengthening an unrelated mitigation. Trusted TypesTrusted Types offer a way to address XSS (but not exfiltration or clickjacking). Before Trusted Types, it was possible to eliminate sources of XSS vulnerabilities through CSP opt-ins. These opt-ins work in a blunt-force way: they disable all of the DOM "sinks" that can result in the execution of dynamic code. Trusted Types build on this foundation by allowing applications to mark specific dynamic strings as trusted, allowing them to be passed to DOM sinks that execute code even when stricter CSP settings are enabled. (The API is carefully crafted to require the application author (the person who is responsible for the HTML code that bootstrapped the app) to have a direct hand in marking a particular string as trusted.) Ultimately, this should result in wider adoption of stricter CSP settings in situations where a small amount of dynamic content (e.g. localization bundled fetched as JSON) currently prevent adoption. How Ember Should Adopt Trusted TypesEmber's XSS mitigations behave differently from Trusted Types: they work even in the absence of strict CSP settings. In a sense, Ember's templating syntax emulates strict CSP settings, but only when constructing and inserting DOM through Ember templates. First, a quick comparison of the features.
In my opinion, we want the following changes to Glimmer:
Footnotes
|
@chancancode is anything else required for this PR? |
…yes? |
I believe the previous comments/reviews still reflect the status quo, has anything changed, or am I missing/misremembering something? Do you want to summarize what you see as the current status and how it aligns with the direction we seem to all wait, and maybe we can find the disconnect that way? |
Summary
Currently TrustedHTML objects converted to strings during rendering before hitting DOM API that means all HTML insertions handled by default policy. That prevents us from migrating to Trusted Types. This change allows to render TrustedHTML objects similar to SafeString.
Testing done