Last active
March 8, 2023 22:56
-
-
Save jhollinger/77265f9ed298ad8077b9fd7a82335c8d to your computer and use it in GitHub Desktop.
Rewrites index.html to with CSP strict-dynamic enabled
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Usage: node csp-strict-dynamic.js dist/index.html [path/to/nginx.conf] | |
* | |
* Replaces all external script tags in index.html with dynamic loaders, calculates their SHA 256 hashes, and adds those | |
* hashes as allowed script sources + strict dynamic. Also works for inline scripts. | |
* | |
* Your index.html and/or given nginx/apache config file should contain your CSP policy with a script-src section like below: | |
* | |
* script-src 'strict-dynamic' {{csp-strict-dynamic-sources}}; | |
*/ | |
const fs = require('fs'); | |
const path = require('path'); | |
const crypto = require('crypto'); | |
const buildDir = process.argv[1]; | |
const htmlPath = process.argv[2]; | |
const cspConfigPath = process.argv[3]; | |
const cspStrictDynamicPlaceholder = '{{csp-strict-dynamic-sources}}'; | |
const scriptPlaceholder = '{{strict-dynamic-js-loader}}'; | |
const cspSources = []; | |
const jsSources = []; | |
const html = fs.readFileSync(htmlPath, {encoding: 'utf8'}); | |
const newHtml = html | |
// make inline scripts CSP-safe | |
.replace(/<script(\s+type="text\/javascript")?\s*>(.+?)<\/script>/gis, (_all, _type, src) => { | |
const hash = genHash(src); | |
cspSources.push(hash); | |
return `\n<!-- ${hash} -->\n<script>${src}</script>\n`; | |
}) | |
// collapse all external scripts into a single template | |
.replace(/<script\s+src\s*=\s*"([^"]+)"[^>]*>\s*<\/script>/gi, (script, src) => { | |
jsSources.push(src); | |
const first = jsSources.length === 1; | |
return first ? scriptPlaceholder : ''; | |
}) | |
// fill in the template with a dynamic loader that's CSP-safe | |
.replace(scriptPlaceholder, () => { | |
const loader = genLoader(jsSources); | |
const hash = genHash(loader); | |
cspSources.push(hash); | |
return `\n<!-- ${hash} -->\n<script>${loader}</script>\n`; | |
}) | |
.replace(cspStrictDynamicPlaceholder, cspSources.join(' ')); | |
fs.writeFileSync(htmlPath, newHtml); | |
if (cspConfigPath) { | |
const cspConfig = fs.readFileSync(cspConfigPath, {encoding: 'utf8'}); | |
const newCspConfig = cspConfig.replace(cspStrictDynamicPlaceholder, cspSources.join(' ')); | |
fs.writeFileSync(cspConfigPath, newCspConfig); | |
} | |
function genLoader(sources) { | |
const jsArray = JSON.stringify(sources); | |
return ` | |
_loadScriptsInOrder(${jsArray}); | |
function _loadScriptsInOrder(sources) { | |
if (sources.length === 0) return; | |
var src = sources.shift(); | |
var script = document.createElement('script'); | |
script.setAttribute('src', src); | |
script.setAttribute('defer', ''); | |
script.onload = function() { _loadScriptsInOrder(sources) }; | |
document.body.appendChild(script); | |
} | |
`; | |
} | |
function genHash(data) { | |
const hash = crypto.createHash('sha256'); | |
hash.update(data); | |
const digest = hash.digest('base64'); | |
return `'sha256-${digest}'`; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment