Astro Info
Astro v5.x (latest)
Node v22.x
Package manager pnpm
Platform linux
If this issue only occurs in one browser, which browser is a problem?
No response
Describe the Bug
pushDirective() in packages/astro/src/core/csp/runtime.ts (lines 33–57) pushes the new directive inside the loop for every existing directive whose name doesn't match. The result is a corrupted CSP header whenever Astro.csp.insertDirective(...) is called with a directive name that doesn't already match the first existing directive.
for (const directive of directives) {
if (deduplicated) {
finalDirectives.push(directive);
continue;
}
const result = deduplicateDirectiveValues(directive, newDirective);
if (result) {
finalDirectives.push(result);
deduplicated = true;
} else {
finalDirectives.push(directive);
finalDirectives.push(newDirective); // ❌ pushed once per non-matching iteration
}
}
Two failure modes follow from this:
1. New directive name doesn't exist in the list. Every iteration takes the else branch and pushes newDirective again.
- existing:
["img-src 'self'", "style-src 'self'"]
- push:
"script-src 'self'"
- expected:
["img-src 'self'", "style-src 'self'", "script-src 'self'"]
- actual:
["img-src 'self'", "script-src 'self'", "style-src 'self'", "script-src 'self'"]
2. Match exists, but only after non-matching directives. Every iteration before the match pushes a stray copy of the un-merged newDirective.
- existing:
["img-src 'self'", "script-src 'self'"]
- push:
"script-src 'unsafe-inline'"
- expected:
["img-src 'self'", "script-src 'self' 'unsafe-inline'"]
- actual:
["img-src 'self'", "script-src 'unsafe-inline'", "script-src 'self' 'unsafe-inline'"]
The rendered Content-Security-Policy header for case 1 becomes img-src 'self'; script-src 'self'; style-src 'self'; script-src 'self' — the same directive name appearing twice on the wire is malformed CSP and browsers will treat duplicates per their own merging rules.
pushDirective is reachable from user code: Astro.csp.insertDirective(payload) calls pushDirective(result.directives, payload) at render-context.ts lines 580 and 869. result.directives is pre-populated from security.csp.directives config, so any project that combines configured directives with runtime insertDirective calls is affected.
The single existing unit test in packages/astro/test/units/csp/runtime.test.ts (lines 42–50) only covers the case where the new directive's name matches the first existing directive — so neither failure mode above is caught.
The fix is to push newDirective once after the loop, gated on a matched flag, instead of inside the else branch.
What's the expected result?
pushDirective(["img-src 'self'", "style-src 'self'"], "script-src 'self'") should return ["img-src 'self'", "style-src 'self'", "script-src 'self'"] — three directives, no duplicates. Each directive name should appear at most once in the resulting list.
Link to Minimal Reproducible Example
https://github.com/Hunnyboy1217/astro-csp-pushdirective-repro
Participation
Astro Info
If this issue only occurs in one browser, which browser is a problem?
No response
Describe the Bug
pushDirective()inpackages/astro/src/core/csp/runtime.ts(lines 33–57) pushes the new directive inside the loop for every existing directive whose name doesn't match. The result is a corrupted CSP header wheneverAstro.csp.insertDirective(...)is called with a directive name that doesn't already match the first existing directive.Two failure modes follow from this:
1. New directive name doesn't exist in the list. Every iteration takes the
elsebranch and pushesnewDirectiveagain.["img-src 'self'", "style-src 'self'"]"script-src 'self'"["img-src 'self'", "style-src 'self'", "script-src 'self'"]["img-src 'self'", "script-src 'self'", "style-src 'self'", "script-src 'self'"]2. Match exists, but only after non-matching directives. Every iteration before the match pushes a stray copy of the un-merged
newDirective.["img-src 'self'", "script-src 'self'"]"script-src 'unsafe-inline'"["img-src 'self'", "script-src 'self' 'unsafe-inline'"]["img-src 'self'", "script-src 'unsafe-inline'", "script-src 'self' 'unsafe-inline'"]The rendered
Content-Security-Policyheader for case 1 becomesimg-src 'self'; script-src 'self'; style-src 'self'; script-src 'self'— the same directive name appearing twice on the wire is malformed CSP and browsers will treat duplicates per their own merging rules.pushDirectiveis reachable from user code:Astro.csp.insertDirective(payload)callspushDirective(result.directives, payload)atrender-context.tslines 580 and 869.result.directivesis pre-populated fromsecurity.csp.directivesconfig, so any project that combines configured directives with runtimeinsertDirectivecalls is affected.The single existing unit test in
packages/astro/test/units/csp/runtime.test.ts(lines 42–50) only covers the case where the new directive's name matches the first existing directive — so neither failure mode above is caught.The fix is to push
newDirectiveonce after the loop, gated on amatchedflag, instead of inside theelsebranch.What's the expected result?
pushDirective(["img-src 'self'", "style-src 'self'"], "script-src 'self'")should return["img-src 'self'", "style-src 'self'", "script-src 'self'"]— three directives, no duplicates. Each directive name should appear at most once in the resulting list.Link to Minimal Reproducible Example
https://github.com/Hunnyboy1217/astro-csp-pushdirective-repro
Participation