Filtering User Input

7 vues Markdown

Every value that arrives from a browser is read through Filter. It cuts the value down to the characters the receiving code expects, before anything else sees it.

Overview

WISECP never reads $_POST, $_GET or $_REQUEST directly. One class takes a path into the superglobal plus a filter name. It answers the value already reduced to what that filter allows.

Filtering is half of input safety. Validation is the other half: filtering removes characters, validation decides whether what is left is usable. A filtered but meaningless value still has to be rejected by your own check.

Reference

Signatures

Filter
public static function init($arg = null, $mod = false, $special = false);

public static function GET($arg = '');
public static function POST($arg = '');
public static function REQUEST($arg = '');
public static function FILES($arg = '');
public static function SERVER($arg = '');

public static function html_clear($arg = null, $allow = '');
public static function phone($arg = '');
public static function route($arg = '', $special = '');
public static function permalink($str, $options = []);

Everything here is static. Most names in the table below are also methods of their own with the same two arguments. Filter::rnumbers($value) does what init does after resolving the path. Three are different. There is no hclear method, because that name calls html_clear. false is the absence of a filter, and domain takes the value alone.

The Three Arguments of init

$arg A source path such as POST/user/name, or a plain value. A string that starts with none of the five source names is filtered as it stands, so Filter::init($text, "hclear") is legal. An empty first argument answers false.
$mod The filter name, one of the nineteen in the table. Default false returns the value untouched, and so does an unrecognised name.
$special Extra characters merged into the filter's allow list, as regular expression syntax. a-z is a range, and a literal hyphen or slash needs escaping. With $mod left at false it becomes the whole allow list, applied after markup is stripped.

Source Paths

The first path segment names the superglobal, the rest walks into it. POST/user/name reads the name key inside the user array of the request body. The same five accessors are public and return the raw value, unfiltered. With no argument they return the whole superglobal; with a missing key they return false, never null.

Filter::GET($arg) Query string. Path prefix GET/.
Filter::POST($arg) Request body. Path prefix POST/.
Filter::REQUEST($arg) Either source. Path prefix REQUEST/, and how an operation name is read.
Filter::FILES($arg) Upload entry for a field name, as PHP built it. Path prefix FILES/.
Filter::SERVER($arg) Server variable such as REMOTE_ADDR. Path prefix SERVER/.

Filter Types

The last column is what you get when the key is not in the request. That answer differs by filter.

FilterKeepsReturnsKey absent
falseThe value untouched, markup includedmixedfalse
hclearTags stripped after entities are decodedstringempty string
textSame, then both quote characters become numeric entitiesstringempty string
dtextSame as hclear, quotes left alonestringempty string
letters_numbersLatin letters and digitsstringfalse
lettersLatin letters plus the active language's lettersstringempty string
numbersDigits and hyphenstringfalse
rnumbersDigits and hyphen, then cast to a whole numberint0
amountDigits, hyphen, dot, comma; separators kept as typedstringempty string
rateSame, comma read as the decimal point, thousands dots droppedfloat0.0
ipLetters, digits, hyphen, dot, colon, so v6 survivesstringfalse
domainLetters, digits, dot, hyphen, then lowercasedstringempty string
emailLetters, digits and the characters @ . + _stringempty string
routeLetters, digits, hyphen, underscore, dot; ../ removed firststringempty string
folderLetters, digits, slash, hyphen, underscore, dotstringempty string
fileLetters, language letters, digits, hyphen, underscore, dotstringempty string
nounLetters, digits, comma, dot, spacestringempty string
identityDigits and hyphen, same rule as numbersstringfalse
passwordEverything, unchangedmixedfalse

password is deliberately a pass-through. A strong password is made of exactly the characters the other filters remove. Stripping them silently changes what the visitor typed. hclear, text, dtext, email, domain, identity and password ignore the third argument.

Helpers You Call Directly

html_clear($arg, $allow) Decodes entities, then strips tags. The second argument is the allow list in PHP's own format, for example '<b><i>'. This is the only way to keep tags: hclear never passes one.
phone($arg) Digits only: a country prefix, spaces and brackets disappear. One argument, no allow list.
route($arg, $special) Removes ../, then keeps letters, digits, hyphen, underscore and dot. Add the escaped slash through the second argument when a route segment carries one.
permalink($str, $options) The slug maker. Options, with their defaults: delimiter (a hyphen), limit (none), lowercase (true), replacements (patterns applied first, empty) and transliterate (true).

Example

Field names are the contract between the two sides. What the form posts is what the path spells.

the form that writes
<input type="hidden" name="operation" value="update_client">
<input type="hidden" name="id" value="42">
<input type="text"     name="user[name]">
<input type="email"    name="email">
<input type="password" name="password">
<input type="text"     name="slug">
the operation that reads
public function update_client(Operation $operation): bool
{
    $operation->demo();

    $id    = (int) Filter::init("POST/id", "rnumbers");
    $name  = Filter::init("POST/user/name", "hclear");
    $email = Filter::init("POST/email", "email");
    $pass  = Filter::init("POST/password", "password");
    $slug  = Filter::init("POST/slug", "route", "\/");

    // Filtering is not validation: these are clean, not necessarily usable.
    if (!$id)                                          throw new Exception(Language::gc("error/id-required"));
    if ($name === '')                                  throw new Exception(Language::gc("error/name-required"));
    if (!filter_var($email, FILTER_VALIDATE_EMAIL))    throw new Exception(Language::gc("error/email-invalid"));

    $bio = Filter::html_clear(Filter::POST("bio"), '<b><i><a>');

    return $operation->output(['status' => "successful"]);
}

Pitfalls

A misspelled filter name returns the raw value

The name is matched against a list and anything unmatched falls through to the untouched value, markup and all. Filter::init("POST/v", "number") is not a numeric read, it is no read at all. Writing the allow list into the second slot instead of the third does the same: Filter::init("POST/v", "a-z") filters nothing.

An absent key does not answer the same way twice

The five accessors always answer false, so a presence test is written !== false. Through init the answer belongs to the filter, as the table's last column shows. A test written as !== '' or !== null is true on every request for some of them.

The third argument is regular expression syntax

It is pasted into a character class exactly as written. It can widen the filter far past what you meant, and a stray bracket breaks the pattern. Escape what must be literal, and never build it from user input.

Array values do not survive a scalar filter

Only password and the unfiltered read pass an array through. The rest answer false, an empty string or zero depending on the filter. A grouped field is read key by key rather than as one value.

Cet article vous a-t-il été utile ?

Merci pour votre retour !

Besoin d'aide supplémentaire ?

Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.