Skip to content
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

Add a default content security policy to the skeleton template #1235

Merged
merged 16 commits into from
Aug 30, 2023
Merged

Conversation

blittle
Copy link
Contributor

@blittle blittle commented Aug 16, 2023

WHY are these changes introduced?

Hydrogen provides no default content security policy (CSP) implementation at the moment. This means that any site created with the default templates is subject to cross-site-scripting and embedded content vulnerabilities. This adds the following primitives and updates the templates:

  1. createContentSecurityPolicy - returns a provider, nonce, and header all at once. I think making the provider not directly exposed is better, because it should only ever be rendered inside entry.server.jsx, right where the CSP is created. In fact I could create a warning if we detect createContentSecurityPolicy is called from the browser.
  2. useNonce - This is how you get access to the nonce inside actual react components.
  3. <Script> - The same as a normal <script> except the nonce is automatically added

templates/skeleton/server.ts Outdated Show resolved Hide resolved
templates/skeleton/server.ts Outdated Show resolved Hide resolved
@blittle
Copy link
Contributor Author

blittle commented Aug 16, 2023

@davidhousedev @duncan-fairley I'm curious if you all use a CSP in your sites? If so, do you use nonces and how have you set them up with Remix?

@wizardlyhel
Copy link
Contributor

wizardlyhel commented Aug 16, 2023

I don't think there will be hydration issue if we set client with nounce="" - https://github.com/kentcdodds/nonce-hydration-issues

I feel we can abstract some Hydrogen components:

  • <ScriptCSP> - renders script tag with nounce
  • <CSPProvider> - nounce provider
  • whatever element needs to add a nounce

While the codes in server.ts seems dynamic, at the very least, I think we can provide:

const {requestWithNounce, nounce} = setNounce(request) - sets the nounce on request for SSR

createCSPHeader(nounce) - create the Content-Security-Policy header with a recommended default and allow for optional override on any directives .. like

createCSPHeader(nounce, {
  'default-src': `'self' cdn.shopify.com checkout.hydrogen.shop`
})

Since graphqli route is a virtual route, can we just define a separate CSP header so that it is valid? It's only gonna exist in dev mode anyways.

Our template uses some inline styles, so I laxed the default policy to allow "unsafe-inline" styles. Is that okay?

Feels like lazy on our part if we allow it. If we got lazy, people who clone with our template will also be lazy. (edit) This is bad. unsafe-inline also allows inline scripts to execute.

Questions:

How would nounce work for dynamic scripts on sub navigation? For example, I have a instagram feed that only show up on about page. I don't want this script to load on home page and only loads it when user navigates to about page.

Would the loader requests contain the nounce? does this validates the CSP? Should each loader request creates a new nounce? but that won't match the main request nounce since it is a single page app.

Reading a couple resources, I think we definitely should have a recommended setting with clear instructions on what it allows. CSP in SPA seems like a bit more work is involved.

@davidhousedev
Copy link
Contributor

@blittle unfortunately this isn't something we've had a chance to look into yet. I'm definitely curious to see what you all recommend!

@wizardlyhel
Copy link
Contributor

One more thing we need to look into is that if there are any missing supports that we need from Remix. We would need to add support for nounce from entry.server as well with renderToReadableStream facebook/react#26738

@blittle
Copy link
Contributor Author

blittle commented Aug 17, 2023

@wizardlyhel I think this actually can be much simpler, I think all of the logic in server.ts can be avoided, and instead everything just inside entry.server.tsx. Thats where the nonce can be generated, added to a provider, and the CSP header can be added to the return Response object.

@blittle
Copy link
Contributor Author

blittle commented Aug 17, 2023

@wizardlyhel I updated the PR to package the following functionality into Hydrogen:

  1. generateNonce() - A utility for generating a script nonce.
  2. <CSPProvider nonce={...} /> - A provider component that just is rendered inside entry.server.tsx
  3. createCSPHeader(nonce, {...}) - A utility for creating the CSP header, it relies on https://www.npmjs.com/package/content-security-policy-builder, which is 5kb and used by HelmetJS
  4. <Script /> component that automatically adds a nonce
  5. useNonce() hook that must be called from within root.tsx to pass the nonce to the Remix <Scripts /> component.

I think this works, but I wish it all could somehow be wrapped up better in a way where we don't need to expose so many individual exports. But it's tricky to do while both React components and non-react functionality all access to the nonce.

@blittle
Copy link
Contributor Author

blittle commented Aug 17, 2023

@juanpprieto at the moment the default CSP allows inline styles, this is because the skeleton route uses inline styles for active elements. I assume this is necessary for now.

Copy link
Contributor

@wizardlyhel wizardlyhel left a comment

Choose a reason for hiding this comment

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

Nice~ This is much cleaner

Comment on lines 26 to 81
function toHexString(byteArray: Uint8Array) {
return Array.from(byteArray, function (byte) {
return ('0' + (byte & 0xff).toString(16)).slice(-2);
}).join('');
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we reuse buildUUID function from hydrogen-react?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It isn't a UUID though, at least all nonce's I've seen are not.

Comment on lines +6 to +11
export const Script = forwardRef<HTMLScriptElement, ScriptProps>(
(props, ref) => {
const nonce = useNonce();
return <script {...props} nonce={nonce} ref={ref} />;
},
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to make this export a different name than Script to avoid import confusion?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What kind of import confusion? I assume if we later have other things we do for 3p scripts, we wouldn't want multiple Script components.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Along with that, perhaps we make the CSPProvider a more generic HydrogenProvider, so down the road we can add more logic to a single provider, instead of exposing many?

Copy link
Contributor

Choose a reason for hiding this comment

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

If we add, for example, integration with partytown or something, would we add it to this same component?

Also, +1 to have a generic provider for Hydrogen instead of CSP-specific.

Copy link
Contributor

@frandiox frandiox left a comment

Choose a reason for hiding this comment

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

Thanks for pushing this Bret!

A couple of things:

  • We should also add the nonce to the scripts in the ErrorBoundary (including the ScrollRestoration like I did in the suggestion comment below).
  • MiniOxygen is adding a script for live reload and that one doesn't include the nonce, so refreshing on changes breaks. I'll try to fix this and release a new version of MiniOxygen. ==> https://github.com/Shopify/mini-oxygen/pull/522

packages/hydrogen/src/csp/csp.ts Outdated Show resolved Hide resolved
templates/skeleton/app/utils.ts Outdated Show resolved Hide resolved
packages/hydrogen/src/csp/csp.ts Outdated Show resolved Hide resolved
Comment on lines +6 to +11
export const Script = forwardRef<HTMLScriptElement, ScriptProps>(
(props, ref) => {
const nonce = useNonce();
return <script {...props} nonce={nonce} ref={ref} />;
},
);
Copy link
Contributor

Choose a reason for hiding this comment

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

If we add, for example, integration with partytown or something, would we add it to this same component?

Also, +1 to have a generic provider for Hydrogen instead of CSP-specific.

templates/skeleton/app/root.tsx Show resolved Hide resolved
templates/skeleton/app/root.tsx Outdated Show resolved Hide resolved
@blittle
Copy link
Contributor Author

blittle commented Aug 18, 2023

@frandiox I really like the simplicity of having all the logic only within entry.server.tsx (adding the provider, getting the nonce, and setting the CSP header), the downside is that only React rendered routes get the CSP. So a static HTML file, like /graphiql does not get the CSP. That's convenient because we don't need to add logic for the /graphiql script, but at the same time, maybe we should be sending the CSP header for all responses? Part of me says no, because it is a decent sized header, and I don't think it should be sent for all loader and action requests. Thoughts?

@frandiox
Copy link
Contributor

So a static HTML file, like /graphiql does not get the CSP. That's convenient because we don't need to add logic for the /graphiql script, but at the same time, maybe we should be sending the CSP header for all responses? Part of me says no, because it is a decent sized header, and I don't think it should be sent for all loader and action requests. Thoughts?

GraphiQL is also only shown in development so we don't need to worry about that one.
For other ""static"" HTML files (returning HTML from a loader), we should just mention in the docs that if you opt-out of React rendering, you should handle CSP manually as well (in scripts and also in the headers in the loader response). This situation should be rather rare, I think.

That leaves us with API responses... I'm not an expert on this topic but I think API responses wouldn't benefit from most of the uses of a CSP. There might be some minor benefits but perhaps something we can revisit when it's simpler to set it up in Remix? Right now we'd need to also modify server.ts for that and I'm not sure if the extra complexity for the users is worth it.

@wizardlyhel
Copy link
Contributor

wizardlyhel commented Aug 28, 2023

We should add the monorail connect-src as part of default ... or add as part of the instructions for Shopify Analytics

Screenshot 2023-08-28 at 11 43 55 AM

Copy link
Contributor

@frandiox frandiox left a comment

Choose a reason for hiding this comment

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

I like how this has evolved 👍

Comment on lines +59 to +60
styleSrc: ["'self'", "'unsafe-inline'", 'https://cdn.shopify.com'],
connectSrc: ['self', 'https://monorail-edge.shopifysvc.com'],
Copy link
Contributor

Choose a reason for hiding this comment

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

These might be overwritten by mistake when adding more domains. Should we have a different strategy to merge directives so that we always include Shopify defaults?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I went back and forth on that. I decided to keep make sure the nonce is always merged in, because that's the main value prop we are providing. So if someone doesn't want the nonce, they should just do their own CSP manually. But if we automatically merge domains as well? If they are deploying off oxygen, there's a high chance that they wouldn't want the shopify domains at all.

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.

5 participants