Blog
9 min readseguridad · formularios · web

Common security mistakes in web forms

Forms look like a simple part of a website, but they concentrate many risks: weak validation, XSS, CSRF, file uploads, spam, data leaks and privacy mistakes.

A form looks like a simple part of a website. A few inputs, a button, a confirmation message and not much else.

But as soon as that form sends data to a server, it is no longer just an interface component. It becomes a direct entry point into your application.

That is where problems start.

When you are learning web development, it is easy to think about forms from the user experience side: make it look good, warn if a field is missing, show a loading state, display a clear success message. All of that matters. But if you only look at the form from the frontend, you can miss the most important part: what can someone do if they do not use your interface the way you expect?

An attacker does not have to fill in your form from the browser. They can modify the HTML, disable JavaScript, repeat requests, change hidden fields, send fake file types or try payloads that would never pass through your design.

That is why it is worth knowing the most common security mistakes in web forms.

1. Trusting frontend validation

Browser validation improves the experience. It gives quick feedback when an email is missing, a password is too short or a required field is empty.

But it is not a security boundary.

This is one of the first mistakes to remove from your mindset:

<input type="email" required maxlength="80" />

This helps, but it does not protect anything by itself.

A user can open DevTools and remove required. They can change maxlength. They can send the request with curl, Postman or a script. They can bypass your form completely.

Important validation must exist on the server.

The frontend can say: this field looks wrong.

The backend must decide: this data is accepted or rejected.

The healthy approach is using both layers:

  • Frontend for fast and accessible feedback.
  • Backend for security, business rules and persistence.

If a rule matters, it cannot live only in client-side JavaScript.

2. Validating shape but not meaning

It is not enough to check that data looks valid. You also need to check whether it makes sense in context.

For example, a date field can have the correct format and still be invalid:

  • A start date after the end date.
  • A booking on a day that is not available.
  • A negative age sent by tampering with the request.
  • A quantity greater than the real stock.

OWASP separates syntactic validation from semantic validation. The first checks the shape of the data. The second checks whether the data makes sense for the business.

Real forms need both.

An email can have email format, but maybe it belongs to a disposable domain you do not want to allow. A price can be a number, but it should not be decided by the client. A role=user sent from a form should not be changeable to role=admin because someone edited the HTML.

The practical question is:

Does this data only need to be well formed, or does it also need to obey a system rule?

If it affects permissions, money, availability, stock, identity or account state, you cannot trust what comes from the browser.

3. Using blocklists as the main defence

Another common mistake is trying to block dangerous characters or words.

For example:

if (message.includes('<script>')) {
  throw new Error('Message not allowed');
}

It looks like a quick fix, but it is fragile.

Attackers do not need to write exactly <script> to cause problems. There are many ways to obfuscate, change context, use attributes, entities, URLs or combinations that bypass simple filters.

You can also block legitimate text. If someone writes a technical explanation and mentions an HTML tag, they are not necessarily attacking your application.

As a general rule, it is better to allow what is expected than to guess everything bad.

For structured data, use allowlists:

  • Available countries.
  • Possible roles.
  • Existing categories.
  • Accepted file types.
  • Number ranges.
  • Minimum and maximum lengths.

For free text, the solution is usually not banning half the keyboard. The key is storing the data safely and encoding it correctly when you display it again.

4. Rendering user data without escaping

This is one of the most dangerous mistakes: receiving text from a form and later displaying it as HTML without treating it as untrusted data.

Imagine a contact form, comments or reviews. If someone submits this:

<img src=x onerror=alert('xss')>

And your application inserts it as HTML, you may open the door to XSS.

The problem is not just an alert box. XSS can allow session theft, actions on behalf of the user, page modification or information capture.

The defence is not only validating input. You also need to encode output according to the context.

It is not the same to place data in:

  • HTML text.
  • An HTML attribute.
  • A URL.
  • JavaScript.
  • CSS.

Each context needs different handling.

That is why innerHTML should make you careful when content comes from a user.

If you only need to show text, use APIs that treat the value as text, not HTML. If you really need to allow rich HTML, use a sanitisation library designed for that and limit the allowed tags.

A form does not end when you store the data. You also need to think about where it will appear later.

5. Forgetting CSRF protection

CSRF happens when an external site causes the user’s browser to send an authenticated request to your application.

The user may be logged into your website. If cookies are sent automatically and your server accepts a sensitive action without additional protection, another page may try to trigger that action.

This matters especially for forms that change state:

  • Changing email.
  • Changing password.
  • Deleting an account.
  • Publishing content.
  • Updating settings.
  • Sending a purchase.

The typical protection is using unique and unpredictable CSRF tokens, plus configuring cookies properly with SameSite, Secure and HttpOnly where appropriate.

Checking headers such as Origin or Referer can also help, but it should not be the only defence for sensitive actions.

The simple idea is:

Do not accept an important action only because a valid cookie arrived.

The request should also prove that it came from a legitimate form or flow in your application.

6. Storing sensitive information in hidden fields

Hidden fields are not secure. They are only visually hidden.

This mistake appears often in forms that send information such as:

<input type="hidden" name="price" value="49.99" />
<input type="hidden" name="role" value="user" />
<input type="hidden" name="discount" value="10" />

A user can change those values before submitting the form.

If the server trusts them, you have a problem.

Hidden fields can be useful for carrying identifiers or non-sensitive state, as long as the backend validates them. But they should not be the source of truth for prices, permissions, discounts, resource owners or any data that affects security.

The practical rule:

The client can suggest. The server decides.

If the product price matters, the server calculates it. If the user has a role, the server reads it. If a discount exists, the server validates it.

7. Uploading files without serious controls

File upload forms are especially delicate.

Checking the extension on the frontend is not enough:

<input type="file" accept="image/png,image/jpeg" />

accept helps the user, but it does not protect the server.

An attacker can upload a file with a fake extension, huge size, unexpected content or a filename designed to cause problems.

Some basic rules:

  • Validate maximum size on the server.
  • Use an allowlist of types.
  • Do not trust only the name or extension.
  • Generate a new name when storing the file.
  • Do not allow user-controlled paths.
  • Serve files with the correct Content-Type.
  • Analyse files if the context requires it.
  • Avoid executable or dangerous types.

It is also worth separating storage from execution. A user-uploaded file should not end up in a folder where it can run as code.

Uploading a profile picture looks like a small feature. In security, it is not.

8. Giving too much information in errors

Error messages are useful for the user, but they can also leak clues.

This is not the same:

We could not process the request.

As this:

SQL error in users.password_hash line 42.

Login forms also need care. If you respond:

  • This email does not exist.
  • The password is incorrect.

You allow user enumeration. Sometimes it is better to respond with a more neutral message:

The credentials are not correct.

This does not mean hiding every error. The user needs to know what to fix. But they do not need stack traces, table names, server paths or validation details that help an attack.

A good balance:

  • Clear messages for the user.
  • Detailed logs for the developer.
  • No internal information in the public response.

9. Not limiting spam, abuse or automation

A public form can receive spam, automated testing and abuse.

This affects contact forms, comments, registration, newsletters, password recovery and any exposed endpoint.

Possible defences include:

  • Rate limiting by IP or user.
  • Properly accessible honeypots.
  • Form tokens.
  • CAPTCHA only when genuinely needed.
  • Length limits.
  • Temporary blocking after too many attempts.
  • Email verification when needed.
  • Alerts if volume changes suddenly.

Not every form needs CAPTCHA. In fact, adding CAPTCHA by default can hurt the experience. But you should think about what happens if someone submits the form 1,000 times.

The useful question:

What does it cost me if this form can be automated?

If it sends emails, creates accounts, writes to a database or consumes resources, you need limits.

10. Asking for more data than necessary

Security is also about privacy.

A form should ask for the data it needs, not the data that might be useful someday.

Every extra field increases responsibility:

  • More information to store.
  • More data to protect.
  • More impact if there is a leak.
  • More friction for the user.

If a contact form only needs name, email and message, maybe it does not need phone number, company, job title, city and budget.

Less data does not only improve conversion. It also reduces risk.

You also need to care about where that data ends up: email, CRM, spreadsheet, logs, analytics, external tools. Sometimes the form is protected and the data later leaks through a log or integration.

11. Not testing outside the happy path

The happy path is what you test when everything goes well:

  1. Fill in valid fields.
  2. Press submit.
  3. See a success message.

That is not enough.

A form should also be tested with uncomfortable cases:

  • Empty fields.
  • Very long text.
  • Unusual characters.
  • Edge-case email formats.
  • Impossible dates.
  • Negative numbers.
  • Large files.
  • Fast double submit.
  • Reload after submit.
  • Repeated requests.
  • JavaScript disabled.
  • Slow server response.

You do not need to turn every form into a full audit, but you should step outside perfect usage.

Many bugs appear when the user, the network or the attacker does not follow the script.

A reasonable checklist before publishing

Before publishing a form, I would check this:

  • Does important validation exist on the server?
  • Are there length, type and format limits?
  • Are business rules validated outside the client?
  • Is user data escaped when displayed?
  • Is there CSRF protection for authenticated actions?
  • Do hidden fields avoid sensitive decisions?
  • Are file uploads validated and stored carefully?
  • Do public errors avoid leaking internal information?
  • Is there any defence against spam or abuse?
  • Are you asking only for the data you need?
  • Have cases outside the happy path been tested?

It is not a perfect list, but it changes the way you look at a form.

The important part

A form is not just an interface. It is a boundary between the user and your system.

As a frontend developer, you can do a lot to make it clear, accessible and comfortable. But you also need to understand which parts cannot depend on the browser.

The idea I keep is this:

Every piece of data that comes from a form should be treated as untrusted until the server validates it.

This does not mean programming with paranoia. It means accepting a basic reality of the web: the user controls the client.

Once you keep that in mind, you start designing forms differently. Frontend validation to help. Backend validation to decide. Encoded output to display. Limits to prevent abuse. And only the data you actually need.

That is when a form stops being just another HTML block and becomes a serious part of application security.