Skip to content

sec(handlers,middleware): escape renderAuthError HTML + drop secret-length leak in e2e bypass log#173

Merged
mastermanas805 merged 1 commit into
masterfrom
sec/api-render-auth-error-escape-and-bypass-log
May 29, 2026
Merged

sec(handlers,middleware): escape renderAuthError HTML + drop secret-length leak in e2e bypass log#173
mastermanas805 merged 1 commit into
masterfrom
sec/api-render-auth-error-escape-and-bypass-log

Conversation

@mastermanas805

Copy link
Copy Markdown
Member

Two defense-in-depth security fixes uncovered during the 2026-05-29 api + common security audit. Full findings catalogue in /tmp/qa-session/shared/SEC-INBOX.md (SEC-API FINDING-23..28).

SEC-API FINDING-23 — renderAuthError HTML interpolation (P2 hardening, CWE-79)

renderAuthError is the only HTML-emitting handler in api. It took (headline, detail string) and fmt.Sprintf'd both into a <!DOCTYPE html> template without escaping. Every existing caller (24 sites across auth.go + magic_link.go) passes static literals, so there is no live exploit today — but the function is unsafe-by-default. Any future caller that passes a user-influenced value (OAuth profile name, JWT email claim, raw upstream error message) silently introduces reflected XSS on the api.instanode.dev origin. Cookies for that host (oauth_state, future session cookies) would be stealable.

Apply html.EscapeString to both args at the sink. The function is now safe for every caller regardless of input provenance — they don't have to remember to escape.

Adds one new regression test TestAuth_RenderAuthError_HTMLEscapesPayload that asserts the literal <script>, </script>, and <img src=x payloads do not survive into the response body, and that their escaped forms (&lt;script&gt; etc.) do.

SEC-API FINDING-26 — e2e bypass mismatch logs expected secret length (P3, CWE-200)

middleware.e2eTokenAccepted's mismatch path logged got_len, expected_len, and got_prefix. Two info-disclosure problems:

  • expected_len leaks the byte-length of E2E_TEST_TOKEN to anyone with log read access (NR Logs / log-aggregation breach). Narrows brute-force search space if the operator picked a short token.
  • got_prefix echoes the attacker's own guess into long-term log storage, attesting the env var is configured in prod and enabling correlation-grep against future attacker payloads.

Drop expected_len and got_prefix. Keep got_len — the attacker already knows the length of their own input, so this leaks nothing new while still letting an SRE distinguish 'wrong-content' from 'missing/malformed' failure modes.

Stats

  • 3 files changed (1 prod file each + 1 test file), 61 insertions, 3 deletions
  • Production LOC delta: 24 lines (well under 50)
  • No behavioural change on the happy path of either function
  • All existing middleware + handler tests pass
  • One new regression test added (TestAuth_RenderAuthError_HTMLEscapesPayload)
  • go vet ./... clean

Coverage block (CLAUDE rule 17)

Symptom:        renderAuthError HTML interpolation sink + e2e bypass log secret-length leak
Enumeration:    rg -F 'renderAuthError(' api/internal --type go;
                rg -F 'e2e_bypass.token_mismatch' api/internal --type go
Sites found:    renderAuthError: 24 (auth.go + magic_link.go) / e2e_bypass: 1 (fingerprint.go)
Sites touched:  Both at sink (one place each — every caller benefits)
Coverage test:  TestAuth_RenderAuthError_HTMLEscapesPayload (added) — fails if any
                future change drops html.EscapeString from the sink. The
                existing middleware coverage test exercises the bypass-mismatch
                path; the new field set is asserted via test-collected slog.
Live verified:  Static-only — patch is hardening with no live exploit today.
                Post-merge: rule 14 (`curl /healthz | jq .commit_id` ==
                `git rev-parse --short HEAD`) before declaring shipped.

Out of scope (filed as findings, deferred):

  • SEC-API FINDING-24 (P2) — Fingerprint middleware wired in dev-mode in production. 2-LOC router.go fix but needs full rate-limit regression suite.
  • SEC-API FINDING-25 (P3) — CLI auth poll GET-then-DEL race. Needs redis test seam refactor.
  • SEC-API FINDING-27 (P3) — Razorpay upstream error verbatim in 502 body. Coordinate with billing PR queue.

🤖 Generated with Claude Code

…ength leak in e2e bypass log

Two defense-in-depth security fixes uncovered during the 2026-05-29 api +
common security audit (see /tmp/qa-session/shared/SEC-INBOX.md).

SEC-API FINDING-23 — renderAuthError HTML interpolation (P2 hardening, CWE-79)

renderAuthError is the only HTML-emitting handler in api. It took
(headline, detail string) and fmt.Sprintf'd both into a <!DOCTYPE html>
template without escaping. Every existing caller (24 sites across
auth.go + magic_link.go) passes static literals so there is no live
exploit today, but the function is unsafe-by-default — any future caller
that passes a user-influenced value (OAuth profile name, JWT email
claim, raw upstream error message) silently introduces reflected XSS on
the api.instanode.dev origin. Cookies for that host (oauth_state, future
session cookies, etc.) would be stealable.

Apply html.EscapeString to both args at the sink. The function is now
safe for every caller regardless of input provenance — they don't have
to remember to escape. Adds one new regression test
TestAuth_RenderAuthError_HTMLEscapesPayload that asserts the literal
<script>, </script>, and <img src=x payloads do not survive into the
response body, and that their escaped forms (&lt;script&gt; etc.) do.

SEC-API FINDING-26 — e2e bypass mismatch logs expected secret length (P3, CWE-200)

middleware.e2eTokenAccepted's mismatch path logged got_len, expected_len,
and got_prefix. Two info-disclosure problems:
  - expected_len leaks the byte-length of E2E_TEST_TOKEN to anyone with
    log read access (NR Logs / log-aggregation breach). Narrows brute-
    force search space if the operator picked a short token.
  - got_prefix echoes the attacker's own guess into long-term log
    storage, attesting the env var is configured in prod and enabling
    correlation-grep against future attacker payloads.

Drop expected_len and got_prefix. Keep got_len — the attacker already
knows the length of their own input, so this leaks nothing new while
still letting an SRE distinguish "wrong-content" from "missing/malformed"
failure modes.

Production LOC delta: 24 lines (well under 50). No behavioural change
on the happy path of either function. Existing tests pass; one new
test added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mastermanas805
mastermanas805 merged commit d3a5f39 into master May 29, 2026
14 checks passed
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