PHP5: Een formulier verwerken met de filter extensie
Zoals je inmiddels vast wel weet heeft PHP 5.2 de nodige nieuwe goodies. In een vorig artikel beschreef ik al de nieuwe datum- en tijds functies die met PHP 5.2 worden meegeleverd en vandaag kijken we eens naar de filter extensie die ook sinds PHP 5.2 wordt meegeleverd.
De filter extensie biedt je als ontwikkelaar een paar hulpmiddelen om bijvoorbeeld cross-site-scripting tegen te gaan maar ook om te controleren of bepaalde data wel in het juiste formaat is.
Een simpel voorbeeld
Er zijn twee manieren waarop je deze filter extensie kunt gebruiken. Je kunt data valideren of opschonen voor verder gebruik. Laten we meteen maar in een voorbeeldje duiken.
Stel dat je een weblog aan het ontwikkelen bent en je wilt de reacties op een bepaald blog item tonen. Om dat doel te bereiken zul je een item ID moeten ontvangen en je moet natuurlijk controleren of dat wel een valide nummer is voordat je het in een SQL query stopt. Normaal gesproken kom je dan op iets als:
-
$itemId = -1;
-
-
{
-
$itemId = (int) $_GET['id'];
-
}
-
-
if ($itemId> 0)
-
{
-
// toon de reacties met $itemId
-
}
Met de filter extensie zou je dit kunnen herschrijven naar:
-
$itemId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
-
-
{
-
// toon de reacties met $itemId
-
}
Het scheelt niet enorm veel in de hoeveelheid code die je moet schrijven om dit doel te bereiken, maar het is wel simpeler. Zo hoef je niet eerst te controleren of het argument wel bestaat - dat gebeurt binnen de filter_input() functie. Het geeft ook iets duidelijker aan wat precies de bedoeling is van de code, al komt dit voornamelijk door de functie naam filter_input().
De handtekening van de filter_input() functie is als volgt:
-
mixed filter_input ( int bron, string variabeleNaam, int filter [, mixed options ] )
Het eerste argument definieert waar de data vandaan moet komen. Er zijn enkele constantes die je hiervoor kunt gebruiken:
- INPUT_GET: de variabelen die via de query string binnenkomt ($_GET)
- INPUT_POST: de variabelen die binnen komen via POST data ($_POST)
- INPUT_COOKIE: de cookies die binnengekomen zijn ($_COOKIE)
- INPUT_ENV: de variabelen die binnenkomen via de systeemsomgeving ($_ENV)
- INPUT_SESSION: de variabelen die binnenkomen via de session ($_SESSION)
- INPUT_DATA: een ietwat vreemde eend in de bijt, maar INPUT_DATA geeft je de mogelijkheid om zelf de te filteren data aan te leveren, zoals we verderop zullen zien
Het tweede argument definieert welke variabele je uit de bron wilt hebben en het derde argument definieert welke filter je wilt toepassen op deze data. Er zijn twee groepen van filters. Meestal geef je als filter direct een constante op welke aangeeft welk filter je wilt gebruiken. Er zijn 2 groepen van filters: er zijn de VALIDATE filters welke controleren of de data in het juiste formaat staan en er zijn de SANITIZE filters welke ongewenste karakters wegfilteren. De constantes voor validate filters beginnen met FILTER_VALIDATE_* en de sanitizing filters, you guessed it, FILTER_SANITIZE_* .
Er is ook een andere manier op filters op te geven: je kunt de hulp-functie filter_id() gebruiken. In de handleiding staat bij elk (sanitizing) filter een kortere naam, bij FILTER_SANITIZE_SPECIAL_CHARS staat bijvoorbeeld als korte naam 'special_chars'. Door deze korte naam op te geven aan filter_id() krijg je vervolgens de juiste FILTER waarde terug. Het is een wat rare feature, aangezien je alleen sanitizing filters kunt op geven en zo lastig te onthouden zijn die constantes nu ook weer niet. Mijn advies is dus om gewoon de constantes te gebruiken.
Het laatste argument aan filter_input(), options, is optioneel. Dit argument kan gebruikt worden om flags of options door te geven aan het geselecteerde filter. Zo kun je bijvoorbeeld bij het FILTER_VALIDATE_INT filter aangeven dat het nummer tussen 2 waarden moet liggen door de opties min_range en max_range op te geven:
De FILTER_VALIDATE_* filters geven altijd FALSE terug als de gegevens niet kloppen.
Eigen gegevens filteren
Hoewel je de filter extensie eigenlijk voornamelijk zou gebruiken voor gegevens die via POST of GET binnenkomen kan het zo zijn dat je gegevens die je via een andere manier binnenkrijgt, bijvoorbeeld via XML-RPC, wilt controleren.
Wanneer je dit nodig hebt gebruik je in plaats van filter_input() of filter_input_array(), simpelweg de functies filter_var() of filter_var_array(), respectievelijk.
-
'leeftijd' => 24,
-
'naam' => '<strong>Mathieu</strong>'
-
);
-
-
'filter' => FILTER_VALIDATE_INT,
-
'min_range' => 23,
-
'max_range' => 25
-
),
-
),
-
'filter' => FILTER_SANITIZE_STRING,
-
'flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH
-
)
-
);
-
-
$result = filter_var_array($data, $velden);
-
Een groter formulier
Je begint pas winst te krijgen op het moment dat je wat meer data moet opschonen en valideren. Laten we even uitgaan van ons weblog voorbeeld van hierboven en een reactie-formulier verwerken waar de mensen een username, emailadres, titel van de reactie en inhoud van de reactie moeten opgeven:
-
<form action="reactie.php" method="post">
-
<input type="hidden" name="itemId" value="1" />
-
-
<label>Username:</label>
-
<input type="text" name="username" />
-
-
<label>Jouw emailadres:</label>
-
<input type="text" name="email" />
-
-
<label>Topic titel</label>
-
<input type="text" name="titel" />
-
-
<label>Inhoud:</label>
-
<textarea rows="10" cols="10" name="inhoud"></textarea>
-
-
<input type="submit" value="Knal er maar tussen.." />
-
</form>
We hebben dus 5 velden en voordat we iets kunnen gaan opschonen of valideren zullen we eerst de nodige regels moeten opstellen:
- itemId - moet een heel nummer zijn
- username - het username mag van alles zijn, maar mag alleen alphanumerieke waarden bevatten
- email - dit moet een valide emailadres zijn
- titel - de titel mag alles zijn, maar mag geen HTML doorlaten en we willen dat enters e.d. eruit gefilterd worden
- inhoud - de inhoud mag ook alles zijn, maar mag ook geen HTML doorlaten
We kunnen met deze regels nu naar de handleiding van de filter extensie gaan om te zien wat we nu precies nodig hebben. Ik vind het niet de meeste duidelijke pagina want er staan allerlei constantes waar je vaak niet direct chocola van kunt maken, maar oke, na wat zoeken komen we dan uit op het volgende:
- Voor itemId gebruiken we FILTER_VALIDATE_INT om te verifieren dat het een heel nummer is
- Voor username gebruiken we FILTER_VALIDATE_REGEXP om precies aan te kunnen geven wat er nu wel of niet in de username mag zitten
- Voor email gebruiken we FILTER_VALIDATE_EMAIL om te verifieren dat dit een juist emailadres is
- Voor titel en inhoud gebruiken we FILTER_SANITIZE_STRING zodat de HTML tags zelf wel verwijderd worden - maar niet hun inhoud.
Voor titel en inhoud hadden we ook FILTER_SANITIZE_SPECIAL_CHARS kunnen gebruiken. FILTER_SANITIZE_SPECIAL_CHARS had de HTML tags omgeschreven naar een HTML entiteit in plaats van ze te verwijderen. Het probleem met dit filter is echter dat enters ook worden omgeschreven naar HTML entiteiten, en dat is niet erg handig aangezien je deze waarschijnlijk wel wilt behouden. Wij houden het dus bij FILTER_SANITIZE_STRING.
We zouden nu de functie filter_input() kunnen gebruiken die we zojuist hebben besproken maar dat zou 5 aparte regels met filter_input() opleveren. Gelukkig is er ook de functie filter_input_array() waaraan we een array van veld definities kunnen opgeven.
De array die je aan filter_input_array() geeft moet een associatieve array zijn waarbij de sleutel het veldnaam uit het formulier is. De waarde kan simpelweg een FILTER_* constante zijn, maar de waarde kan ook een array zijn waarin de nodige opties in opgegeven kunnen worden zoals je zult zien bij de username:
-
'itemId' => FILTER_VALIDATE_INT,
-
'filter' => FILTER_VALIDATE_REGEXP,
-
'regexp' => '~^[a-z0-9]{3,}$~i'
-
)
-
),
-
'email' => FILTER_VALIDATE_EMAIL,
-
'inhoud' => FILTER_SANITIZE_STRING,
-
'filter' => FILTER_SANITIZE_STRING,
-
'flags' => FILTER_FLAG_STRIP_LOW
-
)
-
);
-
-
if ($_SERVER['REQUEST_METHOD'] == 'POST')
-
{
-
$filteredData = filter_input_array(INPUT_POST, $velden);
-
-
}
Zoals je ziet hebben we een associatieve array gebouwd met de veldnamen als sleutel, vervolgens hebben we als waarde opgegeven wat voor filter we willen toepassen op die data. In het geval van 'username' en 'titel' moesten we extra opties opgeven en daarom gebruiken we voor deze velden een array waarin we alle gegevens konden op geven. Deze array kan 3 elementen hebben:
- 'filter' - om aan te geven welk filter het is
- 'options' - om filter specifieke opties op te geven zoals bijvoorbeeld 'max_range'/'min_range' en 'regexp'
- 'flags' - om kleine gedragswijzigingen op te geven zoals het wel of niet strippen van bepaalde karakters
Bij de titel hadden we als regel dat er geen HTML in mocht voorkomen maar ook dat er geen enters in de data terecht mochten komen. Daarvoor hebben we de flag FILTER_FLAG_STRIP_LOW. Deze flag vertelt het filter dat het karakters met een ASCII code lager dan 32 decimaal uit het resultaat moet halen.
Een enter onder Windows bestaat uit 2 karakters: een line feed en een carriage return. Deze 2 karakters hebben respectievelijk de ASCII codes 10 en 13 decimaal en aangezien deze lager zijn dan onze ondergrens van 32 worden ze verwijderd. Er is ook een bijbehorende FILTER_FLAG_STRIP_HIGH welke karakters met een code van hoger dan 128 weg haalt. Gebruik je dus zowel FILTER_FLAG_STRIP_LOW als FILTER_FLAG_STRIP_HIGH dan houd je alleen alfanumerieke waarden over. Wil je de karakters niet weghalen maar encoderen naar HTML entities dan kun je gebruik maken van FILTER_FLAG_ENCODE_LOW en FILTER_FLAG_ENCODE_HIGH als flags.
Het resultaat van deze functie is een associatieve array met daarin alle velden die je hebt gedefinieerd in de $velden array met de bijbehorende waarden. Wanneer een veld niet aanwezig was in de geselecteerde bron krijg je NULL als waarde en wanneer de waarde van het veld niet juist was volgens het filter is de waarde FALSE. Anders bevat het veld de verstuurde en/of gefilterde waarde.
Wanneer je het formulier zou versturen met deze data:
- username
- mathieu
- mathieu@scriptorama
- titel
- Test bericht
- inhoud
- Ja, leuke blogposting<script>alert('XSS');</script>
Dan komt daar dan de volgende array uitrollen:
-
array(5) {
-
["itemId"]=>
-
int(1)
-
["username"]=>
-
string(7) "mathieu"
-
["email"]=>
-
bool(false)
-
["inhoud"]=>
-
string(42) "Ja, leuke blogpostingalert('XSS');"
-
["titel"]=>
-
string(12) "Test bericht"
-
}
Zoals je ziet zijn de twee velden die we opzetten fout hebben ingevuld, het emailadres en de inhoud veranderd door de filter extensie. Het emailadres geeft FALSE terug en was dus geen valide emailadres en uit de inhoud zijn alle tags gestripped.
Arrays van input verwerken
PHP is altijd al in staat geweest om binnenkomende gegevens te verwerken tot array, mits je de juiste syntax gebruikt. De filter extensie heeft hier ook ondersteuning voor. Laten we als voorbeeld een rijtje checkboxes nemen:
Nu wil je dus van alle waarden in test wel zeker weten dat de waarden numeriek zijn. Dit kun je doen door de FILTER_FLAG_ARRAY optie mee te geven aan de FILTER_VALIDATE_INT filter mee te geven:
Met deze flag zal de filter extensie door de opgegeven array lopen en op elk van de waarden het filter toe passen. Het gedrag blijft hetzelfde dus als we de bovenstaande code zouden uitvoeren krijgen we de volgende output:
-
array(1) {
-
["test"]=>
-
array(4) {
-
[0]=>
-
int(1)
-
[1]=>
-
int(2)
-
[2]=>
-
int(3)
-
[3]=>
-
bool(false)
-
}
-
}
Je zult dus nog wel zelf door de waarden moeten lopen om die FALSE waarde er uit te halen.
Meer specifieke eisen
In sommige gevallen heb je iets meer eisen aan de input dan dat de standaard filter extensie je kan bieden. Gelukkig is daar ook aan gedacht en is er een FILTER_CALLBACK filter toegevoegd waarmee je dus zelf in PHP een filter kunt schrijven. Zo zouden we de username controle die we hierboven met een regular expression kunnen herschrijven naar een callback functie zodat we deze functie makkelijk kunnen hergebruiken in een ander project:
-
function validate_username(&$username, $charset)
-
{
-
}
-
-
'filter' => FILTER_CALLBACK,
-
'options' => 'validate_username'
-
)
-
);
-
-
$filteredData = filter_input_array(INPUT_POST, $velden);
Conclusie
Toen ik de filter extensie voor het eerst zag vond ik het er allemaal nodeloos complex uitzien. Echter, nadat ik het een paar keer gebruikt had bleek toch dat zogauw je de filter namen een beetje in je hoofd hebt zitten je behoorlijk snel formulieren kunt verwerken en dat het allemaal best aardig werkt.
Desondanks vind ik dat er nog een hoop niet helemaal snor zit met deze extensie. Er zitten features in waar ik zo snel het nut niet van zie (filter_id() en FILTER_NULL_ON_FAILURE), de documentatie is niet geweldig en het is een potentieel probleem dat controle op juistheid van bijv. email en URLs in de extensie gebakken zitten. Op het moment dat daar iets fout zit, zit je dus in de problemen totdat je hoster eens upgrade.
Jammer vind ik ook het feit dat je alleen functionaliteit kunt toevoegen via FILTER_CALLBACK aanroepen. Ik had liever gezien dat je zelf echt filters kon toevoegen.
Volg Scriptorama via RSS!
Reageer ook!
Wat ik een beetje mis (als ik dit snel lees) is een array met niet-gevalideerde velden of iets dergelijks?
Door Alexander
op 03.16.08 @ 3:51 pm | Permalink
@alexander: je hebt gelijk. Ik heb iets te snel willen posten en was dat, belangrijke :), deel vergeten te schrijven. Ik heb er iets over toegevoegd! Thanks!
Door Mathieu Kooiman
op 03.16.08 @ 8:06 pm | Permalink
Dit komt voor mij precies als geroepen. Ik stond vanmiddag net op het punt om dit soort dingen met regexes te gaan doen. Bedankt voor het idee! ;)
Door Sander
op 03.16.08 @ 10:13 pm | Permalink
Wederom fraaie, uitgebreide tut Mathieu! :)
FILTER_NULL_ON_FAILURE zal er wel in zitten voor luie apen welke geen onderscheid tussen foute input of afwezige input willen maken... :X
filter_id() vormt een setje met filter_list(), maar het hele nut van een alternatieve filternaam ontgaat me. filter_list() had gewoon een array met FILTER_VALIDATE_INT => 'FILTER_VALIDATE_INT' waarden kunnen zijn als er per se een functie moet zijn welke alle filters list.
Door Maarten
op 03.17.08 @ 9:23 am | Permalink
Na het lezen van bovenstaand moest ik aan onderstaande denken. Misschien een goede aanvulling op dit , weer leerzame, artikel.
http://code.google.com/p/inspekt/
http://c7y.phparch.com/c/entry/1/art,inspekt-introduction_to_inspekt
Door Marten
op 03.17.08 @ 9:51 am | Permalink
Ideale dingen dit ;)
Door Mike
op 04.28.08 @ 10:46 am | Permalink
"en het is een potentieel probleem dat controle op juistheid van bijv. email en URLs in de extensie gebakken zitten."
Waarvan akte. In 5.2.0 en 5.2.1 was de regexp van de emailcontrole niet goed: newlines aan het eind van de string waren toegestaan, hetgeen headerinjectie mogelijk maakte. mopb #45
Beter van niet, denk ik zo...
Door Ronald
op 05.04.08 @ 12:15 am | Permalink
if (isset($_GET['id']) && ctype_digit($_GET['id'])) {
}
herschrijven naar
$itemId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if (!is_null($itemId) && $itemId !== FALSE) {
}
Met de filter methode is de waarde 01 niet juist maar met ctype_digit wel. Om exact dezelfde werking te verkrijgen kan er gebruik maakt worden van een callback filter.
filter_input(INPUT_GET, 'id', FILTER_CALLBACK, array('options' => 'ctype_digit'))
Door Remco Tolsma
op 02.04.09 @ 2:55 pm | Permalink
Een ander groot voordeel van de filter extensie is dat je van het gedoe met magic_quotes (http://www.php.net/manual/en/security.magicquotes.php) af bent.
$name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING);
var_dump($name);
$name = $_POST['name'];
var_dump($name);
Als magic_quotes aan staan is dit het resultaat:
string(12) "Tolsma's"
string(9) "Tolsma\'s"
Door Remco Tolsma
op 08.18.09 @ 1:35 pm | Permalink
Line and paragraph breaks automatic, e-mail address never displayed, HTML allowed:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>