-
Notifications
You must be signed in to change notification settings - Fork 17
feat: Enable CSP with nonce for Helix 5 #773
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
Changes from 1 commit
de852c9
2489e05
fdb2aeb
58e9ba0
9736580
c8a84af
1a06aa9
6a237ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
/* | ||
* Copyright 2024 Adobe. All rights reserved. | ||
* This file is licensed to you under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. You may obtain a copy | ||
* of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software distributed under | ||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS | ||
* OF ANY KIND, either express or implied. See the License for the specific language | ||
* governing permissions and limitations under the License. | ||
*/ | ||
import crypto from 'crypto'; | ||
import { select, selectAll } from 'hast-util-select'; | ||
import { remove } from 'unist-util-remove'; | ||
|
||
export const NONCE_AEM = '\'nonce-aem\''; | ||
|
||
function parseCSP(csp) { | ||
const parts = csp.split(';'); | ||
const result = {}; | ||
parts.forEach((part) => { | ||
const [directive, ...values] = part.trim().split(' '); | ||
result[directive] = values.join(' '); | ||
}); | ||
return result; | ||
} | ||
|
||
function shouldApplyNonce(csp) { | ||
const parsedCSP = parseCSP(csp); | ||
return { | ||
scriptNonce: parsedCSP['script-src']?.includes(NONCE_AEM), | ||
styleNonce: parsedCSP['style-src']?.includes(NONCE_AEM), | ||
}; | ||
} | ||
|
||
function createAndApplyNonce(res, tree, metaCSP, headersCSP) { | ||
const nonce = crypto.randomBytes(16).toString('base64'); | ||
let scriptNonceResult = false; | ||
let styleNonceResult = false; | ||
|
||
if (metaCSP) { | ||
const { scriptNonce, styleNonce } = shouldApplyNonce(metaCSP.properties.content); | ||
scriptNonceResult ||= scriptNonce; | ||
styleNonceResult ||= styleNonce; | ||
metaCSP.properties.content = metaCSP.properties.content.replaceAll(NONCE_AEM, `'nonce-${nonce}'`); | ||
} | ||
|
||
if (headersCSP) { | ||
const { scriptNonce, styleNonce } = shouldApplyNonce(headersCSP); | ||
scriptNonceResult ||= scriptNonce; | ||
styleNonceResult ||= styleNonce; | ||
res.headers.set('content-security-policy', headersCSP.replaceAll(NONCE_AEM, `'nonce-${nonce}'`)); | ||
} | ||
|
||
if (scriptNonceResult) { | ||
selectAll('script[nonce="aem"]', tree).forEach((el) => { | ||
el.properties.nonce = nonce; | ||
}); | ||
} | ||
|
||
if (styleNonceResult) { | ||
selectAll('style[nonce="aem"]', tree).forEach((el) => { | ||
el.properties.nonce = nonce; | ||
}); | ||
selectAll('link[rel=stylesheet][nonce="aem"]', tree).forEach((el) => { | ||
el.properties.nonce = nonce; | ||
}); | ||
} | ||
} | ||
|
||
export function checkResponseBodyForMetaBasedCSP(res) { | ||
return res.body?.includes('http-equiv="content-security-policy"') | ||
|| res.body?.includes('http-equiv="Content-Security-Policy"'); | ||
} | ||
|
||
export function checkResponseBodyForAEMNonce(res) { | ||
return res.body?.includes(NONCE_AEM); | ||
andreituicu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
export function getMetaCSP(tree) { | ||
return select('meta[http-equiv="content-security-policy"]', tree) | ||
|| select('meta[http-equiv="Content-Security-Policy"]', tree); | ||
} | ||
|
||
export function getHeaderCSP(res) { | ||
return res.headers?.get('content-security-policy'); | ||
} | ||
|
||
export function contentSecurityPolicy(res, tree) { | ||
const metaCSP = getMetaCSP(tree); | ||
const headersCSP = getHeaderCSP(res); | ||
|
||
if (!metaCSP && !headersCSP) { | ||
// No CSP defined | ||
return; | ||
} | ||
|
||
// CSP with nonce | ||
if ( | ||
(metaCSP && metaCSP.properties.content.includes(NONCE_AEM)) | ||
|| (headersCSP && headersCSP.includes(NONCE_AEM)) | ||
andreituicu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) { | ||
createAndApplyNonce(res, tree, metaCSP, headersCSP); | ||
} | ||
|
||
if (metaCSP && metaCSP.properties['move-as-header']) { | ||
if (!headersCSP) { | ||
// if we have a CSP in meta but no CSP in headers | ||
// we can move the CSP from meta to headers, if requested | ||
res.headers.set('content-security-policy', metaCSP.properties.content); | ||
remove(tree, null, metaCSP); | ||
} else { | ||
delete metaCSP.properties['move-as-header']; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,16 @@ | |
* governing permissions and limitations under the License. | ||
*/ | ||
import mime from 'mime'; | ||
import { unified } from 'unified'; | ||
import rehypeParse from 'rehype-parse'; | ||
import { | ||
contentSecurityPolicy, | ||
getHeaderCSP, | ||
checkResponseBodyForMetaBasedCSP, | ||
NONCE_AEM, | ||
checkResponseBodyForAEMNonce, | ||
} from './csp.js'; | ||
import tohtml from './stringify-response.js'; | ||
|
||
const CHARSET_RE = /charset=([^()<>@,;:"/[\]?.=\s]*)/i; | ||
|
||
|
@@ -32,4 +42,19 @@ export default async function renderCode(state, req, res) { | |
} | ||
} | ||
res.headers.set('content-type', contentType); | ||
|
||
const cspHeader = getHeaderCSP(res); | ||
if (state.type === 'html' | ||
&& (cspHeader?.includes(NONCE_AEM) || ( | ||
checkResponseBodyForAEMNonce(res) && checkResponseBodyForMetaBasedCSP(res)) | ||
) | ||
) { | ||
res.document = await unified() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure if we really should rewrite the provided HTML. and if so, with unified which might alter the html. maybe using a very simple text based parser that only understands minimal HTML and repaces the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I used unified because that what was used in processing the Indeed, it does alter the resulting HTML, it makes it canonical (e.g. from the point of view of spaces, indentation,
Would you have any in mind that I could try out? Intuitively, I would expect that any parser when serialising back to canonicalise, since otherwise it would be very hard to store all the nuances of the original HTML. If we want to keep the customer's HTML untouched except for the nonce generation, we could try a regex approach. I usually avoid regexes for this kind of processing, because I'm not good at them, they become hard to maintain troubleshoot and I've seen Kodiak complain about ReDos, which means they could be slow for certain files. Did a quick try with res.body.replace(/(<script\b[^>]*\bnonce=")aem(")/ig, `$1${nonce}$2`)
.replace(/(<style\b[^>]*\bnonce=")aem(")/ig, `$1${nonce}$2`)
.replace(/(<link\b[^>]*\bnonce=")aem(")/ig, `$1${nonce}$2`)
.replace(/(<meta\b[^>]*'nonce-)aem(')/ig, `$1${nonce}$2`)
.replace(/(<meta\b[^>]*'nonce-)aem(')/ig, `$1${nonce}$2`) //can appear twice I can't speak for all customers, but personally, as a developer, I think I would like that if I drop some messy HTML in github that I copied from somewhere I get a clean one when looking through the delivery service. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @tripodsan WDYT of the latest approach from fdb2aeb? I found a nice parser from Additionally, if I understand correctly, the Here is an example where it preserves everything and doesn't add anything extra: https://github.com/adobe/helix-html-pipeline/pull/773/files#diff-aced0b80c69cd5d57663548aa6ff82a7ab4f3ce37078a65eecc97ecc0c2cf4ba Can now be tried with the following
|
||
.use(rehypeParse) | ||
.parse(res.body); | ||
res.body = undefined; | ||
|
||
contentSecurityPolicy(res, res.document); | ||
await tohtml(state, req, res); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
<html> | ||
<head> | ||
<title>ACME CORP</title> | ||
<link rel="canonical" href="https://www.adobe.com/nonce-headers-meta"> | ||
<meta name="description" content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, urna eu tempor congue, nisi erat condimentum nunc, eget tincidunt nisl nunc euismod."> | ||
<meta property="og:title" content="ACME CORP"> | ||
<meta property="og:description" content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, urna eu tempor congue, nisi erat condimentum nunc, eget tincidunt nisl nunc euismod."> | ||
<meta property="og:url" content="https://www.adobe.com/nonce-headers-meta"> | ||
<meta property="og:image" content="https://www.adobe.com/default-meta-image.png?width=1200&format=pjpg&optimize=medium"> | ||
<meta property="og:image:secure_url" content="https://www.adobe.com/default-meta-image.png?width=1200&format=pjpg&optimize=medium"> | ||
<meta name="twitter:card" content="summary_large_image"> | ||
<meta name="twitter:title" content="ACME CORP"> | ||
<meta name="twitter:description" content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, urna eu tempor congue, nisi erat condimentum nunc, eget tincidunt nisl nunc euismod."> | ||
<meta name="twitter:image" content="https://www.adobe.com/default-meta-image.png?width=1200&format=pjpg&optimize=medium"> | ||
<meta name="locale" content="en-US"> | ||
<meta name="zero-cell" content="0"> | ||
<script nonce="aem" src="/scripts/aem.js" type="module"></script> | ||
<script nonce="aem" src="/scripts/scripts.js" type="module"></script> | ||
<link nonce="aem" rel="stylesheet" href="/styles/styles.css"/> | ||
<script nonce="aem" > const a = 1 </script> | ||
<style nonce="aem" id="at-body-style">body {opacity: 1}</style> | ||
</head> | ||
<body> | ||
<header></header> | ||
<main> | ||
<div> | ||
<h1 id="nonce-test">Nonce Test</h1> | ||
<script nonce="aem" src="/scripts/aem2.js" type="module"></script> | ||
<script nonce="aem" src="/scripts/scripts2.js" type="module"></script> | ||
<link nonce="aem" rel="stylesheet" href="/styles/styles2.css"/> | ||
<script nonce="aem" > const a = 2 </script> | ||
<style nonce="aem" id="at-body-style2">body {opacity: 1}</style> | ||
</div> | ||
</main> | ||
<footer></footer> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
<html> | ||
<head> | ||
<title>ACME CORP</title> | ||
<link rel="canonical" href="https://www.adobe.com/nonce-headers-meta"> | ||
<meta name="description" content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, urna eu tempor congue, nisi erat condimentum nunc, eget tincidunt nisl nunc euismod."> | ||
<meta property="og:title" content="ACME CORP"> | ||
<meta property="og:description" content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, urna eu tempor congue, nisi erat condimentum nunc, eget tincidunt nisl nunc euismod."> | ||
<meta property="og:url" content="https://www.adobe.com/nonce-headers-meta"> | ||
<meta property="og:image" content="https://www.adobe.com/default-meta-image.png?width=1200&format=pjpg&optimize=medium"> | ||
<meta property="og:image:secure_url" content="https://www.adobe.com/default-meta-image.png?width=1200&format=pjpg&optimize=medium"> | ||
<meta name="twitter:card" content="summary_large_image"> | ||
<meta name="twitter:title" content="ACME CORP"> | ||
<meta name="twitter:description" content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, urna eu tempor congue, nisi erat condimentum nunc, eget tincidunt nisl nunc euismod."> | ||
<meta name="twitter:image" content="https://www.adobe.com/default-meta-image.png?width=1200&format=pjpg&optimize=medium"> | ||
<meta name="locale" content="en-US"> | ||
<meta name="zero-cell" content="0"> | ||
<script nonce="ckFuZDBtbW1yQW5kMG1tbQ==" src="/scripts/aem.js" type="module"></script> | ||
<script nonce="ckFuZDBtbW1yQW5kMG1tbQ==" src="/scripts/scripts.js" type="module"></script> | ||
<link nonce="ckFuZDBtbW1yQW5kMG1tbQ==" rel="stylesheet" href="/styles/styles.css"/> | ||
<script nonce="ckFuZDBtbW1yQW5kMG1tbQ=="> const a = 1 </script> | ||
<style nonce="ckFuZDBtbW1yQW5kMG1tbQ==" id="at-body-style">body {opacity: 1}</style> | ||
</head> | ||
<body> | ||
<header></header> | ||
<main> | ||
<div> | ||
<h1 id="nonce-test">Nonce Test</h1> | ||
<script nonce="ckFuZDBtbW1yQW5kMG1tbQ==" src="/scripts/aem2.js" type="module"></script> | ||
<script nonce="ckFuZDBtbW1yQW5kMG1tbQ==" src="/scripts/scripts2.js" type="module"></script> | ||
<link nonce="ckFuZDBtbW1yQW5kMG1tbQ==" rel="stylesheet" href="/styles/styles2.css"/> | ||
<script nonce="ckFuZDBtbW1yQW5kMG1tbQ=="> const a = 2 </script> | ||
<style nonce="ckFuZDBtbW1yQW5kMG1tbQ==" id="at-body-style2">body {opacity: 1}</style> | ||
</div> | ||
</main> | ||
<footer></footer> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<html> | ||
<head> | ||
<meta http-equiv="Content-Security-Policy" content="script-src 'nonce-r4nD0m' 'strict-dynamic'; style-src 'nonce-r4nD0m'; base-uri 'self'; object-src 'none';"> | ||
<title>ACME CORP</title> | ||
<link rel="canonical" href="https://www.adobe.com/nonce-meta-different"> | ||
<meta name="description" content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, urna eu tempor congue, nisi erat condimentum nunc, eget tincidunt nisl nunc euismod."> | ||
<meta property="og:title" content="ACME CORP"> | ||
<meta property="og:description" content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, urna eu tempor congue, nisi erat condimentum nunc, eget tincidunt nisl nunc euismod."> | ||
<meta property="og:url" content="https://www.adobe.com/nonce-meta-different"> | ||
<meta property="og:image" content="https://www.adobe.com/default-meta-image.png?width=1200&format=pjpg&optimize=medium"> | ||
<meta property="og:image:secure_url" content="https://www.adobe.com/default-meta-image.png?width=1200&format=pjpg&optimize=medium"> | ||
<meta name="twitter:card" content="summary_large_image"> | ||
<meta name="twitter:title" content="ACME CORP"> | ||
<meta name="twitter:description" content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, urna eu tempor congue, nisi erat condimentum nunc, eget tincidunt nisl nunc euismod."> | ||
<meta name="twitter:image" content="https://www.adobe.com/default-meta-image.png?width=1200&format=pjpg&optimize=medium"> | ||
<meta name="locale" content="en-US"> | ||
<meta name="zero-cell" content="0"> | ||
<script nonce="r4nD0m" src="/scripts/aem.js" type="module"></script> | ||
<script nonce="r4nD0m" src="/scripts/scripts.js" type="module"></script> | ||
<link nonce="r4nD0m" rel="stylesheet" href="/styles/styles.css"/> | ||
<script nonce="r4nD0m" > const a = 1 </script> | ||
<style nonce="r4nD0m" id="at-body-style">body {opacity: 1}</style> | ||
</head> | ||
<body> | ||
<header></header> | ||
<main> | ||
<div> | ||
<h1 id="nonce-test">Nonce Test</h1> | ||
<script nonce="r4nD0m" src="/scripts/aem2.js" type="module"></script> | ||
<script nonce="r4nD0m" src="/scripts/scripts2.js" type="module"></script> | ||
<link nonce="r4nD0m" rel="stylesheet" href="/styles/styles2.css"/> | ||
<script nonce="r4nD0m" > const a = 2 </script> | ||
<style nonce="r4nD0m" id="at-body-style2">body {opacity: 1}</style> | ||
</div> | ||
</main> | ||
<footer></footer> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<html> | ||
<head> | ||
<meta http-equiv="Content-Security-Policy" content="script-src 'nonce-r4nD0m' 'strict-dynamic'; style-src 'nonce-r4nD0m'; base-uri 'self'; object-src 'none';"> | ||
<title>ACME CORP</title> | ||
<link rel="canonical" href="https://www.adobe.com/nonce-meta-different"> | ||
<meta name="description" content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, urna eu tempor congue, nisi erat condimentum nunc, eget tincidunt nisl nunc euismod."> | ||
<meta property="og:title" content="ACME CORP"> | ||
<meta property="og:description" content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, urna eu tempor congue, nisi erat condimentum nunc, eget tincidunt nisl nunc euismod."> | ||
<meta property="og:url" content="https://www.adobe.com/nonce-meta-different"> | ||
<meta property="og:image" content="https://www.adobe.com/default-meta-image.png?width=1200&format=pjpg&optimize=medium"> | ||
<meta property="og:image:secure_url" content="https://www.adobe.com/default-meta-image.png?width=1200&format=pjpg&optimize=medium"> | ||
<meta name="twitter:card" content="summary_large_image"> | ||
<meta name="twitter:title" content="ACME CORP"> | ||
<meta name="twitter:description" content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, urna eu tempor congue, nisi erat condimentum nunc, eget tincidunt nisl nunc euismod."> | ||
<meta name="twitter:image" content="https://www.adobe.com/default-meta-image.png?width=1200&format=pjpg&optimize=medium"> | ||
<meta name="locale" content="en-US"> | ||
<meta name="zero-cell" content="0"> | ||
<script nonce="r4nD0m" src="/scripts/aem.js" type="module"></script> | ||
<script nonce="r4nD0m" src="/scripts/scripts.js" type="module"></script> | ||
<link nonce="r4nD0m" rel="stylesheet" href="/styles/styles.css"/> | ||
<script nonce="r4nD0m"> const a = 1 </script> | ||
<style nonce="r4nD0m" id="at-body-style">body {opacity: 1}</style> | ||
</head> | ||
<body> | ||
<header></header> | ||
<main> | ||
<div> | ||
<h1 id="nonce-test">Nonce Test</h1> | ||
<script nonce="r4nD0m" src="/scripts/aem2.js" type="module"></script> | ||
<script nonce="r4nD0m" src="/scripts/scripts2.js" type="module"></script> | ||
<link nonce="r4nD0m" rel="stylesheet" href="/styles/styles2.css"/> | ||
<script nonce="r4nD0m"> const a = 2 </script> | ||
<style nonce="r4nD0m" id="at-body-style2">body {opacity: 1}</style> | ||
</div> | ||
</main> | ||
<footer></footer> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<html> | ||
<head> | ||
<meta http-equiv="content-security-policy" content="script-src 'nonce-aem' 'strict-dynamic'; style-src 'nonce-aem'; base-uri 'self'; object-src 'none';" move-as-header="true"> | ||
<title>ACME CORP</title> | ||
<link rel="canonical" href="https://www.adobe.com/nonce-headers-meta"> | ||
<meta name="description" content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, urna eu tempor congue, nisi erat condimentum nunc, eget tincidunt nisl nunc euismod."> | ||
<meta property="og:title" content="ACME CORP"> | ||
<meta property="og:description" content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, urna eu tempor congue, nisi erat condimentum nunc, eget tincidunt nisl nunc euismod."> | ||
<meta property="og:url" content="https://www.adobe.com/nonce-headers-meta"> | ||
<meta property="og:image" content="https://www.adobe.com/default-meta-image.png?width=1200&format=pjpg&optimize=medium"> | ||
<meta property="og:image:secure_url" content="https://www.adobe.com/default-meta-image.png?width=1200&format=pjpg&optimize=medium"> | ||
<meta name="twitter:card" content="summary_large_image"> | ||
<meta name="twitter:title" content="ACME CORP"> | ||
<meta name="twitter:description" content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, urna eu tempor congue, nisi erat condimentum nunc, eget tincidunt nisl nunc euismod."> | ||
<meta name="twitter:image" content="https://www.adobe.com/default-meta-image.png?width=1200&format=pjpg&optimize=medium"> | ||
<meta name="locale" content="en-US"> | ||
<meta name="zero-cell" content="0"> | ||
<script nonce="aem" src="/scripts/aem.js" type="module"></script> | ||
<script nonce="aem" src="/scripts/scripts.js" type="module"></script> | ||
<link nonce="aem" rel="stylesheet" href="/styles/styles.css"/> | ||
<script nonce="aem" > const a = 1 </script> | ||
<style nonce="aem" id="at-body-style">body {opacity: 1}</style> | ||
</head> | ||
<body> | ||
<header></header> | ||
<main> | ||
<div> | ||
<h1 id="nonce-test">Nonce Test</h1> | ||
<script nonce="aem" src="/scripts/aem2.js" type="module"></script> | ||
<script nonce="aem" src="/scripts/scripts2.js" type="module"></script> | ||
<link nonce="aem" rel="stylesheet" href="/styles/styles2.css"/> | ||
<script nonce="aem" > const a = 2 </script> | ||
<style nonce="aem" id="at-body-style2">body {opacity: 1}</style> | ||
</div> | ||
</main> | ||
<footer></footer> | ||
</body> | ||
</html> |
Uh oh!
There was an error while loading. Please reload this page.