Add csp nonce to all inline script tags (#5566)

This commit is contained in:
Tom Moor
2023-07-15 10:15:14 -04:00
committed by GitHub
parent ea07b72c7a
commit 66331d3d4f
4 changed files with 24 additions and 17 deletions

View File

@@ -67,24 +67,24 @@ export const renderApp = async (
const { shareId } = ctx.params; const { shareId } = ctx.params;
const page = await readIndexFile(); const page = await readIndexFile();
const environment = ` const environment = `
<script> <script nonce="${ctx.state.cspNonce}">
window.env = ${JSON.stringify(presentEnv(env, options.analytics))}; window.env = ${JSON.stringify(presentEnv(env, options.analytics))};
</script> </script>
`; `;
const entry = "app/index.tsx"; const entry = "app/index.tsx";
const scriptTags = isProduction const scriptTags = isProduction
? `<script type="module" src="${env.CDN_URL || ""}/static/${ ? `<script type="module" nonce="${ctx.state.cspNonce}" src="${
readManifestFile()[entry]["file"] env.CDN_URL || ""
}"></script>` }/static/${readManifestFile()[entry]["file"]}"></script>`
: `<script type="module"> : `<script type="module" nonce="${ctx.state.cspNonce}">
import RefreshRuntime from 'http://localhost:3001/static/@react-refresh' import RefreshRuntime from 'http://localhost:3001/static/@react-refresh'
RefreshRuntime.injectIntoGlobalHook(window) RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => { } window.$RefreshReg$ = () => { }
window.$RefreshSig$ = () => (type) => type window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true window.__vite_plugin_react_preamble_installed__ = true
</script> </script>
<script type="module" src="http://localhost:3001/static/@vite/client"></script> <script type="module" nonce="${ctx.state.cspNonce}" src="http://localhost:3001/static/@vite/client"></script>
<script type="module" src="http://localhost:3001/static/${entry}"></script> <script type="module" nonce="${ctx.state.cspNonce}" src="http://localhost:3001/static/${entry}"></script>
`; `;
ctx.body = page ctx.body = page
@@ -97,7 +97,8 @@ export const renderApp = async (
.replace(/\{prefetch\}/g, shareId ? "" : prefetchTags) .replace(/\{prefetch\}/g, shareId ? "" : prefetchTags)
.replace(/\{slack-app-id\}/g, env.SLACK_APP_ID || "") .replace(/\{slack-app-id\}/g, env.SLACK_APP_ID || "")
.replace(/\{cdn-url\}/g, env.CDN_URL || "") .replace(/\{cdn-url\}/g, env.CDN_URL || "")
.replace(/\{script-tags\}/g, scriptTags); .replace(/\{script-tags\}/g, scriptTags)
.replace(/\{csp-nonce\}/g, ctx.state.cspNonce);
}; };
export const renderShare = async (ctx: Context, next: Next) => { export const renderShare = async (ctx: Context, next: Next) => {

View File

@@ -156,10 +156,12 @@ koa.use(async (ctx, next) => {
ctx.set("Timing-Allow-Origin", timingOrigins.join(", ")); ctx.set("Timing-Allow-Origin", timingOrigins.join(", "));
await next(); await next();
}); });
koa.use(apexRedirect()); koa.use(apexRedirect());
if (env.ENVIRONMENT === "test") { if (env.ENVIRONMENT === "test") {
koa.use(errors.routes()); koa.use(errors.routes());
} }
koa.use(router.routes()); koa.use(router.routes());
export default koa; export default koa;

View File

@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
import crypto from "crypto";
import { Server } from "https"; import { Server } from "https";
import Koa from "koa"; import Koa from "koa";
import { import {
@@ -27,8 +28,6 @@ const isProduction = env.ENVIRONMENT === "production";
const defaultSrc = ["'self'"]; const defaultSrc = ["'self'"];
const scriptSrc = [ const scriptSrc = [
"'self'", "'self'",
"'unsafe-inline'",
"'unsafe-eval'",
"gist.github.com", "gist.github.com",
"www.googletagmanager.com", "www.googletagmanager.com",
"cdn.zapier.com", "cdn.zapier.com",
@@ -103,19 +102,22 @@ export default function init(app: Koa = new Koa(), server?: Server) {
// Sets common security headers by default, such as no-sniff, hsts, hide powered // Sets common security headers by default, such as no-sniff, hsts, hide powered
// by etc, these are applied after auth and api so they are only returned on // by etc, these are applied after auth and api so they are only returned on
// standard non-XHR accessed routes // standard non-XHR accessed routes
app.use( app.use((ctx, next) => {
contentSecurityPolicy({ ctx.state.cspNonce = crypto.randomBytes(16).toString("hex");
return contentSecurityPolicy({
directives: { directives: {
defaultSrc, defaultSrc,
scriptSrc,
styleSrc, styleSrc,
scriptSrc: [...scriptSrc, `'nonce-${ctx.state.cspNonce}'`],
imgSrc: ["*", "data:", "blob:"], imgSrc: ["*", "data:", "blob:"],
frameSrc: ["*", "data:"], frameSrc: ["*", "data:"],
connectSrc: ["*"], // Do not use connect-src: because self + websockets does not work in // Do not use connect-src: because self + websockets does not work in
// Safari, ref: https://bugs.webkit.org/show_bug.cgi?id=201591 // Safari, ref: https://bugs.webkit.org/show_bug.cgi?id=201591
connectSrc: ["*"],
}, },
}) })(ctx, next);
); });
// Allow DNS prefetching for performance, we do not care about leaking requests // Allow DNS prefetching for performance, we do not care about leaking requests
// to our own CDN's // to our own CDN's
@@ -129,6 +131,8 @@ export default function init(app: Koa = new Koa(), server?: Server) {
policy: "no-referrer", policy: "no-referrer",
}) })
); );
app.use(mount(routes)); app.use(mount(routes));
return app; return app;
} }

View File

@@ -53,7 +53,7 @@
<body> <body>
<div id="root"></div> <div id="root"></div>
{env} {env}
<script> <script nonce="{csp-nonce}">
if ( if (
window.localStorage && window.localStorage &&
window.localStorage.getItem("theme") === "dark" window.localStorage.getItem("theme") === "dark"