Skip to content

Repeated calls to headers.contentSecurityPolicy(...) should not clobber previous ones #19842

Open

Description

@jzheaux

Today, invoking headers.contentSecurityPolicy(...) more than once replaces everything the previous call configured, in both stacks:

  • Servlet: HeadersConfigurer#contentSecurityPolicy(Customizer) constructs a new ContentSecurityPolicyConfig (and with it a new ContentSecurityPolicyHeaderWriter) on every call.
  • Reactive: HeaderSpec#contentSecurityPolicy(Customizer) creates a new ContentSecurityPolicySpec, which resets the policy directives (and, with Support nonce-based Content Security Policy #18499, the exchange matcher and nonce filter), while reportOnly is retained.

Other HeadersConfigurer configs let repeated calls compose so this is inconsistent and surprising. This ticket is to look at addressing this inconsistency.

@ziqin has a branch worth considering at https://github.com/ziqin/spring-security/tree/multi-csp-config-call

Since this changes behavior for applications that call the method more than once, we may need to wait until the next major unless this can be conceived as a bug.

Activity

  1. changed the title [-]Allow repeated headers.contentSecurityPolicy(...) calls to compose instead of replace[/-] [+]Repeated calls to headers.contentSecurityPolicy(...) should not clobber previous ones[/+] on Oct 7, 2026
  2. added a commit that references this issue on Oct 9, 2026
    b9c14ec
  3. ziqin commented on Oct 9, 2026

    @ziqin
    Contributor

    I tend to regard this inconsistency as a bug, especially the chaotic situation in the reactive HeaderSpec.

    Usually, people won’t call headers.contentSecurityPolicy(...) multiple times within one SecurityFilterChain bean definition. It might be repeatedly called by accident, however, when multiple modular configuration Customizer beans are defined in different locations.

    By retaining all customizations instead of resetting, unintended repeated calls of headers.contentSecurityPolicy(...) are more likely to be discovered.

  4. added a commit that references this issue on Oct 14, 2026
    ac0d1b3
  5. ziqin commented on Oct 15, 2026

    @ziqin
    Contributor

    The test headersWhenContentSecurityPolicyConfiguredTwiceThenSecondCallReplacesFirst in reactive stack still conflicts with composable headers.contentSecurityPolicy(...).

    @Test
    public void headersWhenContentSecurityPolicyConfiguredTwiceThenSecondCallReplacesFirst() {
    String headerName = ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY;
    String policyDirectives = "default-src 'self'";
    // @formatter:off
    this.http.headers((headers) -> headers
    .contentSecurityPolicy((csp) -> csp
    .exchangeMatchers("/foo/**")
    .policyDirectives("script-src 'self'"))
    .contentSecurityPolicy((csp) -> csp
    .policyDirectives(policyDirectives)));
    // @formatter:on
    WebTestClient client = WebTestClientBuilder.bindToWebFilters(this.http.build()).build();
    // @formatter:off
    client.get()
    .uri("https://example.com/bar")
    .exchange()
    .expectHeader().valueEquals(headerName, policyDirectives);
    // @formatter:on
    }

    It is assuming that the second HeaderSpec#contentSecurityPolicy(Customizer) call resets the exchangeMatcher to ServerWebExchangeMatchers.anyExchange(), so that the response to /bar contains CSP default-src 'self' instead of empty CSP.

    This is also why #19864 doesn't pass the CI pipeline.


    Related context: #18499 (comment)

  6. ziqin commented on Oct 17, 2026

    @ziqin
    Contributor

    I've opened #19924 to update headersWhenContentSecurityPolicyConfiguredTwiceThenSecondCallReplacesFirst so that it could test CSP replacement without conflicting with the composition feature.

  7. added a commit that references this issue on Oct 17, 2026
    7723678
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    in: configAn issue in spring-security-configtype: enhancementA general enhancement

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions