Replies: 20 comments 65 replies
-
So apparently using the following config makes it possible to force scripts to not be inlined:
This isn't ideal though, bacause assetsInlineLimit (according to vite docs) is also used for images and other resources, not just for scripts. So it would still be nice to have a way to configure this for scripts. |
Beta Was this translation helpful? Give feedback.
-
OK, I can see a couple of paths that could be explored here, but would need some more expert eyes to evaluate the practicality and additional hurdles these approaches could throw up. Maintain the current inline script behaviourOne possibility might for Astro to provide hashes for the inline island scripts to use in a CSP as suggested by @gerjon-eatch in withastro/docs#2150 and documented in MDN’s Add a hash for the contents of each inline script:
Astro could do this automatically:
It would need to know the hash of each inline script on the page and inject each hash into the CSP. Questions
Make it possible to switch off inline scriptsThe other alternative is to hoist inline scripts into one or more external JS modules to avoid the need for managing inline script permissions in the CSP (you can just specify the allowed origin like you do for other hoisted scripts today). This seems superficially simpler, but there may be good reasons why we don’t already do this. |
Beta Was this translation helpful? Give feedback.
-
If there is already a CSP header, the meta tag can only make the policy more restricting. So allowing sources (scripts, styles) by hashes will only work if both the header and the meta tag allow those sources. If only the meta tag contains the hashes, the scripts will not be allowed (unless the header allows them by other means, such as unsafe-inline, or if there is no CSP header).
From an astro user perspective, especially one using SSG, this would imho be the preferred solution. CSP's are commonly managed on proxies, CDN's, or edge servers, or by separate entities within an organisation. In such cases using hashes to whitelist sources is just not practical. |
Beta Was this translation helpful? Give feedback.
-
I would love it if something like this could work: ---
// server-side JS if necessary
---
<div id="some-div" />
<script src="@some/npm-module"></script>
<script>
import SomeNpmModule from "@some/npm-module";
SomeNpmModule("#some-div");
</script> This, of course, assumes that |
Beta Was this translation helpful? Give feedback.
-
Would using |
Beta Was this translation helpful? Give feedback.
-
Question is how much time each approach adds to the build time. Bundling everything together would be faster and calculating hashes for everything sounds much slower, I would think. Maybe implement both? Apparently there is vite-plugin-csp. Readme says its still in early development, but maybe there are foundations there. |
Beta Was this translation helpful? Give feedback.
-
IMHO unless there's a way to fully turn off / block the use of inline scripts (and inline styles too!) it is effectively impossible to produce a website with AstroJS that meets current-generation security standards. The prevailing word is: https://web.dev/csp/#inline-code-is-considered-harmful From that source:
Mozilla, Google, and others offer security analysis tools e.g. https://observatory.mozilla.org/, Mozilla Laboratory, https://csp-evaluator.withgoogle.com/, etc. and they will only get stricter with their recommendations + scoring over time. I think an ideal DevX would have this controlled via Astro config and if set if the dev even tries to use inline scripts or styles it will produce an error with helpful feedback. Wouldn't Astro be all the more impressive if it didn't only score top marks on performance scores out-of-the-box but security and privacy scores as well? |
Beta Was this translation helpful? Give feedback.
-
Just wanted to migrate a website that is currently being built with Hugo. Unfortunately this issue is a showstopper for me as I do not want to sacrifice the website's Observatory A+ rating (and the security aspect it's is based on, of course). Setting a CSP HTTP header instead of a meta tag has several advantages. For example:
This (among other reasons) is why HTTP header is the preferred solution (according to W3C: https://www.w3.org/TR/CSP3/#csp-header). For static sites the CSP HTTP header set by the webserver / proxy / CDN can't contain a hash. Hence, I'd suggest to add an option to disable inline scripts / styles and rather include those as externalized resources. Another option for inline scripts/styles with a hash might be added later. But this comes with higher complexity and questionable benefit (hasn't HTTP/2 or /3 taken the fright of multiple/additional page resources?). Would really love to see this changed, soon! BTW: Forget about nonces. W3C says in https://www.w3.org/TR/CSP3/#security-nonces: "Using a nonce to allow inline script or style is less secure than not using a nonce, as nonces override the restrictions in the directive in which they are present.". |
Beta Was this translation helpful? Give feedback.
-
For now I created a tool to parse the |
Beta Was this translation helpful? Give feedback.
-
I spent yesterday trying to find a solution to avoid using A few notes about this implementation:
import type { MiddlewareHandler } from 'astro'
import { sequence } from 'astro:middleware'
import cspBuilder from 'content-security-policy-builder'
import { parse } from 'node-html-parser'
import crypto from 'crypto'
const setCspHeader: MiddlewareHandler<any> = async function setCspHeader (_, next) {
const response = await next()
if (response.headers.get('content-type') !== 'text/html') return response
const originalHtml = await response.clone().text()
const parsedHtml = parse(originalHtml)
const newResponse = new Response(parsedHtml.outerHTML, {
status: response.status,
statusText: response.statusText,
headers: response.headers
})
const scripts = parsedHtml.querySelectorAll('script:not([src])')
const scriptHashes = scripts.map(script => createCspHash(script.textContent))
const styles = parsedHtml.querySelectorAll('style')
const styleHashes = styles.map(style => createCspHash(style.textContent))
newResponse.headers.set('Content-Security-Policy', cspBuilder({
directives: {
defaultSrc: ['\'self\''],
scriptSrc: [
'\'self\'',
'https://www.googletagmanager.com',
'https://www.googletagmanager.com',
'https://www.google.com',
'https://www.gstatic.com',
...scriptHashes
],
styleSrc: [
'\'self\'',
'https://fonts.googleapis.com',
...styleHashes
],
fontSrc: [
'https://fonts.gstatic.com'
],
imgSrc: [
'\'self\'',
'https://maps.googleapis.com'
],
frameSrc: [
'https://www.google.com'
],
connectSrc: [
'\'self\'',
'https://www.google-analytics.com'
]
}
}))
return newResponse
}
export const onRequest = sequence(setCspHeader)
function createCspHash (content: string): string {
return `'sha256-${crypto.createHash('sha256').update(content).digest('base64')}'`
} The above middleware produces a CSP similar to this
|
Beta Was this translation helpful? Give feedback.
-
I took from your ideas and with the help of this article managed to create something that works for me without CSP. remove-inline.cjs const glob = require('tiny-glob');
const path = require('path');
const fs = require('fs');
function hash(value) {
let hash = 5381;
let i = value.length;
while (i) hash = (hash * 33) ^ value.charCodeAt(--i);
return (hash >>> 0).toString(36);
}
async function removeInlineScriptAndStyle(directory) {
console.log('Removing Inline Scripts and Styles');
const scriptRegx = /<script[^>]*>([\s\S]+?)<\/script>/g;
const styleRegx = /<style[^>]*>([\s\S]+?)<\/style>/g;
const files = await glob('**/*.html', {
cwd: directory,
dot: true,
aboslute: true,
filesOnly: true,
});
console.log(`Found ${files.length} files`);
for (const file of files.map((f) => path.join(directory, f))) {
console.log(`Edit file: ${file}`);
let f = fs.readFileSync(file, { encoding: 'utf-8' });
let script;
while ((script = scriptRegx.exec(f))) {
const inlineScriptContent = script[1]
.replace('__sveltekit', 'const __sveltekit')
.replace(
'document.currentScript.parentElement',
'document.body.firstElementChild'
);
const fn = `/script-${hash(inlineScriptContent)}.js`;
f = f.replace(
script[0], // Using script[0] to replace the entire matched script tag
`<script type="module" src="${fn}"></script>`
);
fs.writeFileSync(`${directory}${fn}`, inlineScriptContent);
console.log(`Inline script extracted and saved at: ${directory}${fn}`);
}
let style;
while ((style = styleRegx.exec(f))) {
const inlineStyleContent = style[1];
const fn = `/style-${hash(inlineStyleContent)}.css`;
f = f.replace(
style[0], // Using style[0] to replace the entire matched style tag
`<link rel="stylesheet" href="${fn}" />`
);
fs.writeFileSync(`${directory}${fn}`, inlineStyleContent);
console.log(`Inline style extracted and saved at: ${directory}${fn}`);
}
fs.writeFileSync(file, f);
}
}
removeInlineScriptAndStyle(path.resolve(__dirname, 'dist')); package.json "scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build && npm run csp",
"preview": "astro preview",
"astro": "astro",
"csp": "node remove-inline.cjs"
},
"dependencies": {
"@astrojs/svelte": "^4.0.2",
"astro": "^3.1.2",
"svelte": "^4.2.1"
},
"devDependencies": {
"tiny-glob": "^0.2.9"
} |
Beta Was this translation helpful? Give feedback.
-
Adding my 2 cents here, not replying to any specific previous post as it's a mix and match, with replies to multiple people. Starting from the obvious, I'd expect to not have inline styles values when setting
This also mentions the Vite option to remove all inline assets which is, again, not respected. I understand it might be a build phase problem, which I can understand but should be clearly mentioned and if possible worked around, but can also very easily avoided with just a different kind of script tag that also helps to keep a cleaner DOM. Going into solutions proposed, coming on each I saw above. @matthewp mentioned in the issue I opened (withastro/astro#8719), too, that hashes and/or nonces are the ideal option for performance reasons.
Some users above proposed scripts that do it post-build, which is nice, until it doesn't work due to regex parsing, etc. and taking a file that Astro has externally (and links externally in dev builds), bundling into each HTML page, just to then re-split it with possible issues of parsing (like I had in my case) seems counterproductive. Of course |
Beta Was this translation helpful? Give feedback.
-
I'm also having an issue with this as I'd like to use Astro + Qwik for a chrome web extension, which is very strict about it's inline scripts since manifest v3. The current solution is to run a post build step to undo what Astro does and put the inline scripts in separate external sources. Would love to see this as a config option out of the box, with possible variants such as "no inline", "with nonce" or "with hash". |
Beta Was this translation helpful? Give feedback.
-
Is there any update here? This is a big blocker and the main reason I can't use Astro Islands. It's been more than a year... and 3 new major versions. For the time being the scope of the project still allows for Astro to work, but if I were to need any more feature in which an Island is almost required, I'd have to look elsewhere unfortunately :( |
Beta Was this translation helpful? Give feedback.
-
For future reference (and in case people still need help with this): It's very easy to write an Astro integration that outputs the CSP SHA-256 hashes for inlined scripts. One could use the output for their nginx/host provider configuration (for a static website) or even add meta tags in the html pages with appropriate CSP values. This is my implementation import type { AstroIntegration } from 'astro'
import { parse } from 'node-html-parser'
import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
const createCspHash = async (s: string) => {
const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(s))
const hashBase64 = btoa(String.fromCharCode(...new Uint8Array(hashBuffer)))
return `'sha256-${hashBase64}'`
}
export const astroCSPHashGenerator: AstroIntegration = {
name: 'astro-csp-hash-generator',
hooks: {
'astro:build:done': async ({ dir, pages, logger }) => {
let hashes = ''
for (let i = 0; i < pages.length; i++) {
const filePath = fileURLToPath(`${dir.href}${pages[i].pathname}index.html`)
try {
const root = parse(await readFile(filePath, { encoding: 'utf-8' }))
const scripts = root.querySelectorAll('script')
for (let j = 0; j < scripts.length; j++) {
const hash = await createCspHash(scripts[j].textContent)
hashes += hash + ' '
}
} catch (e) {
logger.error(`Cannot read file ${filePath}: ${e}`)
}
}
logger.info(hashes)
},
},
} This is zero-configuration and only looks at |
Beta Was this translation helpful? Give feedback.
-
I'm deploying my website to Cloudflare and use this very simple middleware to create a nonce and replace all const nonce = crypto.randomUUID();
for (const [header, value] of securityHeader(nonce)) {
response.headers.set(header, value);
}
const html = await response.text();
const result = html
.replaceAll("<script", `<script nonce="${nonce}"`)
.replaceAll("<style", `<style nonce="${nonce}"`);
... As pointed out here #377 (reply in thread) replacing tags like this defeats the purpose of CSP, as persistent XSS scripts would get the nonce assigned and then run on the client. Otherwise this simple setup works surprisingly well, but is not compatible with View Transitions. It would be great if we could get proper nonce support in Astro. |
Beta Was this translation helpful? Give feedback.
-
I prepared a packaged integration that is able to:
I worked on an Astro integration that adds the integrity hashes to the inlined scripts and styles, and it generates a JS module (in the path of your choice) that exports those hashes, so they can be later used in CSP header configurations or other similar purposes: https://www.npmjs.com/package/@kindspells/astro-sri-csp For now it only does that for the inlined resources, but I'll iterate over it to ensure that it does the same for "external" (both truly external, and served from the origin domain) scripts. |
Beta Was this translation helpful? Give feedback.
-
We could easily offer up hashes for the finite number of scripts we need to inline. Matthew brought this up a year ago, but one user expressed hesitation was that it may be infeasible for some people. However, no one specifically said that it would be infeasible for them specifically. I think this is worth bringing up again, because we shouldn't let perfect be the enemy of good enough. In the absence of a proper solution, I am noticing flimsy hacks gain more and more popularity. So, would it be acceptable if astro exported the hashes for the handful of scripts that it may inline? You could create a meta tag, or the header yourself using these. To emphasize, please answer for yourself, your clients, or your organization - not for hypothetical use-cases that other people may have. |
Beta Was this translation helpful? Give feedback.
-
We have to solve this critical issue and get it over with , I think this is the most demanded Astro feature (not feature, rather fix to a critical security vulnerability). This will hold back enterprises and security-valuing individuals from using and adpoting Astro. I think the easiest way for now is to implement Next.js's solution, which is adding a nonce with Middleware. This is the code from their docs:
Is this already achievable in Astro? Does Astro have response.headers.set()? P.S. |
Beta Was this translation helpful? Give feedback.
-
Is there any update on this topic, not visible here? |
Beta Was this translation helpful? Give feedback.
-
When creating astro components/layouts with client side scripts, as documented here: https://docs.astro.build/en/core-concepts/astro-components/, the processed+bundled scripts are rendered inline in the html of the page, during a production build.
This forces the use of a CSP with
script-src: 'unsafe-inline'
, which is called unsafe for obvious reasons.It would be nice to have an astro config option, or a configration attribute for script tags, to instruct astro to create a javascript file for the bundled javascript, and then reference that file via a src attribute on the rendered script tag, instead of inlining the bundled javascript.
(During devlopment
npm run dev
the processed javascript is actually linked via a script src already, making dev builds and production builds behave differently with some content security policies).The same could be done for stylesheets, making it configurable whether the stylesheet is inlined in the page (requiring style-src unsafe-inline), or included via a link.
Beta Was this translation helpful? Give feedback.
All reactions