security: escape email HTML + 3 hardening fixes#90
Merged
Conversation
Unescaped user input in every email template (HTML injection into recipient + staff inboxes), login open-redirect, false AES-256-GCM privacy claim, and admin password written to deploy logs.
Two new high-severity ReDoS / quadratic-CPU advisories in transitive deps (js-yaml -> brace-expansion), caught by the audit gate. npm audit fix only — lockfile change, no API impact.
goetchstone
force-pushed
the
fix/security-tier1
branch
from
July 21, 2026 10:31
78c7fe9 to
6966f39
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes the Tier-1 findings from the security review — the ones that could embarrass an email-security consultancy or get the domain blocklisted.
1. Every email HTML template interpolated user input unescaped (the real one)
Attacker-controlled values (ticket reply bodies, lead/booker names, subjects, notes) were placed raw into email HTML sent from our SPF/DKIM/DMARC-aligned domain — HTML injection / phishing into recipients' and staff's inboxes.
escapeHtml()(server/email/escape.ts) applied across all templates (ticket,appointment,appointment-reminder,invoice) and the two inline-HTML routes (leads.submit,tickets/reply).&<>only; appointment note did&<only) with the complete escaper (& < > " ').2. Login open redirect
login-form.tsxdidrouter.push(callbackUrl)straight from the query string. Now only same-origin relative paths are honored (/foo, neverhttps://evil.comor//evil.com).3. False privacy-page security claim
The page claimed "API keys and credentials are encrypted at rest using AES-256-GCM" — but there is no encryption code (
APP_ENCRYPTION_KEYreferenced nowhere, nocreateCipheriv). Replaced with only what's true.4. Admin password leaked to deploy logs
The seed
console.log'd the generated password to stdout (→ deploy logs), and the "0600 file" mechanism was dead (eachcompose run --rmis a fresh container; the migrator has no volume). Now the seed writes a 0600 file to a host-mounted path (SEED_ADMIN_CRED_FILE) and prints only a pointer;deploy.shbind-mounts so it reaches the host; removed the deadreset-admin-password.tsreference; added.first-admin-credentialsto.gitignore.Verified
tsc,npm run build,bash -n deploy.sh, and the suite (55 tests, 6 new) all clean.deploy.shseed/credential path needs a smoke test on a real fresh deploy (empty DB) — I can't run the full Docker deploy from here. Theseed.tschange is covered by tsc/tests; thedeploy.shbind-mount is reasoned but unexercised.Not in this PR (Tier-2, flagged for follow-up)
XFF-spoofable rate-limit keying, DNS-amplification fan-out, session-revocation lag, booking accepts past/off-hours times,
files/[id]nosniff, cuid bearer tokens.🤖 Generated with Claude Code