diff --git a/README.md b/README.md index 358510c..feeed82 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # @netlify/plugin-csp-nonce -Use a [nonce](https://content-security-policy.com/nonce/) for the `script-src` directive of your Content Security Policy (CSP) to help prevent [cross-site scripting (XSS)](https://developer.mozilla.org/en-US/docs/Web/Security/Types_of_attacks#cross-site_scripting_xss) attacks. +Use a [nonce](https://content-security-policy.com/nonce/) for the `script-src` (and optionally `style-src`) directive of your Content Security Policy (CSP) to help prevent [cross-site scripting (XSS)](https://developer.mozilla.org/en-US/docs/Web/Security/Types_of_attacks#cross-site_scripting_xss) attacks. This plugin deploys an edge function that adds a response header and transforms the HTML response body to contain a unique nonce on every request, along with an optional function to log CSP violations. @@ -70,6 +70,12 @@ _Default: `[]`_ The glob expressions of path(s) that _should not_ invoke the CSP nonce edge function. Must be an array of strings. This value gets spread with common non-html filetype extensions (`*.css`, `*.js`, `*.svg`, etc). +#### `styleSrc` + +_Default: `false`_ + +When true, adds nonce to the `style-src` directive of your Content Security Policy to prevent attackers from modifying the contents or appearance of your page. + ## Debugging ### Limiting edge function invocations diff --git a/manifest.yml b/manifest.yml index b5ab60d..a1e9028 100644 --- a/manifest.yml +++ b/manifest.yml @@ -14,3 +14,6 @@ inputs: - name: excludedPath description: The glob expressions of path(s) that *should not* invoke the CSP nonce edge function. Must be an array of strings. This value gets spread with common non-html filetype extensions (*.css, *.js, *.svg, etc) default: [] + - name: styleSrc + description: When true, adds nonce to the `style-src` directive of your Content Security Policy to prevent attackers from modifying the contents or appearance of your page. + default: false diff --git a/src/__csp-nonce.ts b/src/__csp-nonce.ts index a38e4c9..8d8c465 100644 --- a/src/__csp-nonce.ts +++ b/src/__csp-nonce.ts @@ -15,6 +15,7 @@ type Params = { unsafeEval: boolean; path: string | string[]; excludedPath: string[]; + styleSrc: boolean; }; const params = inputs as Params; @@ -78,7 +79,10 @@ const handler = async (request: Request, context: Context) => { `https:`, `http:`, ].filter(Boolean); - const scriptSrc = `script-src ${rules.join(" ")}`; + + const joinedRules = rules.join(" "); + const scriptSrc = `script-src ${joinedRules}`; + const styleSrc = `style-src ${joinedRules}`; const reportUri = `report-uri ${ params.reportUri || "/.netlify/functions/__csp-violations" }`; @@ -96,6 +100,12 @@ const handler = async (request: Request, context: Context) => { // https://github.com/netlify/plugin-csp-nonce/issues/72 return d.replace("script-src ", `${scriptSrc} `).trim(); } + // intentionally add trailing space to avoid mangling `style-src-elem` + if (params.styleSrc && d.startsWith("style-src ")) { + // append with trailing space to include any user-supplied values + // https://github.com/netlify/plugin-csp-nonce/issues/72 + return d.replace("style-src ", `${styleSrc} `).trim(); + } // intentionally omit report-uri: theirs should take precedence return d; }) @@ -104,6 +114,9 @@ const handler = async (request: Request, context: Context) => { if (!directives.find((d) => d.startsWith("script-src "))) { directives.push(scriptSrc); } + if (params.styleSrc && !directives.find((d) => d.startsWith("style-src "))) { + directives.push(styleSrc); + } if (!directives.find((d) => d.startsWith("report-uri"))) { directives.push(reportUri); } @@ -111,11 +124,16 @@ const handler = async (request: Request, context: Context) => { response.headers.set(header, value); } else { // make a new ruleset of directives if no CSP present - const value = [scriptSrc, reportUri].join("; "); + const value = [scriptSrc, params.styleSrc && styleSrc, reportUri].filter(Boolean).join("; "); response.headers.set(header, value); } const querySelectors = ["script", 'link[rel="preload"][as="script"]']; + if (params.styleSrc) { + querySelectors.push("style"); + querySelectors.push('link[rel="preload"][as="style"]'); + } + return new HTMLRewriter() .on(querySelectors.join(","), { element(element: HTMLElement) {