Skip to content

chore: harden the outbound mail pipeline#123

Merged
albanm merged 1 commit into
masterfrom
chore-harden-mail
May 18, 2026
Merged

chore: harden the outbound mail pipeline#123
albanm merged 1 commit into
masterfrom
chore-harden-mail

Conversation

@albanm
Copy link
Copy Markdown
Member

@albanm albanm commented May 18, 2026

Hardens the /api/mails* trust boundary so a caller cannot inject arbitrary HTML/JS or non-web URLs into outbound emails, and tightens the auth on POST /api/mails.

  • Auth. POST /api/mails now goes through assertReqInternalSecret (shared helper) instead of a hand-rolled key= query check plus a soft reqIsInternal
    warning. Missing/bad secret now returns 401, and the check is blocking rather than logged.
  • HTML sanitization at the trust boundary. New api/src/mails/escape.ts exposes two policies built on sanitize-html:
    • textToSafeHtml — HTML-escapes caller-supplied plain text and turns newlines into <br>, used for the text-only path of /api/mails and for the
      /api/mails/contact body.
    • sanitizeMailHtml — conservative allow-list (basic formatting tags, a[href] restricted to http/https/mailto), applied to caller-supplied html on
      /api/mails.
  • Link protocol guard. sendMailI18n refuses to send when params.link is not http(s): — defends the templated-link path against javascript: / data:
    URLs sneaking in via callers.
  • Tests. New API tests in tests/features/mails.api.spec.ts cover: text escape, html sanitization (stripping <script>, event handlers, attacker <img>),
    javascript: href stripping, and the contact-form escape. The existing "no secret" test now asserts 401. test-env DELETE / also clears the
    sd-rate-limiter-contact collection so the contact tests don't interact with each other.
  • Docs. Adds docs/architecture/emails.md describing the pipeline (two endpoints, sanitization boundary, MJML substitution, sendMailI18n) and references
    it from AGENTS.md as required reading for changes under api/src/mails/.
  • Dev/test ergonomics. npm run dev and the in-process test server set IGNORE_ASSERT_REQ_INTERNAL=1 so local/dev callers aren't blocked by the new
    internal-secret assertion.
  • Adds sanitize-html + @types/sanitize-html deps.

Caller-supplied content (events htmlBody, contact-form text, …) flowed
through microTemplate → mjml2html with no escaping or sanitization, so
arbitrary HTML/JS rendered in recipients' mail clients. Tighten the
trust boundary and the /api/mails origin/auth check.

- /api/mails: switch to assertReqInternalSecret (internal-origin +
  secret in one helper). Accepts the modern x-secret-key header while
  keeping the legacy ?key= query-param fallback for graceful migration.
- /api/mails: caller-supplied html runs through sanitize-html with a
  conservative allow-list, http/https/mailto hrefs only; text fallback
  is HTML-escaped (textToSafeHtml).
- /api/mails/contact: user-typed text reaches htmlMsg only via
  textToSafeHtml (escape + \n→<br>); the contact-form schema admits
  text only.
- sendMailI18n: reject non-http(s) params.link before deriving the
  {host}/{origin}/{link} substitutions used as button hrefs.
- New api/src/mails/escape.ts holds both helpers as thin wrappers over
  sanitize-html, so the entity-escape and sanitize policies live in one
  library.
- New docs/architecture/emails.md captures the send-pipeline trust
  model and invariants; AGENTS.md references it alongside the existing
  email-trust-and-site-isolation doc.
- IGNORE_ASSERT_REQ_INTERNAL=1 added to the api dev script and the
  in-process test harness so existing tests through nginx still
  exercise the route; production has no env-var bypass.
- test-env DELETE also wipes sd-rate-limiter-contact, matching the
  existing sd-rate-limiter-auth cleanup.
- mails.api.spec.ts: 4 new cases — plain-text escape, html
  sanitization, javascript: href stripping, contact-form escape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@albanm albanm merged commit f2168ef into master May 18, 2026
4 checks passed
@albanm albanm deleted the chore-harden-mail branch May 18, 2026 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant