Application Security
Secure Contact Forms That Fail Honestly
A contact-form architecture covering browser UX, server validation, abuse controls, origin checks, email delivery, privacy, logging, and truthful failure states.
Author: Fahad Bin Shakir · Published: · Updated: · 11 min read
Introduction
A contact form is a small public write API connected to an email system. It accepts untrusted text, can be automated cheaply, and often handles personal information. The design should protect availability and data without making a genuine visitor solve an obstacle course.
The most important product rule is that the interface must not report success unless the server accepted the message for delivery. A green animation after a failed request hides lost enquiries and makes operational problems harder to detect.
Define a narrow request contract
Accept only the fields the business will use. Set explicit minimum and maximum lengths, normalize line endings, reject control characters in single-line fields, validate email syntax conservatively, and reject unknown keys. Bound the total request body before parsing so an oversized payload cannot consume disproportionate memory.
Client-side validation improves feedback but is not a security boundary. Repeat every rule on the server and return stable status codes with general messages. Keep detailed provider or parsing errors in protected telemetry, not in the visitor response.
Verify browser request context
For a same-origin form, require JSON, check Origin against a strict allowlist, and reject unexpected Sec-Fetch-Site values when present. A custom header helps distinguish the intended client contract. These controls reduce cross-site submission, but they do not stop direct automation, so rate and abuse controls are still required.
If the application uses cookie-authenticated state, add CSRF protection appropriate to the framework and cookie settings. Do not reflect an arbitrary Origin in CORS. A public contact endpoint normally does not need broad cross-origin access.
Layer quiet abuse controls
Use a visually hidden honeypot that legitimate users and assistive technology are not asked to complete, plus a minimum completion time to catch the simplest bots. Apply bounded rate limits by a defensible client identifier and cap the rate-limit store so attackers cannot exhaust memory with fabricated keys.
For heavier abuse, add risk-based controls at the edge and preserve an accessible fallback. Do not use client puzzles as the first line of defence when validation, origin rules, provider quotas, and rate limits have not been implemented.
Treat email as an external delivery dependency
Keep the provider key and recipient address on the server. Use a verified sender domain, put the visitor's address in Reply-To rather than From, and construct a plain-text or safely escaped HTML message. Never interpolate user text into raw headers.
Apply an outbound timeout and distinguish invalid input, rate limiting, missing configuration, provider rejection, and timeout. Return success only after the provider accepts the request. If guaranteed processing is required, enqueue the validated message and return an accepted state tied to durable storage rather than calling the provider inline.
Minimize personal data and analytics exposure
Do not place name, email, subject, or message in query parameters, analytics events, error trackers, or ordinary access logs. Analytics can record a generic form_submit outcome after consent without copying field values. Mask or omit IP addresses according to the stated purpose and policy.
Explain which fields are processed, why, which email and hosting providers receive them, and how long correspondence may be retained. Tell visitors not to submit credentials, payment data, or private keys. The privacy page must describe the implementation that actually runs.
Test failure paths deliberately
Test malformed JSON, wrong content type, unknown fields, boundary lengths, honeypot input, too-fast submission, stale forms, foreign origins, rate exhaustion, missing credentials, provider 4xx and 5xx responses, timeout, network failure, and double clicks. Confirm that none produces a false success or duplicate delivery.
Monitor accepted, rejected, limited, provider-failed, and timed-out counts without message content. Add a synthetic test that exercises validation without sending mail, and perform a single controlled end-to-end delivery after configuration changes.
Deployment checklist
- Accept a small explicit schema and cap the body before parsing.
- Repeat all validation on the server and reject unknown fields.
- Restrict browser submissions by content type, origin, and fetch context.
- Use bounded rate limiting, a honeypot, and timing checks.
- Keep provider credentials server-side and use a verified sender.
- Report success only after accepted or durably queued delivery.
- Keep personal fields out of URLs, analytics, and ordinary logs.
- Exercise provider and network failure paths without repeated real emails.
