You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
sec(api): strip /healthz filename + drop env-var names from public 401s (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>
// BUG-API-021 (QA 2026-05-29): the pre-fix agent_action literally
156
+
// named the BREVO_WEBHOOK_SECRET env var, which (a) is informational
157
+
// disclosure to a brute-forcer probing the public 401 surface — they
158
+
// learn the exact env-var name the operator must rotate — and (b)
159
+
// targets the operator using internal vocab the calling agent has no
160
+
// business surfacing to the end-user. The new copy drops the env-var
161
+
// name and points at the public docs page (which documents the
162
+
// rotation procedure for an operator that follows the link). Wire
163
+
// contract preserved: error code, status, message all unchanged —
164
+
// only the agent_action sentence is softened.
155
165
"brevo_secret_mismatch": {
156
-
AgentAction: "Tell the user this is a Brevo-webhook config mismatch, not their auth. Operators must verify the Brevo dashboard webhook URL matches the configured BREVO_WEBHOOK_SECRET — see https://instanode.dev/docs/email.",
166
+
AgentAction: "Tell the user this is a Brevo-webhook configuration mismatch, not their auth — they have no action. Operators: rotate the Brevo webhook secret and update the api Deployment — see https://instanode.dev/docs/email.",
157
167
},
158
168
// webhook_secret_mismatch is the generic per-provider webhook URL-path-token
159
169
// or shared-secret mismatch surface. API-19/96/97/98 (QA 2026-05-29): the
@@ -164,8 +174,12 @@ var codeToAgentAction = map[string]errorCodeMeta{
164
174
// distinguishes the secret-not-configured branch from the signature-mismatch
165
175
// branch below. Operator must wire the corresponding env var
166
176
// (BREVO_WEBHOOK_SECRET / SES_SNS_SUBSCRIPTION_ARN) before the route accepts.
177
+
// BUG-API-021 sibling: "set the corresponding webhook secret env var"
178
+
// pointed at an internal env-var by name. Same softening as
179
+
// brevo_secret_mismatch above — drop env-var vocab from the public
180
+
// 401 surface; the docs page covers operator-side wiring.
167
181
"webhook_secret_mismatch": {
168
-
AgentAction: "Tell the user this is an email-webhook secret-config mismatch, not their auth. Operators must set the corresponding webhook secret env var in the api Deployment — see https://instanode.dev/docs/email.",
182
+
AgentAction: "Tell the user this is an email-webhook configuration mismatch, not their auth — they have no action. Operators: configure the webhook secret in the api Deployment — see https://instanode.dev/docs/email.",
169
183
},
170
184
// webhook_signature_mismatch is the per-provider signature-verification
171
185
// failure surface — the secret IS configured, the inbound payload's HMAC /
@@ -174,8 +188,11 @@ var codeToAgentAction = map[string]errorCodeMeta{
174
188
// the secret yet" from "someone is sending bad signatures (or the provider
175
189
// rotated keys)" without an operator hand-grepping log lines. Used by
// BUG-API-021 sibling: "the api Deployment's env var" leaked the
192
+
// internal wiring; soften to "the configured webhook secret" so the
193
+
// 401 stays self-explanatory without naming env-var keys.
177
194
"webhook_signature_mismatch": {
178
-
AgentAction: "Tell the user the inbound email-webhook signature did not verify. Operators must confirm the dashboard webhook secret matches the api Deployment's env var and that the provider hasn't rotated signing keys — see https://instanode.dev/docs/email.",
195
+
AgentAction: "Tell the user the inbound email-webhook signature did not verify. Operators: confirm the dashboard webhook secret matches the configured value and the provider hasn't rotated signing keys — see https://instanode.dev/docs/email.",
179
196
},
180
197
// webhook_method_not_allowed surfaces the GET-on-a-POST-only webhook URL
181
198
// path (BUG-API-098). Brevo's dashboard sometimes sends a GET pre-flight to
0 commit comments