Шта је СКЛ ињекција и како спречити у ПХП апликацијама?

Dakle, smatrate da je vaša SQL baza podataka efikasna i zaštićena od iznenadnog uništenja? Pa, SQL Injection se sa tim ne slaže!

Da, govorimo o trenutnom uništenju, jer ne želim da započnem ovaj članak standardnom, već izlizanom terminologijom o „poboljšanju bezbednosti“ i „sprečavanju zlonamernog pristupa“. SQL Injekcija je toliko star trik da svaki programer zna za njega i kako ga sprečiti. Ipak, ponekad se desi da im promakne, a posledice mogu biti katastrofalne.

Ako već znate šta je SQL Injekcija, slobodno pređite na drugu polovinu teksta. Ali, za one koji su novi u svetu veb razvoja i žele da napreduju, kratak uvod je neophodan.

Šta je SQL Injekcija?

Ključ za razumevanje SQL Injekcije leži u samom nazivu: SQL + Injection. Reč „injection“ ovde nema medicinsku konotaciju, već se odnosi na glagol „ubrizgati“. Zajedno, ove dve reči prenose ideju o umetanju SQL-a u veb aplikaciju.

Ubacivanje SQL-a u veb aplikaciju. Hmmm… Zar to već ne radimo? Da, ali ne želimo da napadač kontroliše našu bazu podataka. Razjasnićemo to na primeru.

Recimo da pravite tipičnu PHP veb stranicu za lokalnu onlajn prodavnicu i odlučili ste da dodate kontakt formu kao što je ova:

<form action="record_message.php" method="POST">
  <label>Vaše ime</label>
  <input type="text" name="name">
  
  <label>Vaša poruka</label>
  <textarea name="message" rows="5"></textarea>
  
  <input type="submit" value="Pošalji">
</form>

Pretpostavimo da datoteka send_message.php sve informacije čuva u bazi podataka kako bi vlasnici prodavnice mogli kasnije da pročitaju poruke korisnika. Kod bi mogao izgledati ovako:

<?php

$name = $_POST['name'];
$message = $_POST['message'];

// provera da li ovaj korisnik već ima poruku
mysqli_query($conn, "SELECT * from messages where name = $name");

// Ostatak koda ovde

Dakle, prvo proveravate da li taj korisnik već ima nepročitanu poruku. Upit SELECT * from messages where name = $name deluje jednostavno, zar ne?

POGREŠNO!

U svojoj nevinosti, otvorili smo vrata uništenju naše baze podataka. Da bi se to desilo, napadač mora da ispuni sledeće uslove:

  • Aplikacija koristi SQL bazu podataka (danas je to slučaj kod skoro svih aplikacija).
  • Trenutna veza sa bazom podataka ima prava „izmene“ i „brisanja“.
  • Nazivi važnih tabela se mogu pogoditi.

Treća stavka znači da, pošto napadač zna da imate internet prodavnicu, vrlo je verovatno da podatke o narudžbinama čuvate u tabeli „narudžbine“. Naoružan time, napadač samo treba da unese sledeće kao svoje ime:

Joe; truncate orders;? Da, gospodine! Hajde da vidimo kako će upit izgledati kada ga PHP skripta izvrši:

SELECT * FROM messages WHERE name = Joe; truncate orders;

U redu, prvi deo upita ima sintaksnu grešku (nedostaju navodnici oko „Joe“), ali tačka-zarez prisiljava MySQL da počne sa tumačenjem novog upita: truncate orders. Tako, jednim potezom, čitava istorija narudžbina je nestala!

Sada kada znate kako SQL Injekcija funkcioniše, vreme je da pogledamo kako je zaustaviti. Dva uslova koja moraju biti ispunjena za uspešnu SQL Injekciju su:

  • PHP skripta mora imati privilegije za izmenu/brisanje baze podataka. Pretpostavljam da ovo važi za sve aplikacije i da nećete moći da učinite svoje aplikacije samo za čitanje. 🙂 Čak i ako uklonimo sva prava za izmenu, SQL Injekcija i dalje može omogućiti nekom da pokrene SELECT upite i pregleda celu bazu podataka, uključujući i osetljive podatke. Drugim rečima, smanjenje nivoa pristupa bazi podataka ne funkcioniše, a vašoj aplikaciji je ionako potrebno.
  • Korisnički unos se obrađuje. SQL Injekcija je moguća samo kada prihvatate podatke od korisnika. Ponovo, nije praktično da zaustavite sve unose u vašoj aplikaciji samo zbog straha od SQL Injekcije.
  • Sprečavanje SQL Injekcije u PHP-u

    Sada, s obzirom na to da su veze sa bazom podataka, upiti i korisnički unosi deo svakodnevice, kako da sprečimo SQL Injekciju? Srećom, prilično je jednostavno i postoje dva načina: 1) dezinfikovati korisnički unos i 2) koristiti pripremljene izjave.

    Dezinfikujte korisnički unos

    Ako koristite stariju verziju PHP-a (5.5 ili stariju, što je čest slučaj na deljenim hosting serverima), pametno je da propustite sav korisnički unos kroz funkciju koja se zove mysql_real_escape_string(). Ono što ona radi je da uklanja sve specijalne karaktere iz stringa, tako da oni gube svoje značenje kada ih koristi baza podataka.

    Na primer, ako imate string kao što je I’m string, napadač može iskoristiti znak jednostrukog navodnika (‘) da manipuliše upitom baze podataka i izazove SQL Injekciju. Propuštanje kroz mysql_real_escape_string() proizvodi I\’m string, koji dodaje obrnutu kosu crtu ispred jednostrukog navodnika, neutralizujući ga. Kao rezultat toga, ceo string se sada prosleđuje kao bezopasan string bazi podataka, umesto da može da učestvuje u manipulaciji upitima.

    Postoji jedan nedostatak ovog pristupa: to je stara tehnika koja se koristi uz starije načine pristupa bazi podataka u PHP-u. Od PHP 7 ova funkcija više i ne postoji, što nas dovodi do našeg sledećeg rešenja.

    Koristite pripremljene izjave

    Pripremljene izjave su način da se upiti baze podataka učine sigurnijim i pouzdanijim. Ideja je da umesto da šaljemo sirovi upit bazi podataka, prvo joj kažemo strukturu upita koji ćemo poslati. To je ono što se podrazumeva pod „pripremanjem“ izjave. Kada je izjava pripremljena, šaljemo informacije kao parametrizovane unose, tako da baza podataka može da „popuni praznine“ tako što će ih uključiti u strukturu upita koju smo ranije poslali. Ovo oduzima svaku posebnu moć koju ulazni podaci mogu imati, i oni se tretiraju kao obične promenljive (ili koristan teret, ako želite) u celom procesu. Evo kako izgledaju pripremljene izjave:

    <?php
    $servername = "localhost";
    $username = "username";
    $password = "password";
    $dbname = "myDB";
    
    // Kreiranje konekcije
    $conn = new mysqli($servername, $username, $password, $dbname);
    
    // Provera konekcije
    if ($conn->connect_error) {
        die("Konekcija nije uspela: " . $conn->connect_error);
    }
    
    // pripremi i poveži
    $stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)");
    $stmt->bind_param("sss", $firstname, $lastname, $email);
    
    // postavljanje parametara i izvršavanje
    $firstname = "John";
    $lastname = "Doe";
    $email = "[email protected]";
    $stmt->execute();
    
    $firstname = "Mary";
    $lastname = "Moe";
    $email = "[email protected]";
    $stmt->execute();
    
    $firstname = "Julie";
    $lastname = "Dooley";
    $email = "[email protected]";
    $stmt->execute();
    
    echo "Novi zapisi su uspešno kreirani";
    
    $stmt->close();
    $conn->close();
    ?>

    Znam da proces zvuči nepotrebno složeno ako ste novi u pripremljenim izjavama, ali koncept je vredan truda. Evo lepog uvoda.

    Za one koji su već upoznati sa PHP PDO ekstenzijom i koriste je za kreiranje pripremljenih izjava, imam mali savet.

    Upozorenje: Budite pažljivi prilikom podešavanja PDO

    Kada koristimo PDO za pristup bazi podataka, lako možemo upasti u lažan osećaj sigurnosti. „Ah, pa, ja koristim PDO, ne treba da brinem ni o čemu drugom“ – tako uglavnom razmišljamo. Istina je da je PDO (ili MySQLi pripremljene izjave) dovoljan da spreči sve vrste napada SQL Injekcijom, ali morate biti pažljivi prilikom podešavanja. Uobičajeno je samo kopirati i nalepiti kod iz tutorijala ili iz ranijih projekata i nastaviti dalje, ali ovo podešavanje može sve da poništi:

    $dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);

    Ova postavka govori PDO-u da emulira pripremljene izjave, umesto da zaista koristi funkciju pripremljenih izjava baze podataka. Dakle, PHP šalje obične string upite bazi podataka čak i ako vaš kod izgleda kao da kreira pripremljene izjave i postavlja parametre. Drugim rečima, ranjivi ste na SQL Injekciju kao i pre. 🙂

    Rešenje je jednostavno: uverite se da je emulacija podešena na false.

    $dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

    Sada je PHP skripta primorana da koristi pripremljene izjave na nivou baze podataka, sprečavajući sve vrste SQL Injekcija.

    Prevencija korišćenjem WAF-a

    Da li ste znali da možete zaštititi veb aplikacije od SQL Injekcije korišćenjem WAF-a (Web Application Firewall)?

    Ne samo od SQL Injekcije, već i od mnogih drugih ranjivosti 7. sloja kao što su cross-site scripting, neispravna autentifikacija, cross-site request forgery, izloženost podacima itd. Možete koristiti self-hosted rešenja kao što je Mod Security ili rešenja u oblaku, kao što je Cloudflare.

    SQL Injekcija i moderni PHP Framework-ovi

    SQL Injekcija je toliko uobičajena, jednostavna, frustrirajuća i opasna, da svi moderni PHP veb framework-ovi dolaze sa ugrađenim merama zaštite. U WordPress-u, na primer, imamo funkciju $wpdb->prepare(), dok ako koristite MVC framework, on radi sav prljav posao umesto vas i ne morate čak ni da razmišljate o sprečavanju SQL Injekcije. Malo je nezgodno što u WordPress-u morate eksplicitno da pripremate izjave, ali hej, govorimo o WordPress-u. 🙂

    Poenta je da moderan veb programer ne mora da razmišlja o SQL Injekciji, i kao rezultat toga, oni nisu ni svesni mogućnosti. Zato, čak i ako ostave otvoren jedan backdor u svojoj aplikaciji (možda je to parametar upita $_GET i stare navike izvršavanja sirovog upita), posledice mogu biti katastrofalne. Zato je uvek bolje odvojiti vreme da dublje zaronite u temelje.

    Zaključak

    SQL Injection je veoma opasan napad na veb aplikacije, ali ga je lako izbeći. Kao što smo videli u ovom članku, oprez pri obradi korisničkog unosa (SQL Injekcija nije jedina pretnja koju donosi rukovanje korisničkim unosom) i provera baze podataka je sve što je potrebno. Uz to, ne radimo uvek sa bezbednim veb framework-ovima, pa je bolje da budemo svesni ove vrste napada i da ne nasedamo na njega.