-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
Security - Can't use CSP that blocks 'unsafe-inline'; #183
Comments
Inspecting the source I see there's the |
I was going to suggest changing to use <script type="application/json" id="remix-content">data here</script> And then query by the ID. |
Hey thanks for opening the issues, we've got an internal one we've been planning on finishing up. Any opinions on nonce vs. json? |
Nonce doesn't work if you want caching as it needs to be generated for every single request, otherwise it's trivial to bypass. I have a section on "Nonce-based CSP" in the post I linked. |
The thing that's been tripping me up is not the The output ends up something like this: import * as route0 from "/build/root-G7K4STZZ.js";
import * as route1 from "/build/docs/routes/version-IGSESPQ6.js";
import * as route2 from "/build/docs/routes/index-NTPBUVHK.js";
window.__remixRouteModules = {"root":route0,"docs/routes/version":route1,"docs/routes/index":route2}; https://github.com/remix-run/remix/blob/master/packages/remix-react/components.tsx#L567-L577 This inline script won't ever contain user data, so it should be fine, but I don't think it's compatible with the CSP we'd like people using. |
I think we can get rid of this inline script.
|
I'll add that the Scroll Restoration code also adds some additional script inline that would need to mitigated as well: |
That blog post above that I wrote is out of date, and my memory of this topic has turned a bit fuzzy, but I think there should be a way in Remix to provide all the correct hashes for inline scripts, since Remix takes care of the bundling. I'm wary of the nonce approach as I remember that has been misused by other bundlers in the past. It's at odds with the desire to cache things as much as possible since the nonce value must be a new unique random generated value for every page load. |
Using hashes for inline scripts is probably a better idea than nonces, but this can be an issue for streaming responses as you need to send the headers before starting generating the content. Does Remix start streaming the response before it has been fully computed? |
Any word on this? I'd like to set a content security policy for a new project and this is preventing us from doing so. |
It's possible to use a export const loader: LoaderFunction = async () => {
const cspScriptNonce = cryptoGetRandomString(33);
const data: RootLoaderData = {
cspScriptNonce,
};
return data;
};
export const unstable_shouldReload = () => false;
export default function App() {
const { cspScriptNonce } = useLoaderData<RootLoaderData>();
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration nonce={cspScriptNonce} />
<Scripts nonce={cspScriptNonce} />
<LiveReload nonce={cspScriptNonce} port={8002} />
</body>
</html>
);
} And in const nonce =
remixContext.appState.catchBoundaryRouteId === "root" &&
remixContext.appState.error
? undefined
: remixContext.routeData.root?.cspScriptNonce;
responseHeaders.set(
"Content-Security-Policy",
getContentSecurityPolicy(nonce)
); Most of the complexity is in dynamically generating function getContentSecurityPolicy(nonce?: string) {
let script_src: string;
if (typeof nonce === "string" && nonce.length > 40) {
script_src = `'self' 'report-sample' 'nonce-${nonce}'`;
} else if (process.env.NODE_ENV === "development") {
// Allow the <LiveReload /> component to load without a nonce in the error pages
script_src = "'self' 'report-sample' 'unsafe-inline'";
} else {
script_src = "'self' 'report-sample'";
}
const connect_src =
process.env.NODE_ENV === "development"
? "'self' ws://localhost:*"
: "'self'";
return (
"default-src 'self'; " +
`script-src ${script_src}; ` +
"style-src 'self' 'report-sample'; img-src 'self' data:; font-src 'self'; " +
`connect-src ${connect_src}; ` +
"media-src 'self'; object-src 'none'; " +
"prefetch-src 'self'; " +
"child-src 'self'; " +
"frame-src 'self'; worker-src 'self' blob:; frame-ancestors 'none'; " +
"form-action 'self'; " +
"block-all-mixed-content; " +
"base-uri 'self'; manifest-src 'self'"
);
} Also, |
@ngbrown Is there anything we can do about the "Prop |
Hi @davidpfahler, I'm sure you're discovering that CSP is a fairly complex feature. With as little information you've given it's hard to know what is going on. Maybe open a discussion for more interactive follow up? |
@ngbrown Sorry for being unclear. I was referring to your earlier comment saying
I thought that what you meant by that was that there is an error in the browser console, namely:
|
@ngbrown Where can I get responseHeaders? Where can i see code cryptoGetRandomString? |
|
@ngbrown I get a warning too in console Warning: Prop |
This is probably the warning from React that I was refering to:
I used |
To fix "Warning: Prop nonce did not match. Server: "" Client: "irn2qSvqJ-0xG9HuIos8DrJjQBtqYMk8L3PVz4qyG74muQ" ", you need to add:
Full code:
|
@EvgeniyBudaev thank you! This works great! For anyone unclear on why this works - the nonce attribute is stripped from the script elements as soon as the page loads. Then when client hydration happens, the nonce values are re-applied, and are then updated again every time the loader is re-fetched. This shouldn't be happening anyway, and it would be great if there was added documentation in the Remix docs to ensure the nonces are only provided on the initial server-side load. ref: https://html.spec.whatwg.org/multipage/urls-and-fetching.html#nonce-attributes : "Elements that have a nonce content attribute ensure that the cryptographic nonce is only exposed to script (and not to side-channels like CSS attribute selectors) by taking the value from the content attribute, moving it into an internal slot named [[CryptographicNonce]], exposing it to script via the HTMLOrSVGElement interface mixin, and setting the content attribute to the empty string. Unless otherwise specified, the slot's value is the empty string." |
Was also looking at this today. Not sure if this helps - but Kent Dodd's site @kentcdodds - is also using nonce values, generated in his express server, and available via loader context..... https://github.com/kentcdodds/kentcdodds.com/blob/main/app/root.tsx |
@ngbrown thanks for your comment. i am inspired by it and setting CSP for shopify app with remix successfully which is critical for shopify app review and routine evaluation. |
I think this is no longer an issue with the If this is still an issue, please feel free to re-open with a small reproduction. |
This is correct. It has been fixed 👍 |
Edit: See ahuth's solution below, which is much better than mine 🤦 . For anyone as confused by this thread as I was... At the time of writing this isn't currently working on Kent's site .
Using the other comments in this thread, you can get it work using remix In
Then in
Love to know if there is a better way of doing this without using the |
Aha! Thank you @ahuth that's much easier to follow! |
Edit: Turning this into a mini-tutorial as I figured out my derp: I mounted the In addition, the way Remix adds dynamic scripts requires @samcolby @ahuth How are you piecing this together? Adding the nonce itself doesn’t seem to be enough. Is there a guide somewhere? I’ve added …
|
I'm trying to implement something similar, but I'm stuck on getting the object with I can see the console.log working in my express server: app.all(
'/admin/*',
createRequestHandler({
build,
getLoadContext: (request, response) => {
console.log('getLoadContext: hello', response.locals); // <-- this works: getLoadContext: hello {cspNonce: 'foobar123....'}
return { cspNonce: response.locals.cspNonce };
},
}),
); but the loadContext argument is always undefined: export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
loadContext?: AppLoadContext,
) {
console.log(loadContext) // <-- always undefined
return ...
} I also tried to look for the I'm on remix |
@cskeppstedt I’m pretty sure the fifth argument wasn‘t added until pretty recently, and only in 2.x. |
How come |
Would you mind starting a new issue for Links nonce support @outofthisworld? From a quick glance it does seem that we should support a nonce to be applied to link requests: https://html.spec.whatwg.org/#fetching-and-processing-a-resource-from-a-link-element |
How do I add nonce to inline styles? |
I worked around with the following: const LinksWithNonce = ({ nonce }: { nonce: string }) => {
return (
<Fragment>
{Links().props.children[1].map((child: JSX.Element) =>
cloneElement(child, { nonce }),
)}
</Fragment>
);
};
return (
<html>
<head>
<LinksWithNonce nonce={nonce.style} />
</head>
</html>
) |
Just wanted to make a note that I'm still seeing breaks with the inline script that looks like My current solution is using a transform stream on the
Note that this is a node-specific thing. It would obviously be better for e.g. the |
To improve security and mitigate cross site scripting threats, sites should set a Content Security Policy (CSP). I would like to restrict to
script-src: self
to block inline scripts, but Remix crashes on hydration when this is set.Remix version: 0.17.2
Workaround: don't hydrate, server rendered pages still work 👌 🚀
The text was updated successfully, but these errors were encountered: