This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry.
Project Overview
RamenHire (ramenhire.com) is a job board for bootstrapped, profitable startups — built with Next.js and Supabase. Job seekers apply, and companies submit listings through public forms with no login required. Every submission triggers an internal notification email, built from user-supplied fields, sent straight to the team's own inbox.
Bug Fix or Performance Improvement
lib/email-templates.ts interpolated user-controlled fields — applicant name, company name, "why interested," job description, and more — directly into raw HTML email strings with zero output encoding, including values embedded inside href="..." attributes.
That meant any of the site's four public forms (apply, post-a-job, subscribe, company registration) could inject arbitrary HTML into the notification email the team reads internally — no authentication required, no rate limit standing in the way of a single crafted submission. A malicious applicant name could silently smuggle in a fake "click here to verify your login" link, styled to look like it belonged in the template, sitting right next to the real "Review in Supabase →" button. This is a stored HTML-injection vector aimed squarely at whoever opens the inbox — a real phishing risk against the team itself, not a hypothetical.
Code
Commit 3aed95d — specifically the lib/email-templates.ts hunk. Added a small escapeHtml() helper (standard &, <, >, ", ' entity escaping) and applied it to every user-supplied value across all six email template functions — both plain text content and values embedded inside href attributes, escaped before the attribute string is built, not after.
My Improvements
The fix required more than "wrap everything in a helper" — text-node context and attribute context aren't the same problem. A value escaped only for display text still allows a quote-breakout attack inside an href="..." (e.g. " onmouseover="...), so escaping has to happen before the attribute is constructed, not at final render.
Verified this at two levels. First, locally: ran the actual template functions against a real injection payload —
"><img src=x onerror=alert(1)><script>alert(2)</script><a href="https://evil.example/phish">click</a>
— plus a separate quote-based attribute-breakout string, confirming none of it survived unescaped.
Then in production, end-to-end: submitted that same payload through the live site as a real application — real Cloudflare Turnstile challenge (not scripted around), real database insert, real notification email sent to the team inbox through the exact code path this fix touches. I opened the resulting email myself and confirmed the actual outcome: the <script> tag never executed, the fake <img> never attempted to load, and the injected link rendered as inert, garbled text sitting inside obviously-escaped characters — not a working "click here" button.
One honest caveat worth including rather than glossing over: in the screenshot below, the raw evil.example URL still appears blue and clickable. That's Gmail's own auto-linkification of any bare URL it finds in plain text — completely independent of the fix, and something Gmail would do to any plain-text email mentioning a URL, malicious or not. The actual thing this fix prevents — a disguised, legitimate-looking clickable button — is gone; the substring being technically linkable inside garbled text is a cosmetic side effect of Gmail's own behavior, not a gap in the fix.
Built and directed with Claude Code under my own review and testing — including designing the verification payloads and personally confirming the production result described above.
Top comments (0)