Validacija i filtriranje inputa (Sigurnost PHP aplikacija – 3.deo)

Ovo je treći u seriji članaka o sigurnosti PHP aplikacija. Pogledajte Sigurnost PHP aplikacija i Osnove sigurnosti (Sigurnost PHP aplikacija – 2.deo)

Trebamo imati u vidu da su svi inputi stringovi (tekstualne vrednost), tako da i ako su one prikazane kao brojne vrednosti oni su i dalje samo stringovi. PHP ima tu mogućnost da radi sa različitim tipovima podataka i da ih menja dinamično, u hodu. Pa tako ako deklarišemo stringovnu promenjivu koja sadrži vrednost “23″, nju možemo sabrati sa bilo kojim brojem.

<?php
$broj = "23" // razlikuje se od $broj = 23;
$novi_broj = $broj + 3; // 26
?>

Ovaj kôd nikako ne predstavlja sigurnosni rizik, već samo jednu karakteristiku PHP-a, dobri programeri uvek moraju imati na umu sa kakvim tipovima podataka rade. To je jedna od osnova pravilne validacije inputa, pa krenimo redom.

Validacija brojnih vrednosti

Validacija brojnih vrednosti je veoma jednostavna i omogućava nam i da radimo sa ispravnim podacima i da veoma jednostavno povećamo sigurnost naše PHP aplikacije. Konkretno, ukoliko imamo stranicu koja na osnovu URL komande, odnosno zadatog ID parametra, ispisuje informacije o proizvodu i ukoliko nismo proverili da se zaista radi o ID parametru koji je brojčana vrednost (u najvećim slučajevima jeste), napadač lako može izazvati greške u radu aplikacije, pa čak i SQL injection, odnosno izmenu samog upita za “dohvatanje” informacija o proizvodu i tako ugroziti sigurnost aplikacije.

Primer takvog kôda može izgledati ovako:

<?php
// www.example.com/proizvod.php?id=15';DELETE FROM proizvodi;--

$id = $_GET['id'];
$sql = "SELECT * FROM proizvodi WHERE id= '$id'";

//...
?>

Jasno je da bi se umesto jednog, izvršila dva upita i $sql promenjiva bi izgledala ovako:

SELECT * FROM proizvodi WHERE id = '15'; DELETE FROM proizvodi; --'

Validacija ovakvog inputa, odnosno ID parametra, bi bila jednostavna. Trebali bi samo da osiguramo da je uneti parametar zaista broj, a da u ostalim slučajevima prikažemo grešku, odnosno nepostojeću stranu. Za tu svrhu možemo iskoristiti is_numeric() funkciju.

if(!is_numeric($_GET['id'])) {
    // prikaži 404 stranicu
}

Međutim, bolji način je kastovanje podataka, odnosno menjanje tipa. Na ovaj način definitivno osiguravamo da radimo sa pravim tipom podatka.

$id = (int) $_GET['id']

if($id == 0) {
    // prikaži 404 stranu
}

Sledeći kôd je ujedno i primer za “best practice” u ovakvim slučajevima:

<?php
// www.example.com/proizvod.php?id=15';DELETE FROM proizvodi;--

// Prvo proveramo da li je uopšte zadat obezan parametar
if(empty($_GET['id'])) {
    // prikaži 404
}

// kastovanje
$id = (int) $_GET['id']; // 15

// optimizacija: sprečavamo nepotreban upit
if($id <= 0) {
   // prikaži 404
}

// sada je upit siguran
$sql = "SELECT * FROM proizvodi WHERE id= '$id'";

//...
?>

Napomena: mysql_query() ne dozvoljava izvršenje više upita odjednom, ali drzajveri za PostgreSQL i SQLite ih podržavaju.

Savet: Operacije sa bazom podataka su najskuplje, u smislu vremena i memorije, pa tako ukoliko nije neophodo da imamo neki upit, ne trebamo ga ni imati. Ovaj primer odlično opisuje da ne trebamo izvršiti upit ukoliko je $id manji ili jednak nuli, jer u našoj bazi verovatno i nećemo imati proizvod sa tim ID.

Validacija teksualnih inputa

Dok je filtriranje brojeva relativno jednostavno, filtriranje tekstualnih inputa je za nijansu komplikovanije. Za neke prostije formate inputa, kao što su poštanski broj, telefon, email adresa i slični možemo koristiti već postojeće PHP funkcije. Ali, pre detaljnog objašnjena, sledeći primer odlično prikazuje važnost filtriranje inputa.

Ukoliko bi smo imali jednostavnu kontakt formu, koju korisnik popunjava svojim ličnim podacima, email adresom i komentarom, jednostavna skripta koja bi izvršavala tu kontakt formu i slala podatke na našu email adresu bi mogla da izgleda ovako:

<?php
// podaci sa forme
$ime = $_POST['ime'];
$email = $_POST['email'];
$tekst = $_POST['tekst'];

// Heder za ispis pošaljioca u mail klijentu
$heder = "From: $ime <$email> \n\r";

// slanje email-a na našu adresu
mail('kontakt@example.com', $tekst, $heder);

?>

Ova skripta će svakako raditi očekivano, ali samo ukoliko verujemo korisniku da će zaista uneti ispravne podatke. Pošto mu ne smemo verovati, napadač veoma lako može iskoristiti ovakvu skriptu za slanje SPAM poruka sa našeg servera. Dovoljno je da umesto svog imena, ili email adrese unese nešto ovako:

example@example.com> \n\r To: <example2@example.com> \n\r Bcc: <example3@example.com

Jasno je da će se vrednost iz $email direktno kopirati u $header, i da će poruka biti poslata na onoliko adresa koliko napadač želi. Važnost zaštite u ovom slučaju je veoma velika, svakako ne bi želeli da se sa našeg servera šalju SPAM poruke, zbog kojih možemo biti označeni kao maliciozni i završiti na nekoj “crnoj listi”.

Još jednom, rešenje ovog i mnogih drugih problema, leži u filtriranju inputa. Pa krenimo redom:

Ctype funkcije

Character type funkcije imaju odlične mogućnosti, a pritom imaju i odlične performanse. Ove funkcije, proveravaju svaki karakter i rezultat će biti TRUE jedino ako svaki karakter zadovoljava postavljeni kriterijum. U suprotom, ukoliko je karakter nedozvoljenog tipa, rezultat će biti FALSE.

Funkcija Opis
ctype_alnum Provera slovnih i brojnih karaktera
ctype_alpha Provera slovnih karaktera
ctype_digit Provera brojnih karaktera
ctype_lower Provera malih slova
ctype_upper Provera velikih slova

Filter funkcije

Filter funkcije imaju dve mogućnosti – da provere string (validate) po postavljenim kriterijumima ili da ga isprave (sanitization) ukoliko ne odgovara kriterijumima. Svakako je preporučljivo koristi samo validaciju, ali i ispravljanje ima svoju široku primenu.

filter_var($var, $filter);

Prvi atribut je vrednost koja se proverava, a drugi predstavlja kriterijume za proveru, a sledeća tabela predstavlja najčešće korišćene kriterijume:

Filter Opis
FILTER_VALIDATE_EMAIL Provera email adrese
FILTER_VALIDATE_INT Provera brojnih vrednosti, sa opcijama min_range i max_range
FILTER_VALIDATE_IP Provera IP adrese
FILTER_VALIDATE_URL Provera ispravne URL adrese

Sledeći primer prikazuje pravilnu validaciju email adrese. Takođe, predstavlja i rešenje za pređašnji primer koji je omogućavao slanje masovnig SPAM poruka:

<?php

$email = $_POST['email'];
if(filter_var($email, FILTER_VALIDATE_EMAIL)) {
    // sada je sigurno poslati mail
}
?>

Regularni izrazi (regular expression, regex)

Regularni izrazi su skup pravila koji se izvršavaju nad određenim podacima u cilju identifikacije karaktera i/ili znakovnih skupova u nekom tekstualnom objektu. Regularne izraze koriste svi programski jezici, imaju relativno dobre performanse i široku primenjivost.

Sledeća tabela prikazuje često korišćene PHP funkcije koje koriste regularne izraze:

Funkcija Opis
preg_match Izvršava regex proveru nad podacima
preg_match_all Pretražuje podatke na osnovu regex i postavlja rezultate u niz, na osnovu zadatih pravila
preg_replace Pretražuje podatke na osnovu regex i pogotke zamenjuje sa drugim podacima

Pošto regularni izrazi i njihovo funkcionisanje nije tema ovog kursa, biće prikazani samo najšeće korišćeni izrazi u proveri podataka:

<?php
// izraz za proveru email adrese
$regex = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/';

if(preg_match($regex, $email)) {
    // ispavna email adresa
}
?>

U sledećoj tabeli su dati često korišćeni i par zanimljivih regularnih izraza:

Izraz Opis
/^[a-z]*$/ Sva mala slova u intervalu od slova a do z
/^[a-zA-Z0-9]*$/ Slovni i brojni znakovi (mala i velika slova i brojevi)
/^[a-fA-F0-9]{32}$/ Format md5 hash vrednosti
/^(5[1-5][0-9]{14})*$/ Format Master kreditne kartice
/^(4[0-9]{12}(?:[0-9]{3})?)*$/ Format Visa kreditne kartice

Za sad toliko o validaciji i filtriranju inputa. U nastavku ovog kursa možete očekivati tekstove na temu XSS-a (Cross Site Scripting) i SQL injection-a.

6 Comments

  1. Sale, vrlo sam prijatno iznenađen ovim tekstom. Naime, pošto sam PHP krenuo da radim pre nekih dve tri nedelje, upravo je sigurnost od napada iskusnih ljudi postala problem, s obzirom da ću prvi svoj CMS sajt izbaciti u roku od možda nedelju dana…

    Kada očekujem nastavak? 🙂 Totalno nisam ni krenuo da rešavam problem SQL injection-a… Mada sam sam svoje sktipte zloupotrebio više puta, tako da realno imam potrebu time da se bavim…

    Hvala na fantastičnom članku!

  2. Nema na čemu, drago mi je da ti se sviđa. Biće još sličnih tekstova, a sledeći je o SQL injectionu. Biće spremljen za par dana…

    Pozdrav!

  3. Pozdrav Sale,

    Bas mi se dopada ovo sto si krenuo da radis. Samo tako nastavi! 😉
    Sve pohvale!

    PS. Imas malu gresku u kodu, kada sprecavas nepotreban upit
    13 if($id >= 0) {
    14 // prikaži 404
    15 }
    Valjda bi trebalo $id <= 0.

    Pozdrav!

  4. Mislim da će ti biti interesantno da znaš da ću vrlo uskoro krenuti sa svojim sajtom koji će sadržati video tutorijale… Pre svega Java, naravno, ali biće svega… 🙂

  5. @urkes, hvala na komentaru i ukazivanju na grešku 🙂

    @Đeka, baci link čim bude online!

  6. Zdravo!

    Dobar članak, ali bih predložio da uz proveru ID-a prilikom preuzimanja sa GET-a uradiš još i ovo:

    $provera = mysql_query(“select * from IME TABELE where id=’$id'”);

    if (($id == “”)) {
    Header(“Location: ./”);
    }
    else if (mysql_num_rows($provera) == 0) {
    Header(“Location: ./”);
    }

    Na ovajnačin si siguran da neko ne može upisati nepostojeću vrednost ID-a …

    Inače koristim cleanQuery funkciju kojom “čistim” preuzeto sa geta vrlo jednostavno:

    $id = cleanQuery($_GET[‘id’]);

    Pozdrav!