Skip to content

[false-negative]: WebFlux security configuration is invisible to SPR-SRC-003, SPR-SRC-004 and SPR-SRC-005 #43

Description

@vianbas

Related: found in the same smoke test as #45, scanning
halo-dev/halo at a559d15. Independent of it otherwise.

Config that is missed

Two security configurations granting the same access, one servlet and one reactive.

servlet/src/main/java/demo/SecurityConfig.java:

@EnableWebSecurity
public class SecurityConfig {
    @Bean
    SecurityFilterChain chain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(spec -> spec.anyRequest().permitAll());
        http.headers(h -> h.frameOptions(f -> f.disable()));
        return http.build();
    }
}

reactive/src/main/java/demo/SecurityConfig.java:

@EnableWebFluxSecurity
public class SecurityConfig {
    @Bean
    SecurityWebFilterChain chain(ServerHttpSecurity http) {
        http.authorizeExchange(spec -> spec.anyExchange().permitAll());
        http.headers(h -> h.frameOptions(f -> f.disable()));
        return http.build();
    }
}

Both trees also hold an identical @PreAuthorize-annotated service and no
@EnableMethodSecurity.

$ sprig scan servlet
SPR-SRC-004 [MEDIUM] src/main/java/demo/SecurityConfig.java:8   @EnableWebSecurity without @EnableMethodSecurity: ...
SPR-SRC-003 [HIGH]   src/main/java/demo/SecurityConfig.java:10  SecurityFilterChain permits every request via .anyRequest().permitAll() ...
SPR-SRC-005 [MEDIUM] src/main/java/demo/SecurityConfig.java:10  headers().frameOptions() is disabled ...
Found 3 finding(s), 2 medium, 1 high.
$ echo $?
1

$ sprig scan reactive
Found 0 finding(s).
$ echo $?
0

Reproduced identically across two runs.

Why each rule misses

Rule Keys on Reactive equivalent
SPR-SRC-003 methodsReturning("SecurityFilterChain"), hasCallOn("permitAll", "anyRequest") SecurityWebFilterChain, anyExchange()
SPR-SRC-004 the @EnableWebSecurity FQN @EnableWebFluxSecurity
SPR-SRC-005 methodsReturning("SecurityFilterChain") SecurityWebFilterChain

SecurityWebFilterChain does not contain SecurityFilterChain as a substring, so no amount of
loose matching in methodsReturning would have caught it by accident.

Negative control

The blindness is specific to those three rules, not to reactive code in general. Adding a
@CrossOrigin(origins = "*", allowCredentials = "true") controller and a NoOpPasswordEncoder
bean to the same reactive tree:

$ sprig scan reactive
SPR-CORS-001 [HIGH] src/main/java/demo/Api.java:9             Cross-origin configured with origins=* and allowCredentials=true.
SPR-SRC-002  [HIGH] src/main/java/demo/PasswordConfig.java:10 Insecure password handling: NoOpPasswordEncoder or {noop} plaintext password used.

Both fire, because neither depends on the servlet API. Three of five SOURCE rules are affected,
two are not.

Why it is exploitable

anyExchange().permitAll() removes authorization from every route exactly as
anyRequest().permitAll() does, and frameOptions().disable() removes the clickjacking header
on both stacks. A reactive project gets a clean scan and exit 0 for configuration sprig calls
HIGH on the servlet stack. In CI, exit 0 is read as a verdict, which makes this worse than not
scanning at all.

halo is fully reactive: @EnableWebFluxSecurity at
application/src/main/java/run/halo/app/infra/config/WebServerSecurityConfig.java:41, and
spec.anyExchange().permitAll() at
application/src/main/java/run/halo/app/security/authorization/AuthorizationExchangeConfigurers.java:98.
sprig reported no SOURCE findings at all across 1349 Java files.

In halo's case that particular bean is an @Order-last fallback sitting behind earlier
authorization configurers, so it is plausibly deliberate rather than a vulnerability. That is the
point: sprig never had an opinion that could be right or wrong.

Suggested detection

  • methodsReturning should accept SecurityWebFilterChain alongside SecurityFilterChain.
  • anyExchange alongside anyRequest, and authorizeExchange alongside authorizeHttpRequests
    and authorizeRequests.
  • @EnableWebFluxSecurity alongside @EnableWebSecurity.

Each needs a reactive fixture and a safe reactive counter-fixture, per CONTRIBUTING. Finding
messages should name the stack so the output stays unambiguous.

Two decisions worth making while this is open, either here or as follow-ups:

  1. A silent 0 is misleading. A reactive project gets exit 0 today with no indication that
    most of the SOURCE rules did not apply. Even after this fix some rule will be stack-specific.
    Having --verbose report which rules were skipped and why would give the exit code a defined
    meaning.
  2. README wording. "Zero false-positive noise" speaks only to the false-positive side. This
    report and its two companions are all false negatives, and the README currently reads as a
    completeness promise it does not make.

Environment

  • sprig version: sprig 0.1.0, at 80fd14e
  • Java version: openjdk version "17.0.20" 2026-07-21 (Homebrew, build 17.0.20+0)
  • Probes: two source-only trees, servlet and reactive, no build or classpath required
  • Scan target: halo at a559d15, 1349 Java files, which builds on Spring Boot 4.1.0

Activity

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

    false-negativeA real misconfiguration wasn't caught

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions