Skip to content

security: fail-closed defaults for prod auth + Twilio sig#27

Merged
dougdevitre merged 1 commit intomainfrom
security/fail-closed-prod-defaults
Apr 17, 2026
Merged

security: fail-closed defaults for prod auth + Twilio sig#27
dougdevitre merged 1 commit intomainfrom
security/fail-closed-prod-defaults

Conversation

@dougdevitre
Copy link
Copy Markdown
Owner

Summary

Three production-safety changes from a security pass. Each mirrors the existing fail-closed pattern in api/cron/cost-rollup.ts.

  • Twilio signature validation forced on in production (src/core/twiml.ts) — signatureValidationEnabled() now returns true when NODE_ENV=production regardless of VALIDATE_TWILIO_SIGNATURE. Previously the env var defaulted to "false", so any deploy that didn't explicitly set it to "true" accepted forged Twilio webhooks → spoofed call status, arbitrary TwiML, and amplified Anthropic + ElevenLabs cost.
  • Outbound + records auth fail-closed in prod (src/core/outbound.ts, src/core/records.ts) — when both OUTBOUND_API_KEY and CLERK_SECRET_KEY are unset and NODE_ENV=production, return 500 "Server misconfigured" instead of falling open. A misconfigured prod deploy was previously a direct path to billing fraud (/call/outbound) or PII leak (/records). Non-prod still falls open as a local-dev escape hatch.
  • RECORDS_TTL_DAYS NaN guard (src/config/env.ts) — parseFloat("abc") was propagating NaN into DynamoDB TTL timestamps; now falls back to 365.

Test plan

  • npm run typecheck — clean
  • npm test — 284/286 (same 2 pre-existing failures on main, not introduced here)
  • Verify NODE_ENV=production with VALIDATE_TWILIO_SIGNATURE unset → /call/incoming rejects unsigned POSTs with 403
  • Verify NODE_ENV=production with both auth env vars unset → /call/outbound returns 500 with { error: "Server misconfigured" }
  • Verify NODE_ENV=development with both auth env vars unset → still falls open (no behavior change)

🤖 Generated with Claude Code

Three production-safety changes following an audit. Each mirrors the
existing fail-closed pattern in api/cron/cost-rollup.ts:

- Twilio signature validation is now ALWAYS on in production
  (signatureValidationEnabled returns true when NODE_ENV=production
  regardless of VALIDATE_TWILIO_SIGNATURE). Previously the env var
  defaulted to "false", so a deploy without it explicitly set to "true"
  accepted forged Twilio webhooks → spoofed call status, arbitrary
  TwiML, and amplified Anthropic + ElevenLabs cost.

- authorizeOutbound and authorizeRecords now return 500 in production
  when both OUTBOUND_API_KEY and CLERK_SECRET_KEY are unset. Previously
  they fell open silently — a misconfigured prod deploy was a direct
  path to billing fraud (/call/outbound) or PII leak (/records).
  Non-prod still falls open as a local-dev escape hatch.

- RECORDS_TTL_DAYS is now NaN-guarded. parseFloat("abc") → NaN was
  propagating into TTL timestamps as garbage; falls back to 365.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dougdevitre
Copy link
Copy Markdown
Owner Author

Manual verification — code trace ✅

All three remaining test-plan items verified by tracing the code paths:

  • NODE_ENV=production + VALIDATE_TWILIO_SIGNATURE unset → signatureValidationEnabled() returns true unconditionally (twiml.ts:37), unsigned POST → 403 (api/call/incoming.ts:54-56)
  • NODE_ENV=production + both auth vars unset → 500 "Server misconfigured" in both outbound.ts:146-164 and records.ts:64-79
  • NODE_ENV=development + both auth vars unset → skips prod guard, returns null → falls open (no behavior change)

CI green (284/286, same 2 pre-existing on main). Merging.

@dougdevitre dougdevitre merged commit aaa45b1 into main Apr 17, 2026
1 check passed
@dougdevitre dougdevitre deleted the security/fail-closed-prod-defaults branch April 17, 2026 21:04
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