skip to content

In PHP's filter extension, what separates FILTER_VALIDATE_* from FILTER_SANITIZE_* filters, and why is sanitizing input no substitute for escaping output?

level: middleimportance: must knowfreq 55%

answer

  1. accept or reject vs transform
  2. sanitize filters always hand back a string
  3. FILTER_SANITIZE_STRING deprecated in 8.1
  4. FILTER_DEFAULT is FILTER_UNSAFE_RAW
  5. output context unknown at input time

basics

~20 s

Validate filters decide whether a value is acceptable and return it typed or signal failure; sanitize filters strip or encode characters and always return a string. Sanitizing cannot know the output context, so escaping still happens at use.

solid answer

~40 s

A `FILTER_VALIDATE_*` filter answers "is this acceptable?": `FILTER_VALIDATE_EMAIL` returns the address or `false`, `FILTER_VALIDATE_INT` returns an `int` or `false`. A `FILTER_SANITIZE_*` filter answers "what is left after removing characters outside a set?": `FILTER_SANITIZE_NUMBER_INT` turns `"12abc"` into `"12"`, and it never reports failure. Sanitized output is not validated: `FILTER_SANITIZE_EMAIL` keeps apostrophes and still accepts garbage. `FILTER_SANITIZE_STRING`, once used as a catch-all, has been deprecated since PHP 8.1 with the hint to use `htmlspecialchars()`, and `FILTER_DEFAULT` is `FILTER_UNSAFE_RAW`, which changes nothing. Escaping depends on where the value ends up, whether HTML, an attribute, a URL, SQL or a shell command, and that is unknown when input arrives. So the pattern is: validate and reject at the boundary, store the canonical value, escape or bind at each output.

code

php · 10 lines
php
<?php
declare(strict_types=1);

var_dump(filter_var('12abc', FILTER_SANITIZE_NUMBER_INT));   // string(2) "12"
var_dump(filter_var('12abc', FILTER_VALIDATE_INT));          // bool(false)

var_dump(filter_var("o'brien@example", FILTER_SANITIZE_EMAIL));  // quote kept
var_dump(filter_var("o'brien@example", FILTER_VALIDATE_EMAIL));  // bool(false): no dotted domain

var_dump(filter_var('<b>hi</b>'));   // FILTER_DEFAULT: returned unchanged

go deeper

for a junior

Recall that validate filters accept or reject while sanitize filters only strip characters, and that output still needs htmlspecialchars() or its equivalent.

for a middle

Explain what specific sanitizers keep, why FILTER_SANITIZE_STRING was deprecated in 8.1, what FILTER_DEFAULT really does, and why escaping depends on the output context.

for a senior

Show how you retire encode-on-input code without double-encoding stored data, and how you set a validate-store-escape rule the whole team can apply.

for a principal

Weigh a central input-normalisation layer against per-field validation, and decide which guarantees the application may assume once data is stored.

## Two families in one extension PHP's filter extension exposes its filters as integer constants, and they fall into two families that behave very differently. | | `FILTER_VALIDATE_*` | `FILTER_SANITIZE_*` | |---|---|---| | Question answered | is the value acceptable? | what is left after cleaning? | | On bad input | returns `false` (or `null`, or throws, depending on flags) | returns a cleaned string anyway | | Result type | typed where it makes sense (`int`, `float`, `bool`, `string`) | `string` | | Examples | `INT`, `FLOAT`, `BOOL`, `EMAIL`, `URL`, `IP`, `DOMAIN`, `REGEXP` | `NUMBER_INT`, `NUMBER_FLOAT`, `EMAIL`, `URL`, `SPECIAL_CHARS`, `FULL_SPECIAL_CHARS` | A validator lets you **reject**. A sanitizer only **transforms**, which means it quietly accepts input you would have wanted to refuse. ## What sanitizers actually do Each sanitizer removes or encodes characters outside a fixed set: - `FILTER_SANITIZE_NUMBER_INT` keeps digits, `+` and `-`. `"12abc"` becomes `"12"`, `"1-2-3"` stays `"1-2-3"`, and `"abc"` becomes `""`. The result is a string and may still not be a number. - `FILTER_SANITIZE_EMAIL` keeps letters, digits and the punctuation allowed in addresses, including `'`, `` ` `` and `|`. `"o'brien@example"` survives intact, still carrying a quote and still lacking a real domain. - `FILTER_SANITIZE_SPECIAL_CHARS` and `FILTER_SANITIZE_FULL_SPECIAL_CHARS` HTML-encode characters. That is escaping for one context, applied at the wrong time. A sanitizer that "worked" gives no guarantee the value is well-formed; you still need a validator. ## The deprecated catch-all `FILTER_SANITIZE_STRING` (and its alias `FILTER_SANITIZE_STRIPPED`) stripped tags and encoded quotes, and was widely used as a generic "make input safe" step. PHP 8.1 deprecated both constants; the deprecation message points to `htmlspecialchars()`. Its problems illustrate the general point: it damaged legitimate text (anything resembling a tag disappeared), it was only aimed at HTML, and it gave teams a false sense that input had been made safe once and for all. ## FILTER_DEFAULT does nothing `filter_var($x)` with no filter, `filter_input()` without a filter argument, and definitions that name `FILTER_DEFAULT` all use `FILTER_UNSAFE_RAW`: the value comes back as a string, unchanged. Code that calls `filter_input(INPUT_POST, 'name')` and assumes the value was "filtered" has filtered nothing. ## Why escaping cannot move to input time The same string is dangerous in different ways depending on where it is written: - in HTML text, `<` matters; - in a quoted HTML attribute, the quote character matters; - in a URL, `&`, `#` and spaces matter; - in SQL or a shell command, entirely different characters matter, and the correct defence is parameter binding or argument escaping rather than any character removal. At the moment input arrives you do not know which of these outputs a value will reach, and one value often reaches several. Encoding it for one context corrupts it for the others: an HTML-encoded name appears as `&#39;` in a CSV export or a plain-text email. The robust split is: 1. **Validate at the boundary**: check type, format, length and allowed values, and reject what fails. 2. **Store the canonical value**, unencoded. 3. **Escape or bind at each output**, with the function for that context. ## Where sanitizing still fits Sanitizing is a normalisation step, not a security control. It is reasonable when you *want* to accept sloppy input and clean it, for instance removing spaces and dashes from a phone number before validating its digits. Even then, validate the cleaned result, and never treat a sanitized value as safe for output. ## A worked example: the volunteer's display name A sign-up form asks for a display name that will appear on a public rota page, in a CSV export and in reminder emails. - **Encode-on-input approach:** run it through an HTML-encoding sanitizer when it arrives. The rota page looks fine, but the CSV shows `O&#39;Neil`, the email subject shows entities, and if a template also escapes, the page shows them too. - **Validate-store-escape approach:** check the length and reject control characters at input, store `O'Neil` exactly, and let each output apply its own rule: `htmlspecialchars()` for the page, the CSV writer's quoting for the export, plain text for the email. The second approach is both safer and simpler, because every output is responsible only for its own context and none of them has to undo another's encoding.

  • What should replace FILTER_SANITIZE_STRING in code being upgraded to PHP 8.5?
    Not another input filter. Validate the field for what it should be (length, allowed characters or values) and store it raw, then escape at output, with `htmlspecialchars()` for HTML as the deprecation message suggests. Replacing it with `FILTER_SANITIZE_FULL_SPECIAL_CHARS` at input just moves the same encode-too-early mistake to a non-deprecated constant.
  • Is FILTER_SANITIZE_NUMBER_INT enough to make a quantity field safe to use?
    No. It removes characters but keeps `+` and `-` anywhere, so `"1-2-3"` passes through, and an empty result is possible. It returns a string, never failing. Validate with `FILTER_VALIDATE_INT` and a range instead, which rejects malformed input and gives you an `int`.

saying these in an interview costs you the question

  • A sanitize filter returns false when the input is bad
  • Running FILTER_SANITIZE_EMAIL means the address is valid
  • FILTER_SANITIZE_STRING is the recommended way to clean text in PHP 8
  • filter_input() without a filter argument still cleans the value
  • HTML-encoding input once on arrival makes it safe everywhere