Skip to content

feat: report Actuator endpoints that exposure and access together reach - #47

Open
vianbas wants to merge 1 commit into
fix/metadata-absence-means-unconfirmedfrom
fix/actuator-access-gate
Open

feat: report Actuator endpoints that exposure and access together reach#47
vianbas wants to merge 1 commit into
fix/metadata-absence-means-unconfirmedfrom
fix/actuator-access-gate

Conversation

@vianbas

@vianbas vianbas commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Stacked on #46. Base is fix/metadata-absence-means-unconfirmed, because
SPR-CONFIG-006 is keyed on properties absent from Spring's metadata and the
ACCEPTED_ABSENCES entries it adds have to meet the evidence bar #46 writes
down. GitHub will retarget this to main when #46 merges.

What

Adds SPR-CONFIG-006 (CRITICAL): an Actuator endpoint that Boot disables by
default is both exposed over HTTP and switched back on. Removes the shutdown
token from SPR-CONFIG-001, which could never reach anything. Splits
SPR-CONFIG-001's heapdump claim by Boot version.

Before After
shutdown reachable via access/enabled not detected SPR-CONFIG-006, CRITICAL, pointing at the property that opens it
heapdump reachable via access/enabled not detected same
shutdown in exposure.include SPR-CONFIG-001, HIGH not flagged; it reaches nothing
SPR-CONFIG-001 heapdump claim "leaks process memory" true through 3.3, held at 404 from 3.4, both stated
README "Zero false-positive noise" "False positives are treated as bugs", naming the one just removed

Why

Closes #45

sprig read management.endpoints.web.exposure.include and stopped there. That
is half a gate. Neither half reaches a disabled-by-default endpoint alone.

Every cell below is an observed HTTP status against a running app
(spring-boot-starter-web + spring-boot-starter-actuator), with
exposure.include: "*". Each cell is heapdump / shutdown.

Configuration 2.3.12 3.3.13 3.5.16
exposure only 200 / 404 200 / 404 404 / 404
endpoint.<id>.enabled: true 200 / 200 200 / 200 200 / 200
endpoints.enabled-by-default: true 200 / 200 200 / 200 200 / 200
endpoint.<id>.access: unrestricted 200 / 404 200 / 404 200 / 200
endpoint.<id>.access: read-only 200 / 404 200 / 404 200 / 404
endpoints.access.default: unrestricted 200 / 404 200 / 404 200 / 200
endpoints.access.max-permitted 200 / 404 200 / 404 200 / 404

Three places the measurements corrected #45

access without exposure reaches nothing. #45 states that
exposure.include: health,info plus heapdump.access: unrestricted serves the
heap dump. It does not: 404, reproduced twice, with the control at 200 once the
id is added to the include list. The two gates are AND. A rule keyed on the
access property alone would have been a false positive of the kind #42 removed.

read-only is enough to leak. heapdump is a GET, so read-only serves
it. Only shutdown, a POST, needs unrestricted. Keying on unrestricted
alone, as #45 suggests, would have missed the disclosure.

env, configprops, beans, mappings, threaddump are out of scope.
All five answer 200 from exposure alone on every version, so an access
property on them grants nothing new. #45 proposed covering them; they are
SPR-CONFIG-001's finding.

max-permitted is honoured, and caps writes only: read-only takes shutdown
back to 404 while heapdump keeps serving a real HPROF. The rule's one
knowing miss is documented rather than hidden: the cap does nothing before 3.4,
so a pre-3.4 project setting both shutdown.enabled: true and a cap stays
reachable while the rule is quiet. Reading the cap as inert instead would put a
CRITICAL finding on every correctly capped 3.4+ project.

How was this verified?

Probes. Every row above is a separate JVM start against a real app, each
configuration run twice with identical results. HPROF responses were confirmed
by the JAVA PROFILE 1.0.2 header rather than by byte count, and each shutdown
200 was confirmed by the process exiting.

Tests. 59 to 66. mvn -B -ntp clean verify run twice, BUILD SUCCESS both
times, spotless clean across 75 files.

Tests run: 66, Failures: 0, Errors: 0, Skipped: 0
Spotless.Java is keeping 75 files clean - 0 needs changes to be clean

Five new fixtures, one per measured fact: both gates open, access without
exposure, the max-permitted cap, the blanket default with a per-endpoint
override, and shutdown alone in the exposure list. Each fixture carries the
version and status codes it encodes as a comment, so a behaviour change is a
change to both.

Against the config that started the report. halo's actuator block,
reconstructed:

SPR-CONFIG-002 [MEDIUM]   application.yaml:4   Hardcoded secret: spring.r2dbc.password.
SPR-CONFIG-001 [HIGH]     application.yaml:9   Actuator endpoint(s) exposed: *.
SPR-CONFIG-006 [CRITICAL] application.yaml:12  Actuator 'shutdown' is exposed over HTTP and switched on by management.endpoint.shutdown.access=unrestricted. POST /actuator/shutdown stops the application.
SPR-CONFIG-006 [CRITICAL] application.yaml:14  Actuator 'heapdump' is exposed over HTTP and switched on by management.endpoint.heapdump.access=unrestricted. GET /actuator/heapdump returns a dump of process memory, including credentials and session tokens.

The two lines #45 reports as invisible are the two new findings.

Dogfood scan (the command CI runs) reports the same single pre-existing
NoOpPasswordEncoderRule.java:91 self-match, exit 1. No new self-findings.

Goldens regenerated with -DupdateGoldens=true and reviewed as a diff: one
new rules-table entry at security-severity 9.0, SPR-CONFIG-001's description
losing shutdown, and the rule index shift. No finding in demo-app changes.

Notes for review

  • This is sprig's first CRITICAL rule. The tier was already wired through
    SARIF (error, 9.0), the console renderer and --fail-on, and nothing used
    it. A project running --fail-on CRITICAL in CI will start failing on this,
    which is the intent.
  • management.endpoint.<id>.access and .enabled are bound reflectively per
    endpoint id and appear in no metadata release, so they go in
    ACCEPTED_ABSENCES with a measured observation each. The blanket properties
    (access.default, access.max-permitted, enabled-by-default) are declared
    and need no entry.
  • Origins and gates are paired within one file, matching SPR-CONFIG-004.
    Base and profile files are never merged.
  • SPR-CONFIG-001 stays HIGH. env and its neighbours answer 200 on every
    version measured, and /actuator/env alone hands over credentials. The
    version-dependent claim was about heapdump, and that is what the doc now
    splits.

Checklist

  • Linked issue exists and is referenced above
  • Tests added or updated for the behavior change
  • mvn verify passes locally
  • Docs updated (README.md, docs/rules/*.md) if user-facing behavior changed
  • For a new/changed rule: false-positive rationale documented in docs/rules/

sprig read management.endpoints.web.exposure.include and stopped there, which is
half a gate. Measured against running apps on Boot 2.3.12, 3.3.13 and 3.5.16,
neither half reaches a disabled-by-default endpoint on its own: under
include: "*" alone, POST /actuator/shutdown answers 404 on all three, and under
include: health,info with shutdown.access=unrestricted it answers 404 as well.
Name the endpoint in the exposure list and open the gate, and the same request
answers 200 and the process exits.

SPR-CONFIG-006 reports that pair, at CRITICAL. It reads both spellings Boot
honours, per-endpoint and blanket, because sprig does not read pom.xml and has
no idea which Boot version it is looking at. access arrived in 3.4 and is inert
before it; enabled was honoured on all three versions measured.

Three measurements shaped the rule rather than the report in #45:

- read-only is enough for heapdump, a GET. Only shutdown, a POST, needs
  unrestricted. Keying on unrestricted alone would have missed the disclosure.
- access without exposure reaches nothing. #45 says otherwise. It answers 404,
  reproduced twice, with the named-exposure control at 200. Firing on the access
  property alone would have shipped a false positive of the kind #42 removed.
- env, configprops, beans, mappings and threaddump answer 200 from exposure
  alone on every version, so an access property on them grants no reachability
  the exposure list has not. #45 proposed covering them here; they belong to
  SPR-CONFIG-001.

SPR-CONFIG-001 loses the shutdown token in the same change. Exposure never
reached that endpoint on any version measured, so it was a HIGH finding with
nothing behind it. Its doc now splits the heapdump claim by version: served by
exposure alone through 3.3, held at 404 from 3.4 until something opens the gate.

The README claim of zero false-positive noise goes with it. A rule firing on an
unreachable endpoint was noise, and what the project actually does is treat
false positives as bugs.

Suite 59 to 66. Golden regenerated: one new rules-table entry at
security-severity 9.0, one description change, and the rule index shift.

Closes #45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant