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:
- 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.
- 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
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:reactive/src/main/java/demo/SecurityConfig.java:Both trees also hold an identical
@PreAuthorize-annotated service and no@EnableMethodSecurity.Reproduced identically across two runs.
Why each rule misses
methodsReturning("SecurityFilterChain"),hasCallOn("permitAll", "anyRequest")SecurityWebFilterChain,anyExchange()@EnableWebSecurityFQN@EnableWebFluxSecuritymethodsReturning("SecurityFilterChain")SecurityWebFilterChainSecurityWebFilterChaindoes not containSecurityFilterChainas a substring, so no amount ofloose matching in
methodsReturningwould 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 aNoOpPasswordEncoderbean to the same reactive tree:
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 asanyRequest().permitAll()does, andframeOptions().disable()removes the clickjacking headeron 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:
@EnableWebFluxSecurityatapplication/src/main/java/run/halo/app/infra/config/WebServerSecurityConfig.java:41, andspec.anyExchange().permitAll()atapplication/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 earlierauthorization 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
methodsReturningshould acceptSecurityWebFilterChainalongsideSecurityFilterChain.anyExchangealongsideanyRequest, andauthorizeExchangealongsideauthorizeHttpRequestsand
authorizeRequests.@EnableWebFluxSecurityalongside@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:
most of the SOURCE rules did not apply. Even after this fix some rule will be stack-specific.
Having
--verbosereport which rules were skipped and why would give the exit code a definedmeaning.
report and its two companions are all false negatives, and the README currently reads as a
completeness promise it does not make.
Environment
sprig 0.1.0, at80fd14eopenjdk version "17.0.20" 2026-07-21(Homebrew, build 17.0.20+0)a559d15, 1349 Java files, which builds on Spring Boot 4.1.0