Skip to content

csp: pushDirective duplicates new directive on every non-matching iteration #16594

@hunnyboy1217

Description

@hunnyboy1217

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

  • I am willing to submit a pull request for this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    - P4: importantViolate documented behavior or significantly impacts performance (priority)pkg: astroRelated to the core `astro` package (scope)

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions