Input validation missing on the server
CWE-20CWE-602OWASP A04:2021Updated September 4, 20265 min read
Checks that only take place in the browser, required fields, maximum lengths, dropdown lists, price calculations, can be bypassed entirely with a direct request. The browser is a convenience for the user, not a security measure. Every value the server receives must be validated there again and in full.
A form that neatly warns about an overlong field and only shows existing options in a dropdown creates the impression that input is under control. That impression holds only for users who use the form. Whoever composes the request themselves deals with none of those rules. What follows from that is predictable, and the fix is to move the check back to where it counts.
Why does validation in the browser not count?
Everything happening in the browser is under the user’s control. The HTML can be edited, the JavaScript can be disabled or rewritten, and the request can be composed entirely outside the page. Missing input validation on the server means the application relies on checks taking place in that environment.
The distinction that helps: the browser validates for convenience, the server validates for correctness. A maxlength on an input field stops a user typing too much by accident. It does not stop someone sending a request with a value of ten thousand characters. Both checks may exist, but only the second is a measure.
Think of an order form with boxes of a fixed size. Whoever fills in the form is guided by those boxes. But the order is ultimately processed by someone who only looks at the values entered, and they also receive the order written on a sheet of paper without any boxes.
How does an attacker bypass validation in the browser?
Vulnerable:
<!-- The only check sits in the form -->
<form action="/order" method="post">
<input name="quantity" type="number" min="1" max="10" required>
<input name="price" type="hidden" value="49.95">
<select name="shipping">
<option value="standard">Standard</option>
<option value="express">Express (+ 9.95)</option>
</select>
<button>Order</button>
</form>
app.post('/order', requireLogin, async (req, res) => {
const total = req.body.quantity * req.body.price;
await orders.create({
user: req.user.id,
quantity: req.body.quantity,
shipping: req.body.shipping,
total,
});
res.send('Thank you for your order');
});
The server takes over everything that comes in, including the price from a hidden field. A request outside the form is trivial:
POST /order HTTP/1.1
Content-Type: application/json
{"quantity": -5, "price": 0.01, "shipping": "free-courier"}
Three things happen at once. The price is set by the customer. The quantity is negative, which depending on the processing can lead to a credit. And the shipping method is a value that did not appear in the dropdown and whose effect on the rest of the system nobody knows.
Safe:
import { z } from 'zod';
const Order = z.object({
articleId: z.string().uuid(),
quantity: z.number().int().min(1).max(10),
shipping: z.enum(['standard', 'express']),
}).strict(); // unknown fields are refused
app.post('/order', requireLogin, async (req, res) => {
const result = Order.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ error: 'Invalid input' });
}
const { articleId, quantity, shipping } = result.data;
// Prices come from your own records, never from the request
const article = await articles.find(articleId);
if (!article || !article.available) return res.status(404).json({ error: 'Unknown article' });
const total = article.price * quantity + SHIPPING_COST[shipping];
await orders.create({ user: req.user.id, articleId, quantity, shipping, total });
res.json({ ok: true, total });
});
The schema describes the complete shape of the request: which fields exist, of which type, within which bounds and with which permitted values. With strict() unknown fields are refused, which immediately protects against sending along properties that were not intended. And most importantly: the price no longer comes from the request but from your own records. Data the user should not determine is data you should not accept from them.
What is the impact of missing input validation?
The severity runs from medium to high and is determined by what happens behind the missing check. Missing validation is rarely the vulnerability itself; it is the condition under which other vulnerabilities arise.
On the technical side it is the first step in virtually every injection attack. Values passed without checking to a database, a system call, a template or a file path are exactly what makes SQL injection, command injection and path traversal possible. An overlong or unexpectedly shaped value can also cause a failure in which the application reveals more than intended.
On the business side the consequences are more concrete and sometimes directly measurable. A price set by the customer, a negative quantity leading to a refund, a discount stacked on itself, a status set to “paid”. Those are not technical vulnerabilities but logical ones, and those are almost never found by automated tooling.
How do you detect missing input validation?
A tester works not through the form but sends the requests directly. For every field they try what happens with values the interface does not permit: a negative number, an extremely large number, text where a number is expected, an empty value, a missing field, a value that was not in the dropdown.
They also watch for hidden fields and for values the client should not determine: prices, discounts, statuses, owner identifiers, roles. Each of those is altered to see whether the server takes it over. Extra fields not present in the form are sent along too, to test whether they are silently processed. Beyond that they check whether validation is equal on every channel, since the web interface and the API are often implemented separately with differing strictness. AssistSec pays particular attention to the business rules, because those checks differ per application and are therefore not covered by standard tooling.
How do you prevent missing input validation?
- Validate every request on the server with a schema describing the complete shape.
- Refuse unknown fields rather than ignoring them.
- Work with permitted values and ranges, not with a list of what is forbidden.
- Never take prices, discounts, statuses or roles from the request; get them from your own records.
- Validate the business rules alongside the shape: stock, validity, coherence between fields.
- Perform validation in one place per endpoint, so no path can go around it.
- Treat validation in the browser as convenience and repeat everything on the server.
- Escaping on output remains necessary; validation does not replace it.
- Apply the same rules to APIs, webhooks and background processing, not only to web forms.
Sources
Frequently asked questions
Do I have to validate everything twice then?
Yes, but with a different purpose. In the browser you validate for convenience: immediate feedback, less traffic, a nicer form. On the server you validate for correctness and security. The rules overlapping is not waste; they are two different functions.
Is an allow list better than a deny list?
Almost always. A list of what is not permitted is by definition incomplete: there is always a variant nobody thought of. A list of what is permitted excludes everything outside it, including what you did not foresee. That difference is exactly where filters on forbidden characters fail.
Does validation replace escaping on output?
No, and that confusion causes vulnerabilities. Validation determines whether a value is acceptable on arrival; escaping ensures a value is processed safely in the context it lands in. A valid name such as O'Brien must be accepted and also handled correctly in a query.
Where is the best place to validate?
As early as possible on the server, in one place per endpoint, with a schema describing the shape of the whole request. That is more reliable than separate checks scattered through the code, because then no path can bypass the validation.
Related articles
- VulnerabilitiesCWE-79A03:2021Cross-site scripting (XSS)Cross-site scripting (XSS) lets attackers inject malicious scripts into web pages that run in visitors' browsers. Learn how XSS works and how to prevent it.
- VulnerabilitiesCWE-1236A03:2021CSV injection in export filesAn exported CSV can contain formulas that Excel executes on your colleague's computer. Learn how that works and how to prevent it.
- VulnerabilitiesCWE-434A04:2021Unrestricted file uploadAn upload function without restrictions can lead to code execution on your server. Learn which checks are needed and which are not enough.
- VulnerabilitiesCWE-915A01:2021Mass assignmentMass assignment explained: how binding a whole request body onto a model makes isAdmin or balance writable, and how an allowlist prevents it.
- VulnerabilitiesCWE-89A03:2021SQL injectionSQL injection explained: how attackers use unfiltered input to read or change your database, what the impact is, and how prepared statements stop it.