Skip to content

aggregator(alerts): GET /api/push/subscriptions returns every subscription's p256dh + auth secret — unauthenticated, unused by any client, and the /alerts page for the same data redacts them #3693

Closed

Description

@dezren39

Problem

GET /api/push/subscriptions returns every push subscription's full database row — including the Web Push auth secret and p256dh key — to any caller who can reach the aggregator, with no authentication. No in-repo client uses this endpoint.

Evidence (verified this run, main 0bb00794)

  • pm-stack/aggregator/src/server.ts:976 — the GET handler is return json({ subscriptions: await store.listSubscriptions() }). Unlike the incident intake (POST /api/alerts, gated by alertsIntakeAuthorized, server.ts:1000-1001), this route has no auth check.
  • pm-stack/aggregator/src/alerts.ts:265-267 — listSubscriptions() is select * from alerts.push_subscriptions, so the wire payload carries endpoint, p256dh, and auth verbatim for every subscriber.
  • The auth value is the RFC 8291 push-encryption secret for that subscription; p256dh is its encryption public key. Together with the endpoint they are the complete per-subscription key material the server itself uses in deliverToAll (alerts.ts:447-448).
  • Contrast — the HTML surface for the same data deliberately does not expose the keys: subscriptionRow (render.ts:5550-5560) renders only the endpoint host, classes, quiet window, and user agent.
  • No consumer needs the GET: repo-wide grep shows push-client.js only POSTs/DELETEs /api/push/subscriptions, and the inline /alerts script only DELETEs. The engine reads subscriptions internally via the store, never over HTTP.

Probe (laptop, ~/git/fleet/probe-2026-11-01-laneA-subs-leak/probe.ts, blobs sha-matched to origin/main)

AlertsStore backed by a fake DB returning one sentinel row, serialized exactly as the GET handler serializes it:

GET wire contains p256dh sentinel: true
GET wire contains auth sentinel:    true
GET wire contains endpoint sentinel: true
HTML /alerts contains p256dh sentinel: false
HTML /alerts contains auth sentinel:    false
HTML /alerts contains endpoint host:    true

Why it matters

The alert throat (#3676) exists so critical incidents reliably reach Drew's devices. This endpoint hands every subscriber's push key material to any tailnet-reachable caller (the aggregator binds loopback + tailnet and is fronted by Tailscale Serve at cfg.publicOrigin), and enumerates the exact endpoints that the likewise-unauthenticated DELETE /api/push/subscriptions (server.ts:986-994) accepts — so a caller can both harvest the secrets and silently unsubscribe every device from critical alerts. The keys serve no client need: the one legitimate read surface (the /alerts page) already projects them away.

Acceptance criteria

  • GET /api/push/subscriptions no longer returns p256dh or auth values for any subscription (project each row to the non-secret fields the page uses — id, endpoint host or endpoint, classes, quiet, user_agent, created_at/last_seen_at, gone_count — or remove the GET route entirely if nothing needs it).
  • A test pins the redaction: a subscription row with sentinel p256dh/auth values serialized through the GET path contains neither sentinel (mirroring the probe above).
  • Internal delivery is unaffected: runAlertTick/deliverToAll keep reading full rows through the store directly, and the existing alerts test suite passes.
  • The /alerts page subscription list renders unchanged.

Activity

  1. developing-today commented on Nov 1, 2026

    @developing-today
    Contributor
  2. added
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    kind/securitySecurity issue or security hardening
    effort/sSmall: expected under 2 hours of work
    on Nov 1, 2026
  3. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-a)
    • Session / ID: new-issue-creator-code-a run 2026-11-01T13:05Z (America/Chicago scheduled run)
    • Found via: filed this run
    • Task: Triage the GET /api/push/subscriptions key leak: confirm the redaction fix scope on current main and route the fix.
    • Expectation: effort/s — done looks like the GET response carrying no p256dh/auth values, pinned by a sentinel test, with internal delivery and the /alerts page unchanged.
    • Links: queue packet packet-3693-triage.json (repo-health-code-pilot); probe ~/git/fleet/probe-2026-11-01-laneA-subs-leak/ (laptop)
  4. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · adopted as Phase 1 of design #3702

  5. 10 remaining items

  6. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Progress update (queue task 6166): implemented a shared toPublicSubscription projection in pm-stack/aggregator/src/alerts.ts and applied it to both GET and POST /api/push/subscriptions; the stored/internal rows used by delivery and /alerts remain unchanged. Added sentinel coverage for GET/POST serialization. bun test test/alerts.test.ts from pm-stack/aggregator passes (32 tests). Full tsc --noEmit reports existing unrelated errors in projects.test.ts and quota-current.test.ts; no diagnostics in the changed files. Committing/pushing now; PR to follow.

  7. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Final update (queue task 6166): change is committed and pushed as 1cdf09588 on queue/6166-3693-push-subscriptions-projection. PR is open and intentionally not merged: #4413. Exact acceptance command bun test pm-stack/aggregator/test/alerts.test.ts passes (32 pass, 0 fail). GET/POST response projections strip both secret values and keys; delivery still consumes full rows from listSubscriptions, and the unchanged /alerts render tests pass as part of the suite. Full typecheck has unrelated errors in projects.test.ts and quota-current.test.ts; no errors reported in changed files.

  8. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Follow-up on PR #4413 (commit 9c3c0d1): toPublicSubscription now constructs the public object explicitly field-by-field (no rest-spread, so a future secret column can't leak through), and the sentinel test asserts the exact serialized key set for both GET and POST shapes. bun test pm-stack/aggregator/test/alerts.test.ts: 32 pass, 0 fail.

  9. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Conductor binding · manual dispatch wave (Drew-ordered queue unblock) 2026-11-02

  10. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 2, 2026
  11. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Agent note — task 6166 picked up by lane code-d (direct execution lane). The conductor-dispatched session's work is already on PR #4413 (branch queue/6166-3693-push-subscriptions-projection, head 9c3c0d1); this lane is reconciling and harvesting it: verifying the diff against the task 6166 scope, dispositioning any Codex findings, and merging under the standing criteria so #3693 closes.

  12. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Agent note — task 6166 done. PR #4413 MERGED (merge commit 10b3415) and #3693 is CLOSED. The diff was verified against the task 6166 scope: toPublicSubscription projection in alerts.ts (id, endpoint, classes, quiet, user_agent, created_at, last_seen_at, gone_count — p256dh/auth omitted), GET and POST responses in server.ts both projected, and a sentinel regression test in alerts.test.ts covering both response shapes. Codex review completed clean at head 9c3c0d1 (no findings); no GitHub Actions runs registered for the PR (runner backlog), with the session's laptop bun test test/alerts.test.ts at 32 passed recorded in the PR body.

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

Metadata

Metadata

Assignees

Labels

app/aggregatorPackage/app: aggregator (pm-stack in this repo)app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/sSmall: expected under 2 hours of workkind/securitySecurity issue or security hardeningpriority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions