Skip to content

[false-negative]: Actuator CORS misconfiguration is never checked #41

Description

@vianbas

Related: #39 (SPR-CONFIG-004 targets a namespace that does not exist; the two are best
fixed together).

Config that is missed

management:
  endpoints:
    web:
      cors:
        allowed-origins: "*"
        allow-credentials: true
$ sprig scan ./realprops
Checked 0 Java file(s), 1 config file(s) in 28 ms. Found 0 finding(s).
$ echo $?
0

Reproduced identically across two runs.

The namespace is real, unlike the one SPR-CONFIG-004 targets (#39)

Property counts from META-INF/spring-configuration-metadata.json in
spring-boot-actuator-autoconfigure:

Boot management.endpoints.web.cors.* legacy endpoints.cors.*
2.0.9 6 6
2.1.18 6 6
2.2.13 6 6
2.3.12 6 0
2.4.13 6 0
2.5.15 7 0
2.6.15 7 0
2.7.18 7 0
3.0.13 7 0
3.2.12 7 0
3.5.16 7 0

Present in every Boot 2.x and 3.x release checked. The 7th property is
allowed-origin-patterns, added in 2.5. (spring-boot-actuator-autoconfigure does not
exist as a separate artifact before 2.0, so 1.5 is not comparable.)

spring.graphql.cors.* is the second real surface, with the same 7 properties. Verified
cutoff: 0 in 2.5.15, 0 in 2.6.15, 7 from 2.7.18 onward.

Note for whoever implements this: the legacy endpoints.cors.* prefix exists through 2.2
and is gone from 2.3. Probably not worth supporting, but worth a deliberate decision rather
than an accidental omission.

Why it is exploitable

Two distinct cases, both verified against a running app whose only CORS configuration
comes from these properties (no @CrossOrigin anywhere in the source).

Case 1: wildcard with credentials, Spring Framework below 5.3

Boot 2.3.12 (ships spring-web 5.2.15), attacker origin against an exposed health endpoint:

$ curl -i -H "Origin: https://evil.example" http://localhost:8088/actuator/health
HTTP/1.1 200
Access-Control-Allow-Origin: https://evil.example
Access-Control-Allow-Credentials: true

{"status":"UP"}

The origin is reflected and credentials are permitted, so any site can read Actuator
responses with the victim's session attached.

Case 2: wildcard without credentials, current Spring

From Framework 5.3 onward validateAllowCredentials() rejects wildcard-plus-credentials at
startup, so that combination cannot survive in a running Boot 3 app; it is loud and
self-announcing. A wildcard without credentials, however, starts cleanly and still leaks:

$ curl -i -H "Origin: https://evil.example" http://localhost:8087/actuator/health
HTTP/1.1 200
Access-Control-Allow-Origin: *

{"status":"UP"}

Boot 3.5.16 (ships spring-web 6.2.19). Any origin can read Actuator endpoint bodies
cross-origin. With a broader
management.endpoints.web.exposure.include, that reaches /env, /configprops,
/beans, /mappings and similar: the same exposure surface SPR-CONFIG-001 already treats
as sensitive, reached through a different door. In my judgement this is the case worth
prioritising, since it is the only one of the two that can describe a running Boot 3 app.

Negative control

Same app and Boot version, wildcard replaced by a concrete allowlist plus credentials. The
attacker origin is rejected outright, on both the preflight and the plain GET:

$ curl -i -X OPTIONS -H "Origin: https://evil.example" \
       -H "Access-Control-Request-Method: GET" http://localhost:8094/actuator/health
HTTP/1.1 403

$ curl -i -H "Origin: https://evil.example" http://localhost:8094/actuator/health
HTTP/1.1 403

while the allowed origin is served normally:

$ curl -i -X OPTIONS -H "Origin: https://app.example.com" \
       -H "Access-Control-Request-Method: GET" http://localhost:8094/actuator/health
HTTP/1.1 200
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1800

So the leaks above are caused by the wildcard, not by an artifact of the test setup.

Suggested detection

Condition Severity
allowed-origins contains * and allow-credentials: true HIGH
allowed-origins contains * and credentials unset or false MEDIUM

Across management.endpoints.web.cors.* and spring.graphql.cors.*.

allowed-origin-patterns needs a deliberate decision. Note that SPR-CORS-001 is not a
straight precedent here: it lets originPatterns suppress only the implicit wildcard
(origins unset), while an explicit origins = "*" still fires even when originPatterns
is present (CorsWildcardCredentialsRule.java:71-76).

The MEDIUM tier could reasonably be argued upward when
management.endpoints.web.exposure.include is itself broad, since the value of what leaks
rises sharply. Worth deciding before implementation rather than after.

References

  • Spring Boot Actuator: management.endpoints.web.cors.*, documented under "CORS support"
    in the Actuator reference.
  • CorsConfiguration.validateAllowCredentials(), Spring Framework 5.3+, rejects
    allowedOrigins = ["*"] combined with allowCredentials = true.
  • CWE-942: Permissive Cross-domain Policy with Untrusted Domains.

Environment

  • sprig version: sprig 0.1.0
  • Java version: openjdk version "17.0.20" 2026-07-21 (Homebrew, build 17.0.20+0)
  • Probes: Boot 2.3.12.RELEASE and Boot 3.5.16, spring-boot-starter-web +
    spring-boot-starter-actuator, no CORS annotations in source.

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 caughtnew-ruleRequest for a brand-new detection rule

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions