Skip to content

Add setCSPTrustedTypesPolicy() for CSP trusted types. #81

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

Merged
merged 21 commits into from
Dec 1, 2022
Merged

Add setCSPTrustedTypesPolicy() for CSP trusted types. #81

merged 21 commits into from
Dec 1, 2022

Conversation

lgarron
Copy link
Contributor

@lgarron lgarron commented Oct 27, 2022

CSP trusted types is an API that allows a website to reduce the possibility of XSS by controlling what kind of content can be placed in a "sink" like .innerHTML. Since <include-fragment-element> is essentially a fancy .innerHTML convenience, it needs changes to be compatible with CSP trusted types.

This PR introduces a flexible callback that allows the calling code to provide its own sanitization or rejection of a server response for an <include-fragment-element>. For example, the site may want to allow the server to send a header to assert that certain HTML is sanitized and safe to use as-is, or the site may want to run the response through a sanitizer.

See the README.md changes for snippets making use of this change: https://github.com/github/include-fragment-element/pull/81/files#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5

@lgarron lgarron requested a review from a team as a code owner October 27, 2022 22:01
@primer-css
Copy link

👋 Hello and thanks for pinging us! This issue or PR has been added to our inbox and a Design Infrastructure first responder will review it soon.

  • 🎨 If this is a PR that includes a visual change, please make sure to add screenshots in the description or deploy this code to a lab machine with instructions for how to test.
  • If this is a PR that includes changes to an interaction, please include a video recording in the description.
  • ⚠️ If this is urgent, please visit us in #primer on Slack and tag the first responders listed in the channel topic.

@lgarron
Copy link
Contributor Author

lgarron commented Oct 27, 2022

I believe this PR still needs:

  • Tests
  • Documentation

I could also use thoughts on how to let the callback reject a response. Is it alright to let it throw an error? Should we add anything to the existing error handler for that?

Please let me know if 1) it also needs anything else, and 2) any thoughts I might want to keep in mind before writing those?

Copy link
Contributor

@keithamus keithamus left a comment

Choose a reason for hiding this comment

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

Does it make more sense here to pass the whole policy to the element (perhaps IncludeFragmentElement.trustedPolicy could be a writable field) and then have the code call createHTML on that? We could also pass the response object as a second arg for additional verification perhaps?

@lgarron
Copy link
Contributor Author

lgarron commented Oct 28, 2022

Does it make more sense here to pass the whole policy to the element (perhaps IncludeFragmentElement.trustedPolicy could be a writable field) and then have the code call createHTML on that? We could also pass the response object as a second arg for additional verification perhaps?

I think this is a reasonable approach, given that there is an accommodation for args in the API: https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicy/createHTML

It might mean that the site ends up with more policies than otherwise, as the policy for include-fragment-element would have to wrap any other(s) that could have been used with (light) pre-processing. I haven't read much on the composition of policies, but I don't see why it shouldn't work.

Let me see how that would look.

@lgarron
Copy link
Contributor Author

lgarron commented Oct 28, 2022

It might mean that the site ends up with more policies than otherwise, as the policy for include-fragment-element would have to wrap any other(s) that could have been used with (light) pre-processing. I haven't read much on the composition of policies, but I don't see why it shouldn't work.

Let me see how that would look.

There seems to be at least one fundamental tradeoff, which is that createHTML is required to be synchronous. However, I think we can allow the policy itself to be passed in as a Promise, to reduce the chance of race conditions.

[CSP trusted types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types) is an API that allows a website to reduce the possibility of XSS by controlling what kind of content can be placed in a "sink" like `.innerHTML`. This commit introduces a flexible callback that allows the calling code to provide its own validation or rejection of an server response for an `<include-fragment-element>`. For example, the site may want to allow the server to send a header to assert that certain HTML is sanitized and safe to use as-is, or the site may want to run the response through a sanitizer. Here is a snippet that looks for such a header and falls back to the `dompurify` library for extremely basic sanitization.

```ts
import { setCSPTrustedTypesCallback } from "include-fragment-element";
import { default as DOMPurify } from "dompurify";

const policy = trustedTypes.createPolicy("server-sanitized", {
  createHTML: (s) => s
});

setCSPTrustedTypesCallback(async (r: Response) => {
  if (r.headers.get("X-Response-Trusted-Types")?.split(",").includes("server-sanitized=true")) {
    return policy.createHTML(r.text());
  }
  return DOMPurify.sanitize(await r.text(), { RETURN_TRUSTED_TYPE: true });
});
```
keithamus
keithamus previously approved these changes Nov 1, 2022
Copy link
Contributor

@keithamus keithamus left a comment

Choose a reason for hiding this comment

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

This LGTM 👍

@lgarron lgarron changed the title Add setCSPTrustedTypesCallback for CSP trusted types. Add setCSPTrustedTypesPolicy() for CSP trusted types. Nov 14, 2022
src/index.ts Outdated
}

// TODO: find another way to make this available for testing.
;(globalThis as any).setCSPTrustedTypesPolicy = setCSPTrustedTypesPolicy
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The test doesn't actually have a way to use exported functions. Do you have a recommendation for how to get access to setCSPTrustedTypesPolicy from test.js without this hack?

Copy link
Contributor

Choose a reason for hiding this comment

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

The test suite should be able to import functions. I'd like us to avoid attaching globals (that aren't the elements themselves).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could you point me to the mechanism/syntax for importing functions from the test? I only see the test config somehow importing the file separately.

Copy link
Contributor

Choose a reason for hiding this comment

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

Apologies I thought we had updated to web-test-runner. I've now done so in #86 so if you rebase it should be evident how to import the function now, as there is an import statement at the top of the file.

@lgarron lgarron requested a review from keithamus November 19, 2022 01:00
@lgarron
Copy link
Contributor Author

lgarron commented Nov 19, 2022

Just realized this was still marked as approved/reviewed, despite substantial revisions and pending questions. I hope I've marked it correctly now to request another review (certainly no rush, though, given this is late on Friday. :-P).

Copy link
Member

@fletchto99 fletchto99 left a comment

Choose a reason for hiding this comment

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

I'll let web systems provide specific code feedback but we were able to test this for our use case in trusted types and it worked as expected 🎉

Copy link

@JoshBowdenConcepts JoshBowdenConcepts left a comment

Choose a reason for hiding this comment

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

Looks good on behalf of @github/primer-reviewers

@lgarron
Copy link
Contributor Author

lgarron commented Nov 21, 2022

Looks good on behalf of @github/primer-reviewers

Thanks! I still have a few open questions in the comments that I'd love to resolve before merging; could I ask for any thoughts on those?

src/index.ts Outdated
@@ -45,8 +62,9 @@ export default class IncludeFragmentElement extends HTMLElement {
this.setAttribute('accept', val)
}

// TODO: Should this return a TrustedHTML if available, or always a string?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I could use advice on this. On the one hand, it would be less surprising (and backwards-compatible) to always return a string.

However, that string can't be assigned to another .innerHTML without going through a CSP trusted types policy again.

(Copied from an older comment.)

Copy link
Contributor

Choose a reason for hiding this comment

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

We can always mark this as a breaking change and return TrustedHTML

Copy link
Contributor Author

@lgarron lgarron Nov 30, 2022

Choose a reason for hiding this comment

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

We discussed this in team pairing and replaced it with a comment, because we're uncertain whom this might break and we don't need it right now. Does that sound alright to you?

Copy link
Contributor

@keithamus keithamus left a comment

Choose a reason for hiding this comment

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

I'm largely in favour of this change. I have some small nitpicks, and I think it would be worth us making a breaking change here, to allow for the change in types.

src/index.ts Outdated
@@ -45,8 +62,9 @@ export default class IncludeFragmentElement extends HTMLElement {
this.setAttribute('accept', val)
}

// TODO: Should this return a TrustedHTML if available, or always a string?
Copy link
Contributor

Choose a reason for hiding this comment

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

We can always mark this as a breaking change and return TrustedHTML

Co-authored-by: Matt Langlois <fletchto99@github.com>
Co-authored-by: Lucas Garron <lgarron@github.com>
@rzhade3 rzhade3 requested a review from keithamus November 30, 2022 19:03
@lgarron lgarron dismissed keithamus’s stale review November 30, 2022 19:03

Made two changes based on feedback.

@rzhade3
Copy link
Contributor

rzhade3 commented Nov 30, 2022

@keithamus we've adjusted the PR to address your feedback above. Mind taking one last pass before we merge in?

@lgarron
Copy link
Contributor Author

lgarron commented Dec 1, 2022

Since the last commit after approval was the requested documentation changes, I'm gonna go ahead and merge and then prep a version bump PR!

@lgarron lgarron merged commit c56996d into github:main Dec 1, 2022
@fletchto99 fletchto99 deleted the lgarron/csp-trusted-types branch December 1, 2022 22:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants