(Nie)bezpieczny kod – SQL injection

SQL injection jest techniką ataku starą jak świat. Mimo tego wciąż jest niestety dość powszechną i prostą metodą ataku na strony www (nawet na te największe jak portale). Dzieje się tak, ponieważ przy tej metodzie najsłabszym ogniwem jest programista. Zobacz na czym polega ten atak i jak się przed nim zabezpieczyć.

Trochę teorii

SQL injection polega na wstrzyknięciu kodu sql w zapytanie wysyłane do bazy danych. Dzięki użyciu znaków specjalnych (apostrofy, cudzysłowy itp), można dowolnie zmodyfikować treść zapytania.

Przykładowy atak SQL injection

No to teorii byłoby na tyle :). Myślę, że analiza przykładowego ataku pokaże o co chodzi w SQL injection. Jako przykład posłuży nam prosty skrypt logowania użytkownika.

Na początek przygotuj strukturę bazy danych:

CREATE TABLE admin (
  id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  login VARCHAR(250) NOT NULL,
  password VARCHAR(250) NOT NULL
)
ENGINE=InnoDB;
INSERT INTO `admin` (
`id` ,
`login` ,
`password` 
)
VALUES (
NULL , 'admin1', 'haslo1'
); 
INSERT INTO `admin` (
`id` ,
`login` ,
`password` 
)
VALUES (
NULL , 'admin2', 'haslo2'
); 

Jak widać nie ma tu nic odkrywczego. Tworzę tabelę admin zawierającą 3 kolumny (id, login, password) i dodaję 2 administratorów. Tak wygląda gotowa struktura bazy danych:

Zrzut bazy danych

Baza danych gotowa, teraz pora na formularz:

<html>
<head>
    <title>SQL injection</title>
</head>
<body>
<form action="" method="post">
    Login: <input type="name" name="login" /><br/>
    Password: <input type="name" name="password" /><br/>
    <input type="submit" value="Login"/>
</form>
</body>
</html>

Jest to standardowy formularz HTML z dwoma polami: loginem i hasłem. 99% formularzy do logowania tak wygląda.

Baza i formularz gotowe, pora na skrypt PHP:

<?php
$dbh = new PDO('mysql:host=localhost;dbname=***', '***', '***');
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); 
if(isset($_POST)) {
    $login=$_POST['login'];
    $password=$_POST['password'];
    $user=$dbh->query("SELECT * from admin WHERE login='".$login."' AND password='".$password."'");
if($user->fetchColumn()) {
    echo 'Zalogowany';
} else {
    echo 'Niezalogowany';
}
    echo '<br/>';
    var_dump($user->queryString);
    var_dump($_POST);
}
?>

Do połączenia z bazą danych korzystam z biblioteki PDO. Jeżeli jej nie znasz odsyłam do dokumentacji. Najbardziej istotnym fragmentem jest:

$user=$dbh->query("SELECT * from admin WHERE login='".$login."' AND password='".$password."'");

Jest to bardzo proste zapytanie z dwoma warunkami. Zwróć uwagę, że wartość zmiennych pochodzi bezpośrednio z tablicy $_POST.

Za pierwszym razem próbuję zalogować się na dane: login: admin, password: hasla nie znam. Jak widać poniżej, skrypt zachowuje się zgodnie z oczekiwaniami. Podałem błędne dane, a więc nie loguje.

SQL injection - użytkownik niezalogowany

Co będzie w sytuacji, gdy wpiszę login: admin, password: ‚ OR 1 = ‚1? Powinno mnie nie zalogować, ale…

SQL injection - użytkownik zalogowany

jak widać zalogowałem się bez problemu. Istotą tego ataku jest warunek logiczny OR 1=1. Wykorzystując fakt, że mogę do zapytania wstrzyknąć dowolny kod (wraz z apostrofami) stwarzam sytuację, gdy dane zapytanie jest zawsze prawdziwe – prawda/fałsz LUB prawda zawsze zwróci prawdę.

Oczywiście nic nie stoi na przeszkodzie, by wstrzyknąć inny kod SQL, na przykład usuwający dane z jakieś tabeli.

Jak się zabezpieczyć przed SQL injection?

Na szczęscie istnieje prosty sposób na zabezpieczenie się przed tym atakiem. Wystarczy przed kazdym znakiem specjalnym dodać znak „\”. Do tego celu PDO udostępnia odpowiednie metody.

<?php
$dbh = new PDO('mysql:host=localhost;dbname=***', '***', '***');
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); 
if(isset($_POST)) {
    $login=$_POST['login'];
    $password=$_POST['password'];
    $user=$dbh->prepare("SELECT * from admin WHERE login=:login AND password=:password");
    $user->bindValue('login', $login, PDO::PARAM_STR);
    $user->bindValue('password', $password, PDO::PARAM_STR);
    $user->execute();
if($user->fetchColumn()) {
    echo 'Zalogowany';
} else {
    echo 'Niezalogowany';
}
    echo '<br/>';
    var_dump($user->queryString);
    var_dump($_POST);
}
?>

Jeżeli korzystasz z PDO wystarczy każdą zmienną przekazać do zapytania za pomocą metody bindValue(). Metoda ta przeparsuje odpowiednio wartość zmiennej. Gdy wpiszę teraz login: admin, password: ‚ OR 1 = ‚1 skrypt zwróci:

Załatany skrypt przed SQL injection

Czyli zachowuje się zgodnie z przewidywaniami – podano błędne dane, a więc nie loguje użytkownika. Jeżeli podam poprawne dane skrypt zaloguje użytkownika.

Zalogowany użytkownik w skrypcie załatanym przed SQL injection

Kompletny kod wygląda następująco:

<?php
$dbh = new PDO('mysql:host=localhost;dbname=***', '***', '***');
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); 
if(isset($_POST)) {
    $login=$_POST['login'];
    $password=$_POST['password'];
    // Zapytanie "dziurawe"
    //$user=$dbh->query("SELECT * from admin WHERE login='".$login."' AND password='".$password."'");
    $user=$dbh->prepare("SELECT * from admin WHERE login=:login AND password=:password");
    $user->bindValue('login', $login, PDO::PARAM_STR);
    $user->bindValue('password', $password, PDO::PARAM_STR);
    $user->execute();
if($user->fetchColumn()) {
    echo 'Zalogowany';
} else {
    echo 'Niezalogowany';
}
    echo '<br/>';
    var_dump($user->queryString);
    var_dump($_POST);
}
?>
<html>
<head>
    <title>SQL injection</title>
</head>
<body>
<form action="" method="post">
    Login: <input type="name" name="login" /><br/>
    Password: <input type="name" name="password" /><br/>
    <input type="submit" value="Login"/>
</form>
</body>
</html>

Uwaga: informacje przedstawione w artykule służą tylko celom edukacyjnym. ZABRONIONE jest wykorzystywanie informacji przedstawionych w artykule do celów niezgodnych z prawem. Autor nie ponosi odpowiedzialności za ewentualne szkody.

Podoba ci się wpis? Udostępnij go znajomym! :)

Print Friendly, PDF & Email