Skip to content

sec(api): strip /healthz filename + drop env-var names from public 401s (API-21/90/217)#179

Merged
mastermanas805 merged 2 commits into
masterfrom
fix/brevo-agent-action-and-healthz-migration-leak
May 29, 2026
Merged

sec(api): strip /healthz filename + drop env-var names from public 401s (API-21/90/217)#179
mastermanas805 merged 2 commits into
masterfrom
fix/brevo-agent-action-and-healthz-migration-leak

Conversation

@mastermanas805
Copy link
Copy Markdown
Member

Summary

Two recon-leak fixes on public anon-reachable surfaces.

BUG-API-090 / BUG-API-217 — /healthz migration_version filename leak

Before:

$ curl https://api.instanode.dev/healthz | jq .migration_version
"063_forwarder_sent_audit_link.sql"

That literally tells an attacker which feature shipped at which migration number. An anon-reachable probe must not provide that recon. Add migrations.State.PublicVersion() that strips to the numeric prefix only ("063"). Canaries keep the commit_id + migration_count + migration_version drift tuple; attackers learn nothing about the schema.

BUG-API-021 (+ siblings) — webhook 401 envelope literally named env vars

Before:
```json
"agent_action": "...Operators must verify the Brevo dashboard webhook URL matches the configured BREVO_WEBHOOK_SECRET..."
```
The public 401 returned by `/webhooks/brevo/` — reachable by any brute-forcer — named the exact env-var key. Soften 3 agent_action sentences (`brevo_secret_mismatch`, `webhook_secret_mismatch`, `webhook_signature_mismatch`). Point at the docs page where operator-side rotation lives.

What did NOT change

  • Wire shape: error codes, statuses, messages all preserved
  • No new endpoints, no new fields, no auth changes
  • migration_count + migration_status unchanged

Surface checklist (rule 22)

  • ✅ `api/internal/migrations/state.go` — `PublicVersion()` helper
  • ✅ `api/internal/router/router.go` — `/healthz` emits `PublicVersion()`
  • ✅ `api/internal/router/healthz_test.go` — pinned-shape test updated + new regression
  • ✅ `api/internal/handlers/helpers.go` — 3 agent_action sentences softened
  • ✅ `api/internal/handlers/brevo_webhook_test.go` — assertion BREVO_* env names never reach wire
  • ✅ OpenAPI / dashboard / marketing — no surface change (envelope shape unchanged, schema lines update text only)

Coverage block

```
Symptom: /healthz.migration_version "063_forwarder_sent_audit_link.sql";
brevo 401 agent_action names BREVO_WEBHOOK_SECRET
Enumeration: rg -F 'BREVO_WEBHOOK_SECRET' internal/handlers/helpers.go
rg -F 'mstate.Filename' internal/router/
Sites found: 3 agent_action strings + 1 router emit
Sites touched: 4 / 4
Coverage test: TestHealthzMigrationVersionStripsFilenameSuffix (asserts no '' or '.sql'
in output across 7 cases — would fail if a future refactor reverts to
emitting filename); brevo_webhook_test asserts no BREVO
* in body.
Live verified: pre-merge above (curl evidence shown)
```

Local gate

  • `go build ./...` ✅
  • `go vet ./...` ✅
  • `go test ./internal/migrations/` ✅
  • `go test ./internal/router/` ✅
  • `go test -run 'Brevo' ./internal/handlers/` ✅

Live verify plan (post-merge)

```bash
curl https://api.instanode.dev/healthz | jq .commit_id # must match merge SHA
curl https://api.instanode.dev/healthz | jq .migration_version # must be "063"
curl https://api.instanode.dev/webhooks/brevo/x -X POST | jq .agent_action # must NOT contain BREVO_
```

🤖 Generated with Claude Code

claude added 2 commits May 29, 2026 23:27
…1s (API-21/90/217)

BUG-API-090 + BUG-API-217 — /healthz is anon-reachable and emits the
raw migration filename, e.g. "063_forwarder_sent_audit_link.sql". That
literally tells an attacker which feature shipped at which migration
number — recon a public probe should not provide. Add
migrations.State.PublicVersion() that strips to the numeric prefix
("063"). Canaries keep the commit_id+count+version drift tuple,
attackers learn nothing about the schema. migration_count and
migration_status unchanged.

BUG-API-021 (plus siblings on webhook_secret_mismatch and
webhook_signature_mismatch) — the public 401 envelope returned by
/webhooks/brevo/<wrong-secret> literally named the BREVO_WEBHOOK_SECRET
env var in agent_action. Same recon-leak problem: anyone probing the
public URL learned the exact env-var name. Drop env-var vocabulary
from all three webhook-config error agent_actions; point at the docs
page where the operator-side rotation procedure lives.

Wire contract preserved: error codes, statuses, messages unchanged —
only agent_action sentences are softened. No new endpoints, no new
fields, no auth changes.

Surface checklist (rule 22):
- api/internal/migrations/state.go        — PublicVersion helper + Strings import
- api/internal/router/router.go           — /healthz emits PublicVersion()
- api/internal/router/healthz_test.go     — pinned shape updated + new regression
- api/internal/handlers/helpers.go        — 3 agent_action sentences softened
- api/internal/handlers/brevo_webhook_test.go — explicit assertion that BREVO_ env names never reach wire
- OpenAPI / dashboard / marketing         — no surface change (envelope shape unchanged)

Coverage block:
  Symptom:        /healthz.migration_version "063_forwarder_sent_audit_link.sql"; brevo 401 names BREVO_WEBHOOK_SECRET
  Enumeration:    rg -F 'BREVO_WEBHOOK_SECRET' internal/handlers/helpers.go ; rg -F 'mstate.Filename' internal/router/
  Sites found:    3 agent_action strings + 1 router emit site
  Sites touched:  4 / 4
  Coverage test:  TestHealthzMigrationVersionStripsFilenameSuffix (asserts no '_' or '.sql' in output across 7 cases); brevo_webhook_test BREVO_* + BREVO_WEBHOOK_SECRET assertion
  Live verified:  pre-merge: curl https://api.instanode.dev/healthz returned "063_forwarder_sent_audit_link.sql"; curl https://api.instanode.dev/webhooks/brevo/x returned "BREVO_WEBHOOK_SECRET" in agent_action.
                  post-merge: verify in PR comment.

Local gate:
  - go build ./...                      PASS
  - go vet ./...                        PASS
  - go test ./internal/migrations/      PASS
  - go test ./internal/router/          PASS
  - go test -run 'Brevo' ./internal/handlers/  PASS

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The package-external test in router_test exercises PublicVersion but
diff-cover attributes coverage by package, so the migrations/state.go
PublicVersion body landed at 0% for diff-cover. Add a package-internal
test (state_public_version_test.go) iterating the same 7 cases so
the patch-coverage gate sees migrations/state.go at 100%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mastermanas805 mastermanas805 merged commit 7dbed94 into master May 29, 2026
14 checks passed
@mastermanas805 mastermanas805 deleted the fix/brevo-agent-action-and-healthz-migration-leak branch May 29, 2026 18: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.

2 participants