Skip to content

Scope public API endpoints to caller visibility (GHSA-6ghm-wf22-pvx5)#367

Draft
jbrooksuk wants to merge 3 commits into
mainfrom
security/api-visibility-scoping
Draft

Scope public API endpoints to caller visibility (GHSA-6ghm-wf22-pvx5)#367
jbrooksuk wants to merge 3 commits into
mainfrom
security/api-visibility-scoping

Conversation

@jbrooksuk

Copy link
Copy Markdown
Member

Summary

The public JSON API index/show endpoints never applied the HasVisibility scopes that the web status page and RSS feed use. Any unauthenticated caller could list and read incidents, metrics and component groups marked authenticated-only or hidden. Fixes GHSA-6ghm-wf22-pvx5 (CWE-862, CVSS 7.5).

What changed

  • Incident / Metric / ComponentGroup index + show: apply ->visible(auth()->check()); show now returns 404 for out-of-scope records instead of leaking them.
  • Component index + show: components have no visibility column — they inherit it from their group. Scoped by group visibility (whereNull('component_group_id') OR group is visible), so ungrouped components stay public, matching the status page. Also hides disabled components by default with an opt-in filter[enabled]=false; show 404s disabled components.
  • Nested controllers (IncidentUpdate, MetricPoint): gated on the parent's visibility so children of hidden/auth-only parents 404.
  • ComponentGroupFactory: fixed a latent bug where visible was set from ComponentGroupVisibilityEnum::expanded (value 0), which under the ResourceVisibilityEnum cast means authenticated. Default is now guest.
  • Tests: added API visibility regression coverage (none existed) across every affected endpoint — guest sees only guest; authenticated sees authenticated+guest; hidden never; ungrouped components stay public; disabled hidden-by-default but reachable via filter; nested children respect parent visibility.

Not affected

Schedule / ScheduleUpdateController have no visibility concept and are unchanged. The advisory's inclusion of components/schedules in the "identical pattern" list is partially inaccurate: Schedule has no visible column, and Component's leak is via its group rather than a column of its own.

Verification

  • Full suite green (476 tests), PHPStan clean, Pint clean.

Warning

Draft: this is a public PR for an unpatched advisory, so the diff discloses the vulnerability. Coordinate with the release/advisory before marking ready.

jbrooksuk and others added 2 commits July 8, 2026 10:20
The public JSON API `index`/`show` endpoints never applied the
`HasVisibility` scopes that the web UI and RSS feed use, so anonymous
callers could read incidents, metrics and component groups marked
`authenticated`-only or `hidden`.

- Scope Incident, Metric and ComponentGroup index/show to the caller's
  visibility, returning 404 for out-of-scope records on show.
- Scope Component index/show by the visibility of its group (components
  inherit visibility from their group; ungrouped stay public, matching
  the status page). Hide disabled components by default with an opt-in
  `enabled` filter; show 404s disabled components.
- Gate the nested IncidentUpdate and MetricPoint controllers on their
  parent's visibility.
- Fix ComponentGroupFactory using the wrong enum for `visible`, which
  defaulted groups to `authenticated` instead of `guest`.
- Add API visibility regression tests across all affected endpoints.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…includes

The visibility scoping resolved callers via the default guard, which never
sees bearer tokens on the unauthenticated read routes, so authenticated
API consumers were served guest-scoped data. Includes could also bypass
the scoping: components?include=incidents revealed hidden incidents, and
incidents/schedules?include=components.group revealed hidden groups.
Disabled components are now consistently hidden from guests and readable
by authenticated callers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jbrooksuk jbrooksuk force-pushed the security/api-visibility-scoping branch from 4efc0e8 to 3b49c3f Compare July 8, 2026 09:44
@jbrooksuk

Copy link
Copy Markdown
Member Author

Rebased onto main (clean, no conflicts) and re-reviewed the full advisory surface. The re-review found three gaps, fixed in the follow-up commit:

1. Bearer-token callers were treated as guests (critical). The controllers resolved the caller via auth()->check(), i.e. the default web guard — but the public index/show routes carry no auth middleware, so a real API consumer sending a Sanctum bearer token was always guest-scoped and could never see authenticated-visibility resources. The existing tests passed only because Sanctum::actingAs() swaps the default guard. All controllers now resolve the caller through auth('sanctum')->check() (new ChecksApiAuthentication concern), and each resource has a regression test using a real bearer token.

2. Relationship includes bypassed the scoping. components?include=incidents returned hidden/authenticated incidents on visible components, and incidents|schedules?include=components.group revealed hidden component groups. Includes are now constrained per caller via AllowedInclude::callback().

3. Disabled components were inconsistently guarded. show hard-404'd disabled components even for authenticated callers (breaking authed reads), while guests could still list them via filter[enabled]=false. Now: guests never see disabled components (matching the status page component list); authenticated callers can fetch them via show and list them via the filter. ⚠️ Behavior change: the pre-existing "guests can filter disabled components" test was updated to require auth — shout if you'd rather keep that public.

Verified as already covered / not applicable: schedules have no visible column (advisory mention is moot except the group include, fixed above); subscribers are auth-only; the user include only exposes the user ID; RSS already uses guests(); metric points/incident updates inherit their parent checks; the factory visible enum fix is correct (expanded->value = authenticated).

Flagged for a separate decision: incident-templates index/show are publicly readable with no visibility concept — template names/bodies (draft comms) are exposed to anonymous callers. Worth considering moving those reads behind auth:sanctum in a follow-up.

Full suite: 663 passed. PHPStan and Pint clean.

🤖 Generated with Claude Code

…lisions

The include-leak tests matched included resources by name, which fails
when faker generates a component name equal to the hidden group's name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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