Aspekte të sigurisë në PHP

Çdo zhvillues i Web aplikacioneve gjithmonë duhet të niset nga fakti se Web-i është ambient i pasigurtë dhe se çdo input nga cilido burim - është “i ndotur”. Nuk ka rëndësi nëse inputi ka ardhur nga HTTP requests (GET, POST, COOKIE…), apo është lexuar nga databaza apo nga ndonjë fajll. Në të gjitha rrethanat, inputi fillimisht duhet të “pastrohet” (sanitize) dhe të validohet (validate), para se të procedohet më tutje me të.

error_reporting

display_errors, log_errors, error_log

$_REQUEST - Rasti 1

Një ndër masat parandaluese më bazike është verifikimi i burimit të inputit. Nuk bën të procedojmë me një input nëse ai vie nga një burim tjetër nga ai që ne presim. E ilustrojmë këtë me rastin e superglobalit $_REQUEST.

Superglobali $_REQUEST është një varg (array) që në vete përmban vlerat e dërguara me $_POST, $_GET dhe $_COOKIE. Nëse e kemi një formular si vijon:

login.php

1 <form action='test.php' method="post">
2   <label for="fjalekalimi">Fjalekalimi:</label>
3   <input type="password" name="fjalekalimi">
4   <input type="submit" value="Submit">
5 </form> 

shohim se metoda e dërgimit është POST dhe se dërgohet fusha e quajtur “fjalekalimi”.

Skripta që e lexon këtë vlerë, mund të duket kështu:

check.php

1 <?php
2 $f = $_POST['fjalekalimi'];
3 ?>

Por, në dukje efekti i njëjtë arrihet edhe me:

1 <?php
2 $f = $_REQUEST['fjalekalimi'];
3 ?>

Ku qëndron dallimi? Dallimi ndërmjet versionit të parë të check.php dhe atij të dytë qëndron në faktin se të dytës mund t’i japim vlerë edhe me GET, gjegjësisht me URL, pa pasur nevojë ta plotësojmë formularin në login.php:

1 http://domaini.com/check.php?fjalekalimi=abc5847

Tash, sulmuesi e ka shumë më lehtë të provojë kombinime të ndryshme të fjalëkalimit, vetëm duke i ndryshuar vlerat në URL. Apo, nëse është në pyetje çmimi i një produkti - të manipulojë me të në URL.

$_REQUEST - Rasti 2

Në të njëjtën mënyrë mund të manipulohet edhe me përmbajtjen e cookie, nëse nuk e kemi konfiguruar PHP-në si duhet. Nëse në php.ini, në direktivën request_order e shënojmë vlerën “CGP” (COOKIE-GET-POST), e jo “GPC” (GET-POST-COOKIE) si është vlera standarde, është i mundur ekzekutimi i skriptave vijuese.

test.php

1 <?php
2 setcookie("anetari", "123456", time() + 3600);
3 header("Location: test2.php");
4 ?>

test2.php

1 <?php
2 $s = $_REQUEST['anetari'];
3 echo $s;
4 ?>

Me një link si ky:

1 http://domaini.com/test.php

që redirekton në test2.php, ku më pas e shtojmë një query string: ?anetari=54847

1 http://domaini.com/test2.php?anetari=54847

do të ndryshohet vlera e asaj që është dashur të lexohet nga cookie me emrin “anetari”, vlerë kjo që më pas do të regjistrohet në databazë, apo do të përdoret për procesim të mëtutjeshëm. Kjo për shkak se i kemi dhënë prioritet metodës GET në krahasim me atë COOKIE për leximin e vlerës së variablit me emër të njëjtë.

Por, është i mundur edhe skenari i dytë, që s’ka lidhje me request_order: që skripta test2.php të thirret direkt, me URL-në e cekur më sipër, por tash në incognito mode apo në browser tjetër, apo duke i fshirë paraprakisht cookies në browserin që jemi duke e përdorur.

Në këtë situatë, skripta “nuk është në dijeni” se vlera e kërkuar duhet të kërkohet në cookie sepse ai cookie as që ekziston, prandaj me $_REQUEST e lexon vlerën e pranuar me GET, pra nga URL-ja. Kështu, me një manipulim të thjeshtë në URL, do të jemi në gjendje ta ndryshojmë vlerën e cookie, me pasoja në rrjedhën e mëtutjeshme të programit, varësisht prej asaj se për çka do të përdoret ajo vlerë.


Vërejtje: Në konfigurimet e reja të PHP, është pamundësuar leximi i cookie nga $_REQUEST.

php.ini:

1 request_order = "GP"

Shohim se në $_REQUEST lejohet vetëm GET (G) DHE POST (P), ndërsa COOKIE (C) mungon.

Request order përcakton renditjen e leximit të vlerave, në rastet kur variabli në GET ka emër të njëjtë me atë në POST.

Pra, nëse në një formular e kemi fushën “emaili”, dhe skriptën që e proceson atë formular e thirrim me një URL që ka në fund një query string si kjo:

1 ?emaili=adresa@domaini.com

atëherë, $_REQUEST në skriptën pranuesi i ka dy vlera të quajtura “emaili”, dhe njëra nga këto duhet të eliminohet.

Me request_order = “GP” i jepet përparësi variablit që ka ardhur me metodën POST, pra ajo i bën overwrite variablit me emrin e njëjtë që ka ardhur me metodën GET. Me këtë mundësohet që vlerat e dërguara me formular, të mos “mbulohen” me vlera të shënuara në URL.

Eliminimi i cookie nga request_order nuk e parandalon skenarin e dytë që u cek më sipër.


Konkluzion: nëse presim që një vlerë do të vie me metodën GET - e lexojmë me $_GET, atë me POST e lexojmë me $_POST, dhe në fund, inputet nga COOKIE do t’i lexojmë vetëm me $_COOKIE. Në asnjë mënyrë nuk duhet të mbështetemi në $_REQUEST për shkak të situatave të përmendura më lartë.

Client-side validation

Shumë Web developerë nuk i japin aq fort rëndësi validimit të të dhënave në PHP, por mjaftohen me masat e ndërmarra në front-end: HTML dhe JavaScript.

Për shembull:

  • Nëse është plotësuar një fushë
  • Nëse emri ka minimalisht x shkronja,
  • Nëse një vlerë numerike është brenda një intervali të paracaktuar, etj.

Në HTML5, te fushat që duhet patjetër të plotësohen shtohet atributi required, me çka do të pamundësohet dërgimi i vlerave të fushave të formularit në server. Por, kjo “mbrojtje” është e dobishme vetëm si përkujtues vizitorëve të faqes se kanë harruar ta plotësojnë ndonjë fushë të caktuar, dhe vlera e asaj fushës duhet të verifikohet edhe njëherë: kur të arrijë në server, gjegjësisht në skriptën që e proceson atë formular.

Kjo për shkak se atributi required lehtë mund të largohet me “Inspect element” në browser.

JavaScript (e me të edhe jQuery) mund të deaktivizohet nga browseri tërësisht, duke pamundësuar çfarëdo client-side validation.

Në këto rrethana, të dhënat që i dërgohen serverit do të jenë tërësisht të pavaliduara dhe skripta jonë nuk guxon t’i marrë ashtu si janë, por duhet së pari t’i validojë dhe pastrojë, para se të procedohet më tutje me to.

Pra, validimi me HTML apo JavaScript duhet të konsiderohet vetëm si një lehtësim për vizitorin për t’i parandaluar gabimet e mundshme gjatë shënimit të të dhënave, por assesi si të dhëna tashmë të verifikuara. Verifikimet e bëra në client-side, duhet të përsëriten edhe në server-side.

Kodi në JavaScript/jQuery:

1 var emri = $('#emri').val()
2 if (emri.length < 3)
3 	{
4 	alert("Minimum 3 shkronja për emrin!")
5 	}

duhet ta ketë ekuivalentin e vet edhe në PHP skriptën tonë:

1 $emri = $_POST['emri'];
2 if (strlen($emri) < 3)
3 	{
4 	echo "Minimum 3 shkronja për emrin!";
5 	}

Një mënyrë tjetër e eliminimit të client-side validation arrihet me ruajtjen e faqes me formular në disk, pra duke zgjedhur “Save As” në browser e duke e ruajtur faqen për shembull në Desktop. Pasi të jetë ruajtur, e hapim faqen e formularit me një tekst editor dhe fshijmë gjithçka që ka të bëjë me validim, si në HTML, ashtu edhe në JavaScript. Për të na funksionuar ky formualr, mjafton që në action ta shënojmë URL-në e plotë, pra në vend të:

1 <form action="test.php" method="POST">

shkruajmë:

1 <form action ="http://domaini.com/test.php" method="POST"> 

apo duke ia shënuar emrin e folderit përpara emrit të skriptës, nëse skripta gjendet brenda ndonjë folderi. Me këtë modifikim, ne do të jemi në gjendje të dërgojmë të dhëna skriptës test.php nga faqja që e kemi ruajtur në kompjuterin tonë!

Anti-CRSF token

Për t’u mbrojtur nga situata të tilla, ne duhet formularëve t’ua shtojmë nga një fushë të tipit hidden, në të cilin do të vendosim një vlerë të rastësishme (anti-CRSF token). Ajo vlerë njëkohësisht ruhet edhe në një variabël sesioni. Skripta që i lexon vlerat nga formulari, duhet ta krahasojë vlerën që vjen nga fusha “e fshehtë” me vlerën e ruajtur në variablin e sesionit. Nëse këto vlera përputhen, kjo do të thotë se formulari është plotësuar nga faqja e pamodifikuar. Në të kundërtën, skripta është furnizuar me të dhëna nga një faqe e modifikuar, nga një skriptë ku manipulimet bëhen me PHP, apo duke u përdorur ndonjë metodë tjetër, si psh me përdorimin e bibliotekës cURL (http://curl.haxx.se/), i cili në mënyrë programore mund të dërgojë POST request.

form.php

 1 <?php
 2 session_start();
 3 $crsftoken =  md5(uniqid(rand(), true));
 4 $_SESSION['token'] = $crsftoken;
 5 ?>
 6 <form action='formcheck.php' method="post">
 7   <label for="emri">Emri:</label>
 8   <input type="text" name="emri">
 9   <input type="hidden" name="token" value="<?=$crsftoken;?>">
10   <input type="submit" value="Submit">
11 </form>

formcheck.php

 1 <?php
 2 session_start();
 3 $token = $_POST['token'];
 4 
 5 if ($_SESSION['token'] != $token)
 6 	{
 7 		die("Input jovalid!");
 8 	}
 9 	
10 $emri = $_POST['emri'];
11 echo $emri;

Nëse formularin do ta kishim të ruajtur lokalisht, ose nëse do të përdornim cURL, vlera e fushës crsftoken do të ishte e zbrazët, kështu që me krahasimin e asaj vlere me vlerën e ruajtur në $_SESSION[‘token’] do të konstatohet se “origjina” e faqes së formularit nuk është serveri ynë.

Edhe sikur t’i jepnim ndonjë vlerë asaj fushe, gjasat janë tejet të vogla, mos të thënë inekzistente, se do ta kishte vlerën e hash-it të një numri të rastësishëm, të cilën e gjeneron serveri, secilën herë me vlerë tjetër, sa herë që hapet faqja e formularit.

Duhet të theksohet se kjo mbrojtje ka kuptim vetëm për formularët që janë brenda faqeve të autentikuara me $_SESSION! Nëse faqet janë publike dhe tokeni i pandryshueshëm, atëherë sulmuesi mund ta gjejë tokenin thjesht duke e lexuar nga “View Page Source”.

Ruajtja e tokenit si variabël sesioni i ka edhe të metat e veta, nëse atë e gjenerojmë sa herë hapet formulari. Kjo vie në shprehje kur e hapim formularin e njëjtë disa herë në browser tabs të ndryshëm. Në këtë situatë, kur hapet tab-i i dytë, vlera e variablit të sesionit ndryshohet, pra zëvendësohet me një vlerë të re. Kur kthehemi te tab-i i parë për ta dërguar formularin, skripta do ta konsiderojë si CRSF sepse tokeni i ruajtur si fushë e fshehtë në formular nuk përputhet me vlerën e tokenit në variablën e sesionit.

Elaborimi i mëtejmë i skenarëve të mundshëm për zgjidhjen e këtyre situatave i tejkalon suazat e këtij manuali.

Dispatch Method

Kjo ka të bëjë me vetë arkitekturën e aplikacionit, ku vetëm një skriptë thirret nga URL-ja, ndërsa, varësisht nga parametrat e dërguar me GET, ajo skriptë cakton cila skriptë do të inkludohet/ekzekutohet.

Shembull i URL-së së një aplikacioni të bazuar në dispatch method:

1 http://domaini.com/index.php?cmd=kontakti

Gjatë gjithë kohës, thirret vetëm dispatch.php, ndërsa brenda saj bëhen thirrjet e skriptave të tjera.

 1 switch ($_GET['cmd'])
 2 		{
 3 			case 'login':
 4 				require 'login.php';
 5 				break;
 6 			case 'check':
 7 				require 'check.php';
 8 				break;	
 9 .
10 .
11 .

Duke i analizuar inputet me switch, ne efektivisht jemi duke i eliminuar inputet që nuk janë të numëruara në case, duke parandaluar kështu inkludimin arbitrar të skriptave, si këtu:

1 requre($_GET['cmd'].".php");

ku do të tentohej inkludimi i cilitdo fajll që ceket në query string të URL-së, e ku mund të ishte ndonjë fajll me të dhëna sensitive, apo edhe një URL i një serveri tjetër (nëse direktiva allow_url_fopenphp.ini është On, e që by default edhe është. Po të mundësohej inkludimi i një skripte nga një URL jashtë domainit tonë, pasojat mund të jenë shkatërrimtare sepse sulmuesi aty mund të fusë kod i cili do të “akomodohej” në serverin tonë, ku do të mund të ndërmerrte çfarëdo veprimi, si psh: nxjerrja e të dhënave, fshirja e databazës, etj.

Kjo qasje, në aspektin e sigurisë e ka një avantazh të madh: të gjitha masat e sigurisë që janë të domosdoshme të ndërmerren për të gjitha skriptat, mund të implementohen brenda këtij fajlli të vetëm. Nëse ato masa do t’i zbatonim veç e veç nëpër të gjitha skriptat, do të na paraqiteshin gabime aty-këtu, sepse diku mund të harrojmë të shtojmë atë që kemi shtuar në skriptat tjera.

Duke e centralizuar logjikën e sigurisë, ne e thjeshtësojmë punën tonë në skriptat tjera, te të cilat do të jetë e nevojshme vetëm të fokusohemi në masat e sigurisë që janë specifike për ato skripta.

I tërë aplikacioni ynë është i bazuar në dispatch method.

Parandalimi i thirrjes direkte të skriptave

Nëse aplikacionin tonë e kemi organizuar sipas “dispatch method”, ne duhet të ndalojmë thirrjen direkte të skriptave, sepse thirrja e tyre duhet të bëhet vetëm në mënyrë të centralizuar nga index.php.

Kjo realizohet në atë mënyrë që në index.php definohet një konstantë, së cilës i jepet një vlerë çfarëdo, psh:

1 define("THIRRJA", 1);

Kemi përdorur konstantë e jo variabël, në mënyrë që vlera e saj mos të ndryshohet aksidentalisht gjatë rrjedhës së ekzekutimit të programit.

Tash, në fillim të secilës skriptë që inkludohet në index.php, duhet të vendoset rreshti i cili verifikon se a ekziston ajo konstantë:

1 <?php if (!defined('THIRRJA')) exit('Nuk keni qasje direkte'); ?>

Pra, nëse kjo skriptë thirret me:

1 http://domaini.com/?cmd=kontakti

atëherë skripta kontakti.php do të inkludohet dhe ekzekutohet, sepse, duke qenë brenda fajllit index.php, ajo do ta “shohë” vlerën e konstantës THIRRJA.

Mirëpo, nëse e njëjta skriptë thirret me:

1 http://domaini.com/kontakti.php

ajo nuk do të ekzekutohet sepse, duke qenë jashtë index.php, ajo nuk është në gjendje ta lexojë konstantën THIRRJA, dhe kështu që plotësohet kushti if (!defined('THIRRJA')), pra që konstanta THIRRJA nuk është e definuar, në mënyrë që më pas të ekzekutohet exit('Nuk keni qasje direkte'), e cila e ndërpren ekzekutimin e skriptës.

Kufizimi i variablave të inputit

Gjatë leximit të vlerave të inputit, ne nuk duhet të lejojmë që çdo vlerë e arritur të përdoret automatikisht nga skripta. Psh. në rastin e $_POST, do t’i lexojmë vetëm vlerat e fushave të pritshme, psh në login na intereson email adresa dhe fjalëkalimi, gjersa fushat tjera eventuale nuk na interesojnë.

1 <?php
2 $emaili = $_POST['emaili'];
3 $fjalekalimi = $_POST['fjalekalimi'];
4 ?>

Qasja e gabuar:

1 <?php
2 foreach ($_POST as $key=>$value)
3 	{
4 	$$key = $value;
5 	}

Me këtë mundësohet që nga çdo anëtar të vargut $_POST të krijohet nga një variabël me emrin e indeksit të vargut e me vlerën e atij anëtari të vargut.

Pra:

  • Për $_POST['emri'], krijohet variabli $emri,
  • Për $_POST['fjalekalimi'], krijohet variabli $fjalekalimi,
  • e kështu me radhë, deri te anëtari i fundit i $_POST

Po prej nga do të vinin anëtarët e tjerë të $_POST, kur në formular i kemi vetëm dy fusha? Do të vinin nga një formular i modifikuar, apo një skriptë e sulmuesit.

Ja një shembull se si mund të ndërrohen vlerat e variablave të skriptës, duke u ndikuar nga vlerat e lexuar nga $_POST, me konverzion automatik si në shembullin e mësipërm:

 1 <?php
 2 $level = $_SESSION['level']; // le të themi është lexuar vlera 1
 3 foreach ($_POST as $key=>$value)
 4 	{
 5 	$$key = $value;
 6 	}
 7 
 8 if ($level == 2)
 9 	{
10 	// Një veprim destruktiv
11 	}

Ku është problemi në këtë rast? Problemi është se sulmuesi mund ta ketë shtuar një fushë me emrin “level” së cilës ia jep vlerën 2, dhe me këtë, $_POST['level'] duke u shndërruar në $level me vlerën 2, do ta mbulojë vlerën paraprake të variablit $level që ka mundur të jetë 1. Kështu, kur analizohet vlera me if ($level == 2), përgjigja do të jetë pozitive, edhe pse në fakt, ai përdorues nuk e ka pasur atë vlerë kur është lexuar vlera nga sesioni. Thënë më qartë, nga një anëtar i thjeshtë u shndërrua në administrator!

Kufizimi i vlerave të inputit

//

Pastrimi i inputit

Çdo input duhet të konsiderohet si potencialisht i rrezikshëm, pa marrë parasysh burimin. Rreziqet që vijnë nga inputi i papastruar ndahen në dy grupe kryesore:

  • XSS (Cross site scripting)
  • SQL injection

Me XSS nënkuptohet futja e kodit potencialisht të rrezikshëm (HTML, JavaScript) brenda tekstit që dërgohet me POST ose GET. Kjo rëndom bëhet kur kemi textarea në të cilat na lejohet të fusim tekst të gjatë.