Form security: server-side checks and spam

Why:A form is the one place on a site where a stranger’s text gets into your database and your inbox. Afterwards you know why a check in the browser is not protection, what the server checks, how to stop spam without a CAPTCHA, which bothers people and shares data with third parties, and you write your project’s main action form with rules for the server.

30 minutes

Read by speech synthesis (Qwen3-TTS); the voice is not a person’s.

Server-side validation is protection that a request sent around the page can’t bypass. A trap, a rate limit and a delay stop spam better than a CAPTCHA does.

The only door

A static site accepts almost nothing: it only serves. A form is the exception. Through it, into your inbox, database and CRM comes the text of a stranger, and more often of a stranger’s program. Everything that arrives from outside must be treated as untrusted until you have checked it yourself. That is the rule from the lesson on security (injection, access rights), now applied to one particular place.

In the lesson on forms you already saw that required, type="tel" and pattern are a convenience for the person, not protection: a request sent around the page (by a script, by curl, by a developer tool) bypasses any check in the browser. So a form has two checks, and the main one is on the server.

Check yourself: why check on the server if the browser already has required?

A check in the browser can be bypassed: a request can be sent around the page. It helps people, and the server check protects you.

What the server checks

The principle is one: allow the known, don’t forbid the bad. For each field you write in advance what is acceptable, and everything else is rejected.

  • Presence and type. The name is there and is a string; the phone is there.
  • Length. A name of 2 to 80 characters, a message up to 1,000. With no upper limit, one request can bring megabytes.
  • Format. A phone: digits, plus, spaces, parentheses, hyphens. An email: a simple pattern, with the real check done by a confirmation email.
  • Extra fields. The form sends three fields; if six arrived, that is an attempt.
  • The size of the whole request. Limited on the web server.
  • Files (if the form accepts attachments): a list of allowed types and a size limit, your own random file name, storage outside the public folder, and never run them. The details are in the OWASP cheat sheet.
  • Email. If the form sends an email, values go into headers (the subject, the “from”) only through a sending library, and line breaks are stripped. Otherwise “header injection” lets someone add hidden recipients, and the form becomes a spam mailer.
  • Database queries only with parameters (see the lesson on security), and output to the page only with escaping.

On an error the server answers with the right code: 400 or 422 “the data is wrong,” 429 “too many requests,” 500 “internal error.” And with the right message to the person: “Please check the phone number,” “We couldn’t send this, please try again later.” Don’t show internals: table names, file paths and exception texts help an attacker and tell the person nothing. The details go to the log. Announce the error to a screen reader with role="alert" (the lesson on screen readers).

One more caveat about login: the messages “no such user” and “wrong password” let someone work out who is registered with you. Write them the same: “wrong address or password.”

Where spam comes from

Three sources: simple bots (they fill in every field and send hundreds of times), clever bots (a real browser, behaving almost like a person) and people who are paid to spam. The first are the most numerous, and cheap measures are enough against them.

The measures run from the cheapest for the person to the costliest.

  • A trap field (a honeypot). You add an extra field that a person doesn’t see and name it so that a bot is tempted (“Website,” “URL”). A person won’t fill it in, a bot will. If the field is filled in, the server pretends all is well and quietly drops the inquiry. There is a trap here for you yourself: the field must be hidden from all people. A field hidden by color alone (white on white) is read by a screen reader, and a password manager may fill it in, and an honest person ends up as a “bot.” The right way: shift it off the edge of the screen and add aria-hidden="true", tabindex="-1" and autocomplete="off". As for display: none, clever bots skip those.
  • A timing check. A timestamp (signed by the server) is put into a hidden field when the form is shown. A form submitted in under three seconds isn’t human.
  • A rate limit. You met it in the lesson on REST APIs, and here it is applied to a form: no more than a few inquiries an hour from one address or number, after that a 429 response. This is the only measure that also stops clever bots.
  • Content rules. For example, no more than two links in a first message.
  • Confirmation. For a mailing list: an email with a link, and until the person clicks, the address is not subscribed (double opt-in).
  • A CAPTCHA. A puzzle “prove you are not a robot.” It bothers people (distorted letters are hard to read, and some variants have no accessible alternative, which the W3C calls a barrier) and it usually passes data about the device and behavior to a third-party service. That is a question for data protection law and for accessibility (3.3.8). If a CAPTCHA is still needed, choose an accessible one that collects little extra data (for example Cloudflare Turnstile; in Russia Yandex SmartCaptcha is used) and name the service in your privacy notice. A foreign CAPTCHA service is also a question of transferring data abroad, which the next lessons cover.

Test it in the diagram: honest people and bots, and four measures. Look especially at what happens to a person with a screen reader if the trap is hidden by color alone.

A trap for bots: who gets through, who is stopped

A contact form and four visitors. Switch protections on and see whether spam is let through and whether honest people suffer.

Trap field (honeypot)
  • A person with a mouse
    Let through. Gets through: doesn’t see or touch the trap field, and doesn’t fill in the form instantly.
  • A person with a screen reader and a password manager
    Let through. Gets through.
  • A simple bot: fills in every field instantly, hundreds of times a minute
    Let through. Gets through: nothing stops it.
  • A clever bot: a real browser, skips hidden fields, waits, but sends thirty inquiries a minute from one address
    Let through. Gets through.

Spam got through: 2 of 2. Honest people stopped: 0 of 2. Some spam still gets through: each measure has its own weakness, which is why they are combined.

Text alternative: the diagram shows that a hidden trap field stops simple bots, but a trap hidden by color alone also catches a person with a screen reader. A field shifted off the edge, with aria-hidden, tabindex=-1 and autocomplete=off, is safe for people. A clever bot is stopped by a rate limit, not by the trap. A mandatory CAPTCHA from someone else’s service is inconvenient for people with disabilities and passes data to a third party.

Check yourself: why must a trap field not be hidden with white on white?

A screen reader will read it, autofill will reach it, and an honest person will fill it in and be taken for a bot. It must be hidden from everyone: off the edge of the screen, with aria-hidden, tabindex of minus one and autocomplete off.

The rest of the list

  • CSRF (cross-site request forgery). If a person is logged in to your site, someone else’s page can make their browser send a request in their name. The defense: a token in the form, or a cookie with SameSite=Lax (see the lesson on authentication) and a check of the Origin header. For an inquiry form without login the risk is small, and for a personal account it is mandatory.
  • Where the form goes. For a static site the form is received by a service or by your own function (the lesson on serverless). The recipient of the data is also a recipient of personal data: where it is located and who owns it decides questions of law (the next lessons). The keys to the receiver sit in environment variables, not in the page.
  • CSP for the form. form-action 'self' won’t let an injected foreign form change where data is sent.
  • Check it yourself. Submit the form empty, with a very long text, with a quotation mark and a script in a field, ten times in a row. What you didn’t get around to checking, a bot will check. In the lesson on tests you drew up scenarios like that.

Try the second view of the diagram too: the same measures in a feed of attempts, limited to the form.

A feed of attempts: what is stopped, what gets through

These are ordinary automatic attempts that hit any site. Switch measures on and see what is stopped.

  1. A bot guesses passwords on the admin login page
    Got through. OWASP category: A07 Authentication Failures.
  2. A script is pasted into the “Comment” field to run for other visitors
    Got through. OWASP category: A05 Injection (XSS).
  3. A quotation mark and a scrap of SQL are typed into a form field to pull out the database
    Got through. OWASP category: A05 Injection (SQL).
  4. A stranger on open Wi-Fi reads what the form sends
    Got through. OWASP category: A04 Cryptographic Failures.
  5. Your site is shown as an invisible layer on someone else’s page to hijack a click
    Got through. OWASP category: A02 Security Misconfiguration.
  6. A bot submits the inquiry form a thousand times with ads
    Got through. OWASP category: A06 Insecure Design.

Stopped 0 of 6. Nothing yet: the site is open to anyone who tries.

Text alternative: a feed of attempts (password guessing, a hole in a plugin, a script and SQL in a form, interception on Wi-Fi, embedding the site in someone else’s page, a search for keys, a swapped number in the address, spam) and a list of measures. Each attempt is stopped by at least one suitable measure: a second factor or rate limiting, updates, input checks, HTTPS, headers, keeping secrets out of the code, permission checks.

Practice

You stay in the era of 2022. Defend the main action form: add a trap, a time field and maxlength, remove the third-party CAPTCHA and write down the rules the server will apply.

The check reads markup and text: it will find a correctly hidden trap, required and maxlength, the absence of a third-party CAPTCHA, role="alert" and numbers in the rules. It won’t check the server’s work: you’ll write the real server using this table as your guide. You can show it to whoever builds the receiver: it is the specification.

What used to be different

A form in the 1990s sent an email with a Perl script that pasted everything in as it was, and “header injection” turned such forms into mailers of spam to thousands of addresses. Against spam people invented distorted letters (CAPTCHA), then “I’m not a robot.” They helped but divided people: some couldn’t pass them, and data about the device went to a third-party service. Today the approach is a combination of cheap measures that don’t bother a person, with a CAPTCHA kept for the last resort.

Practice

Preparing your page…

Test

Question 1 of 6

The form fields are marked required and maxlength. Is that enough to protect the server?

One answer

Cards

These cards go into your review once you finish the lesson.

  • Why is a form checked on the server?

    Answer

    A check in the browser can be bypassed: a request can be sent around the page. The browser check helps people, and the server check protects you. Check against what is allowed: type, length, format, size, extra fields.

  • How does a trap field (honeypot) work?

    Answer

    A field hidden from people: shifted off the edge, with aria-hidden=true, tabindex=-1 and autocomplete=off. A bot fills it in, and the server quietly drops the inquiry. Hiding by color alone is wrong: a screen reader will read it.

  • Which anti-spam measures don’t bother people?

    Answer

    A trap, a rate limit (with the code 429), a timing check, and refusing links in a first message. Add a CAPTCHA when these aren’t enough, and choose an accessible one that collects little data.

  • Which error messages should you show?

    Answer

    To the person: what to do (“please check the phone number”), without internals. Details (the query, the path, the version) go only to the log. Announce the error to a screen reader with role=alert.

  • What is dangerous in a form that sends an email?

    Answer

    Header injection: a line break in the name adds recipients. Strip line breaks, use a sending library and limit the number of emails.

Sources

Next

You now know: form security and spam.