Business & Agency Website
A newline in the email field turns your contact form into a spam relay — and the visitor is still shown "Thank you".
- PHP
- MySQL
About this project
An agency website — services, case studies, team, testimonials and a contact form — with an admin covering an enquiries inbox, services, portfolio and site settings.
For a small agency the entire website exists to produce one thing: an enquiry. The naive contact form calls mail() with the visitor's address concatenated into a From header and prints "Thank you" unconditionally. Three failures follow, all real. It stores nothing, so when mail() returns false — which it does constantly on shared hosting — the lead is gone and the visitor was told it worked. Putting unvalidated input into a mail header is email header injection: a newline lets an attacker append Bcc and turn the site into a relay until the domain is blacklisted and the owner's ordinary business email stops arriving. And with no honeypot or rate limit, bots fill the inbox until nobody reads it. The fix persists the lead first, attempts delivery second, reports what actually happened, and strips CR/LF from anything reaching a header. The proof demonstrates the injection working — printing the forged headers — then shows the same input neutralised, across seventeen further attack vectors.
What you will learn
- Understand email header injection, and why user input must never reach a mail header raw
- Persist a lead before attempting delivery, and report success on what actually happened
- Stop bots with a honeypot, a time check and a per-IP rate limit
- Refuse javascript: URLs in admin-editable link fields
- Log mail outcomes so a silent delivery failure is visible instead of invisible
Before you start
- PHP basics, and having written a contact form before
- PHP 8.1+ and MySQL 8 (or MariaDB 10.4+)
- No Composer, no Node — deploys to cPanel shared hosting as files
New to PHP? The free PHP course covers everything this project assumes.
Academic integrity notice
These projects are provided for learning, experimentation and reference. Understand the code, modify it for your own requirements, and acknowledge external work according to your institution’s academic policies. Priodemy does not supply work to be submitted as your own.
Features
- The injection demonstrated, not described — real forged Bcc headers printed from the naive handler
- Seventeen further header-injection vectors, none of which break out of the fixed pipeline
- Leads persisted before delivery is attempted, and flagged when mail actually fails
- Honeypot, minimum time-on-form and a per-IP rate limit generous enough for a shared office
- javascript: URLs refused in admin-editable link fields, at save and at render
- A thirteen-part proof covering XSS, SQL injection, forgery and rate limiting
Technology stack
Track: Web Development · Level: Major project
What is included
- The full site and admin — commented throughout
- schema.sql with nine tables and seed content, including one deliberately failed delivery
- proof/prove.php and the naive handler it attacks, so both halves are runnable
Delivered as a 148 KB ZIP. SHA-256 published under Changelog so you can verify it.
System requirements
- Operating system
- Any
- Runtime
- PHP 8.1+ and MySQL 8 / MariaDB 10.4+. No Composer, no Node.
Licence
Learn from it, change it, and use it in your own coursework and personal projects. Do not resell it or pass it off as your own original work.
- Personal and educational use
- Modifying the code however you like
- Using it as the basis of your own coursework, with honest acknowledgement
- Putting your modified version in a portfolio or on your own GitHub
- Reselling or redistributing the project, modified or not
- Presenting it as your own original work
- Commercial deployment — see the Commercial licence
Support window: 6 months from download.
Frequently asked
What is email header injection, in one sentence?
Putting user input into a mail header without stripping newlines, so an attacker can end your From header and start their own Bcc header — turning your contact form into a relay that sends mail signed with your domain until it gets blacklisted. The proof script performs the attack against the shipped naive handler and prints the forged header block so you can see exactly how it happens.
Why store the lead before sending the email?
Because mail() fails routinely on shared hosting and returns false, and a form that only emails has nowhere to put the enquiry when that happens. The visitor is thanked, the owner never hears about it, and nobody finds out. Storing first means a delivery failure costs you a notification, not a customer — and the admin inbox flags which ones failed. The seed data deliberately includes one failed delivery so you see it on the first run.
Are the testimonials real?
No, and they are deliberately obvious as samples rather than invented clients presented as real — the README says so explicitly. Replace them through the admin before putting this in front of anyone.
Changelog
- First release
SHA-256abef9ec0346aaf64d7d08d4f5a7ad8646c93e4a9c89dfb5132e4cbb90dd188e1