Safeguard
Application Security•

ASVS 5.0 V2.2.1: Validate Input Against Allow Lists

ASVS 5.0 V2.2.1 (Level 1) requires positive validation of input against allowed values, patterns and ranges. Code in Python and TypeScript, plus tests.

Safeguard Security Team
3 min read

ASVS 5.0 V2.2.1 requires that input is validated against business or functional expectations, using an allow list of values, patterns and ranges, or by comparing it to an expected structure and logical limits. At Level 1 this can focus on input used for business or security decisions. From Level 2 up it applies to all input.

Why it matters

Deny lists try to name the bad things, and attackers find the one you missed. An allow list defines the good and rejects everything else, which also removes whole classes of surprises: oversized values, odd encodings, unexpected types and extra fields.

How to implement it

Validate at the boundary with a schema, so the rule is declared once and enforced for every request.

import { z } from "zod";

const Transfer = z.object({
  accountId: z.string().regex(/^[A-Z0-9]{10}$/),
  amountCents: z.number().int().min(1).max(5_000_00),
  currency: z.enum(["USD", "EUR", "GBP"]),
}).strict();

const parsed = Transfer.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ error: "invalid_request" });

The .strict() call rejects unknown fields. In Python, pydantic models with extra="forbid" do the same. Decide the type first (integer, enum, date), then the range, and use a pattern only for genuinely structured strings. For free text, set a length limit and handle the output safely instead of trying to filter characters.

Validation does not replace output encoding or parameterized queries. It narrows what reaches them.

How to test it

Send each field with the wrong type, an empty value, a value one past each limit, an unexpected extra field, and a very long string. Every case should return a 400 level response with no stack trace. A property based test library can generate hundreds of these for you.

Picking the right allow list

Prefer the narrowest description that fits. An enumerated value, such as a status or currency code, beats a pattern. A bounded integer beats a string that happens to contain digits. For identifiers, use a fixed pattern and length. Names and addresses are the awkward case: allow a wide range of Unicode letters and punctuation, limit the length, and rely on output encoding and parameterized queries for safety rather than banning characters that real people use.

Reject, do not repair. Silently stripping characters can produce a value the attacker designed, and it hides bugs. Return an error that names the field but not the internal rule set, and log the failure.

Normalize before validating. Convert to a single Unicode form and trim, then validate, then use the validated value. Checking one form and storing another is a frequent source of bypasses.

Common mistakes

  • Validating only the fields the form shows, while the API accepts more.
  • Using a deny list of special characters.
  • Accepting a number as a string and converting it loosely.
  • Skipping validation on internal endpoints.

Quick check

What is the difference between L1 and L2 here?

At L1 you can limit validation to input used for business or security decisions. At L2 and above, all input is validated.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.