Skip to content

fix(core/api): gate PII reads + enumeration endpoints#83

Merged
BK1031 merged 1 commit into
mainfrom
bk1031/harden-pii-reads
Jun 17, 2026
Merged

fix(core/api): gate PII reads + enumeration endpoints#83
BK1031 merged 1 commit into
mainfrom
bk1031/harden-pii-reads

Conversation

@BK1031

@BK1031 BK1031 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Stacked on top of #82 — base is `bk1031/harden-user-entity-gates`. Final in the harden-core-api-gates stack.

Audit holes covered

These don't let an attacker change state, but they leak PII and authorization signals to unauthenticated callers.

MEDIUM:

  • `GET /core/entity/:entityID` (GetEntityByID) — PII leak (email, phone, external auths)
  • `GET /core/entity/:entityID/logins` (GetEntityLogins) — audit log leak
  • `GET /core/entity/external/:provider` (ListExternalAuthsByProvider) — enumeration
  • `GET /core/entity/external/:provider/:externalID` (GetEntityByExternalAuth) — discord-id → entity reverse lookup
  • `GET /users/:id` (GetUserByID) — full user profile read
  • `GET /applications/client/:clientID` (GetApplicationByClientID) — owner_id leak

LOW:

  • `GET /core/entity/:entityID/groups`, `/memberships` — group membership is an authorization signal
  • `GET /users/:id/groups` — same
  • `GET /core/applications/client/:clientID/groups` (GetApplicationGroupsByClientID) — internal-only
  • `POST /core/saml/sp/resolve` (ResolveSAMLServiceProvider) — internal-only

Gate model

Self / admin / internal:
```go
Require(c, Any(
RequestTokenHasScope(c, "sentinel:all"),
RequestTokenHasEntityID(c, entityID), // or RequestTokenHasUserID for /users/*
RequestUserIsAdmin(c),
))
```

Internal-only (sentinel:all): the discord-id reverse lookup, external-auth enumeration, internal route variants. The respective non-core services now carry sentinel:all via PR #79.

After this lands

Every endpoint flagged in the post-s2s-auth audit has a gate. `CheckUsername` is left open (intentional — signup form needs it) and `POST /core/applications/verify` stays open (its own constant-time client_id/secret check is the auth). Everything else either requires a bearer or rejects.

Test plan

  • Unauthed GET to /core/entity/:id → 401
  • User A reading own /core/entity/:A → 200
  • User A reading /core/entity/:B → 401
  • Discord-sync's full sweep (calls GetEntityByExternalAuth) still works (carries sentinel:all)
  • OAuth's BuildTokenClaims (calls GetApplicationGroupsByClientID) still works
  • SAML SSO flow (calls ResolveSAMLServiceProvider) still works

Closes the read-side audit findings. Most of these don't let an
attacker change state, but they leak PII (emails, phone numbers,
external identity pairings, login history) and authorization signals
(group memberships, owner_ids) to unauthenticated callers.

Self / admin / internal-only reads (Any of: sentinel:all, bearer's
entity/user matches the target, RequestUserIsAdmin):
  - GET /core/entity/:entityID (GetEntityByID)
  - GET /core/entity/:entityID/groups (GetEntityGroups)
  - GET /core/entity/:entityID/memberships (GetEntityMemberships)
  - GET /core/entity/:entityID/logins (GetEntityLogins)
  - GET /users/:id (GetUserByID)
  - GET /users/:id/groups (GetUserGroups)

Admin / internal-only reads:
  - GET /applications/client/:clientID (GetApplicationByClientID) —
    leaks owner_id + metadata

Internal-only reads (sentinel:all):
  - GET /core/entity/external/:provider (ListExternalAuthsByProvider)
  - GET /core/entity/external/:provider/:externalID
    (GetEntityByExternalAuth) — discord-id → entity reverse lookup
  - GET /core/applications/client/:clientID/groups (was unauth — used
    by oauth's BuildTokenClaims, which now carries sentinel:all)
  - POST /core/saml/sp/resolve (ResolveSAMLServiceProvider) — used by
    the saml service, which carries sentinel:all

Internal services (oauth/discord/saml) carry sentinel:all on their
SA bearers, so they keep working through the new gates. External
callers attempting to enumerate or harvest PII now get 401/403.

Last in the stack of "harden core API gates" PRs. After this lands,
core has gates on every endpoint that was previously ungated from
the post-s2s-auth audit.
Base automatically changed from bk1031/harden-user-entity-gates to main June 17, 2026 08:30
@BK1031 BK1031 merged commit 303ec27 into main Jun 17, 2026
15 checks passed
@BK1031 BK1031 deleted the bk1031/harden-pii-reads branch June 17, 2026 08:30
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