Lion Cipher Lab
Exploit and fix email header injection in a contact form
- AppSec / DevSecOps
- Intermediate
- ~45 min
- Free · no account required
Why it matters
An email is not a magic object — it is just a block of plain text in a format called RFC 5322, where the top part is HEADERS (To:, From:, Subject:, Bcc:, one per line) and a blank line separates them from the BODY. The one detail that makes this a security topic is how those lines are separated: by a CARRIAGE RETURN followed by a LINE FEED, the two invisible control characters written \r\n (CRLF). So the moment a program builds an email by pasting untrusted user input into that text — for example `Subject: ` + whatever a visitor typed into a contact form — the visitor can type a \r\n of their…
What you'll learn
- explaining EMAIL HEADER INJECTION (CRLF injection into email headers, CWE-93): an email is plain text whose headers are separated by carriage-return + line-feed (\r\n), so untrusted input concatenated into a header can smuggle its own NEW headers or split the message
- defining the vocabulary inline — header, CRLF, SMTP, envelope vs message, Bcc, RFC 5322, aiosmtpd — so no prior email-internals knowledge is assumed
- standing up a fully offline lab: a local aiosmtpd stub SMTP sink on 127.0.0.1:8025 that only RECORDS recipients and the raw message, so nothing is ever delivered to a real mailbox
- building the vulnerable contact-form handler that f-string-concatenates a form field straight into the RFC 5322 header block, and submitting a \r\nBcc: payload to add hidden recipients
- MEASURING the break objectively: the number of envelope recipients the SMTP sink receives goes from 1 (the intended destination) to 3 (the intended destination plus two injected Bcc addresses)
- fixing it with email.message.EmailMessage: assigning headers via msg['Subject'] = value so the library REJECTS embedded newlines, and validating-and-rejecting any field containing CR or LF at the input boundary
- re-running the SAME payload against the fixed handler and watching it be rejected (or the newline neutralised) so the recipient count returns to 1 — the before/after proof
- judgment: why this is only exploitable when headers are hand-assembled, why stripping \n but not \r is an incomplete fix, and why the body/header split is a related variant to watch for
Steps in this lab
- The vocabulary: an email is plain text, and \r\n is what separates its lines
- Set up: Python, aiosmtpd, and a clean lab folder
- Build the local SMTP sink (it records recipients and the raw message — nothing leaves your machine)
- Build the VULNERABLE contact form (it hand-assembles the header block from user input)
- Measure the baseline: one clean submission, one recipient
- Exploit it: inject \r\nBcc: and watch recipients go from 1 to 3
- Why the tempting fix is incomplete — strip \n but not \r
- Fix it right — EmailMessage builds the headers, and reject CR/LF at the boundary
- Measure the fix: the identical payload is rejected, recipients back to 1
- What you did — the judgment, the honest negatives, and the reflex to keep