Kompletny przewodnik po SQL injection dla developerów PHP (i nie tylko) Krzysztof Kotowicz PHP Developer OWASP 10.03.2010 http://web.eskot.pl Medycyna Praktyczna [email protected] Copyright © The OWASP Foundation Permission is granted to copy, distribute and/or modify this document under the terms of the OWASP License. The OWASP Foundation http://www.owasp.org Plan prezentacji Co to jest SQL injection? Dlaczego SQL injection jest groźne (demo)? Jak się bronić? • Prepared statements • Escape'owanie • Procedury składowane • Metody uzupełniające Podsumowanie OWASP 2 Omawiane bazy danych (RDBMS) MySQL Oracle MS SQL Server W mniejszym stopniu: • PostgreSQL • SQLite OWASP 3 Omawiane projekty PHP PDO – PHP data objects • Wspólny interfejs dla różnych RDBMS Doctrine 1.2 • ORM (Object Relational Mapper) używany m.in. we frameworku Symfony Propel 1.4 • ORM konkurencyjny dla Doctrine • Używany we frameworku Symfony Zend Framework 1.10 • Popularny framework MVC dla PHP MDB2 2.4.1 • Warstwa abstrakcji bazy danych (DBAL) • Dystrybuowany przez PEAR OWASP 4 Co to jest SQL injection? OWASP 5 SQL injection – krótka definicja Jest to rodzaj ataku na aplikacje internetowe. Polega na tym, że dane od użytkownika pochodzące z: URL: www.example.com?id=1 Formularzy: [email protected] Innych elementów: np. cookie, nagłówki HTTP zostają zmanipulowane tak, że w podatnej aplikacji zostaje wykonane „wstrzyknięte” przez atakującego polecenie SQL. OWASP 6 Przykład – formularz logowania SELECT * FROM users WHERE login = '{$login}' and password_hash = MD5('{$password}') $login = "' or 1=1 -- "; $password = "dowolne"; // zamierzalismy osiagnac to (kod \ dane) SELECT * FROM users WHERE login = '' or 1=1 -- ' and password_hash = MD5('dowolne') // serwer interpretuje to tak SELECT * FROM users WHERE login = '' or 1=1 -- ' and password_hash = MD5('dowolne') Użytkownik jest zalogowany bez znajomości loginu ani hasła OWASP 7 Dlaczego jest groźne? DEMO OWASP 8 Czym grozi podatność na SQL injection? Nieuprawniony dostęp do aplikacji Dostęp do całej zawartości bazy / baz na serwerze Denial of service Możliwość modyfikacji danych w bazie Przeczytanie / zapisanie pliku na serwerze Wykonanie kodu na serwerze OWASP 9 Kilka faktów Podatności na injection na pierwszym miejscu OWASP Top 10 2010 RC Odpowiada za 40–60% przypadków wycieku danych [1] [2] Obecne techniki ataku są bardzo zaawansowane i często automatyzowane • Podatność nie tylko w części WHERE • Czasem celem jest zepsucie zapytania Codziennie znajdowane podatności, nawet w nowych aplikacjach OWASP 10 Jak się bronić? OWASP 11 Jak się bronić przed SQL injection? Źródło podatności - łączenie kodu z danymi SELECT * FROM users WHERE login = 'login' Metody obrony Oddzielenie kodu od danych prepared statements stored procedures Escape'owanie danych OWASP 12 Jak się bronić? Prepared statements OWASP 13 Prepared statements – zasada działania 1. Przygotowujemy polecenie SQL (string) W miejsce danych wstawiamy znaczniki WHERE a = ? ... WHERE a = :col 2. 3. 4. Przesyłamy polecenie na serwer Podajemy zestaw danych do polecenia Wykonujemy polecenie 5. Odbieramy rezultat 6. 3, 4, 5 można powtarzać... Czyścimy polecenie PREPARE EXECUTE OWASP 14 Prepared statements - przykład Przykład działania (PDO) // przygotowujemy zapytanie $stmt = $dbh->prepare("INSERT INTO SUMMARIES (name, sum) VALUES (:name, :sum)"); // podajemy wartosci zmiennych – RAZEM Z TYPAMI! $stmt->bindParam(':name', $name, PDO::PARAM_STR); $stmt->bindParam(':sum', $sum, PDO::PARAM_INT); // podajemy wartości zmiennych $name = 'something'; $value = 1234; // wykonujemy zapytanie $stmt->execute(); $stmt = null; //zwalniamy pamiec OWASP 15 Prepared statements - zalety Polecenia SQL są całkowicie oddzielone od przetwarzanych danych Brak możliwości wstrzyknięcia kodu SQL Polecenie SQL jest przez serwer kompilowane tylko raz – potencjalne zwiększenie wydajności zapytań $stmt->bindParam(':name', $name, PDO::PARAM_STR); $stmt->bindParam(':sum', $sum, PDO::PARAM_INT); // petla po danych... foreach ($do_bazy as $name => $value) { $stmt->execute(); } OWASP 16 Prepared statements - uwagi Nie wszystkie typy poleceń można parametryzować Nie w każdym miejscu polecenia można wstawić parametr -- blad SELECT * FROM :tabela SELECT :funkcja(:kolumna) FROM :widok -- nie tego się spodziewacie SELECT * FROM tabela WHERE :kolumna = 1 SELECT * FROM tabela GROUP BY :kolumna Samo ich użycie nie wymusza stosowania parametrów Czasem są emulowane (ale to dobrze!) OWASP 17 Prepared statements w Doctrine Używa PDO Zamiast SQL używa własnego języka – DQL (emulacja dla Oracle) i prepared statements $q = Doctrine_Query::create() ->select('u.id') ->from('User u') ->where('u.login = ?', ‘mylogin'); echo $q->getSqlQuery(); // SELECT u.id AS u__id FROM user u // WHERE (u.login = ?) $users = $q->execute(); OWASP 18 Prepared statements w Doctrine cd. Wciąż można „wpaść” $q = Doctrine_Query::create() ->update('Account') ->set('amount', 'amount + 200') ->where("id > {$_GET['id']}"); Trzeba poprawić na: ->where("id > ?", (int) $_GET['id']); NIGDY nie umieszczaj danych wejściowych bezpośrednio w treści zapytań OWASP 19 Prepared statements w Propel Podobnie jak Doctrine, oparty na PDO // poprzez Criteria $c = new Criteria(); $c->add(AuthorPeer::FIRST_NAME, "Karl"); $authors = AuthorPeer::doSelect($c); // poprzez customowy SQL (czasem jest latwiej) $pdo = Propel::getConnection(BookPeer::DATABASE_NAME); $sql = "SELECT * FROM skomplikowany_sql JOIN cos_jeszcze_gorszego USING cos_tam WHERE kolumna = :col)”; $stmt = $pdo->prepare($sql); $stmt->execute(array('col' => 'Bye bye SQLi!'); OWASP 20 Prepared statements w Zend Framework PDO (+ mysqli + oci8 + sqlsrv) // prepare + execute $stmt = $db->prepare('INSERT INTO server (key, value) VALUES (:key,:value)'); $stmt->bindParam('key', $k); $stmt->bindParam('value', $v); foreach ($_SERVER as $k => $v) $stmt->execute(); // prepare + execute w jednym kroku $stmt = $db->query('SELECT * FROM bugs WHERE reported_by = ? AND bug_status = ?', array('goofy', 'FIXED')); while ($row = $stmt->fetch()) echo $row['bug_description']; OWASP 21 MDB2 Oparty na konkretnych sterownikach baz danych (mysql, oci8, mssql, ...) Emuluje PS, jeśli baza ich nie wspiera $types = array('integer', 'text', 'text'); $stmt = $mdb2->prepare('INSERT INTO numbers VALUES (:id, :name, :lang)', $types); $data = array('id' => 1, 'name' => 'one', 'lang' => 'en'); $affectedRows = $stmt->execute($data); $stmt->free(); OWASP 22 Prepared statements - podsumowanie Oferują bardzo dobre zabezpieczenie (jeśli użyte poprawnie) Łatwe w użyciu, niewielkie zmiany w kodzie Dobre wsparcie we frameworkach Mają swoje ograniczenia Czasem muszą być uzupełniane innymi metodami zabezpieczeń OWASP 23 Jak się bronić? Escape'owanie danych OWASP 24 Escape'owanie – zasada działania Dane i polecenia wciąż trzymamy w jednej zmiennej, ale zabezpieczamy je Liczby • Rzutowanie na (int) / (float) – nie is_numeric [1]! Teksty - zwykle otoczone apostrofami: ' .. WHERE pole = 'DANE TEKSTOWE' AND ... • Jeśli w tekście również są apostrofy, trzeba je odróżnić od apostrofu „kończącego” • Apostrof wewnątrz danych jest poprzedzany znakiem specjalnym, np. "\" • Reguły escape'owania zależą od kontekstu! OWASP 25 Escape'owanie – kontekst addslashes() Returns a string with backslashes before characters that need to be quoted in database queries etc. These characters are single quote ('), double quote ("), backslash (\) and NUL (the NULL byte). / Źródło: php.net manual / $user = addslashes($_GET['u']); $pass = addslashes($_GET['p']); $sql = "SELECT * FROM users WHERE username = '{$user}' AND password = '{$pass}'"; $ret = exec_sql($sql); Czy jesteś bezpieczny? OWASP 26 NIE OWASP Escape'owanie – kontekst cd. Różne RDBMS mają różne sposoby escape'owania danych (zależy to też od konfiguracji bazy) addslashes() tylko „przypadkiem” działa dla MySQL RBDMS Funkcja mam 'apostrofy' PDO $pdo->quote($val, $type) n/d (różnie) MySQL (mysql) mysql_real_escape_string mam \'apostrofy\' MySQL (mysqli) mysqli_real_escape_string mam \'apostrofy\' Oracle (oci8) n/d - str_replace() mam ''apostrofy'' SQLite sqlite_escape_string mam ''apostrofy'' MS SQL (mssql) n/d - str_replace() mam ''apostrofy'' PostgreSQL pg_escape_string() mam ''apostrofy'' OWASP 28 Escape'owanie – kontekst cd. // SELECT * FROM users WHERE username = // '{$user}' AND password = '{$pass}' $_GET['u'] = "cokolwiek'"; $_GET['p'] = " or 1=1 -- "; // MySQL widzi to tak: SELECT * FROM users WHERE username = 'cokolwiek\'' AND password = ' or 1=1 -- ' // SQLite / MS SQL / Oracle / PostgreSQL - tak: SELECT * FROM users WHERE username = 'cokolwiek\'' AND password = ' or 1=1 -- ' Nie używaj addslashes(), używaj funkcji konkretnej bazy Czy teraz jesteś bezpieczny? OWASP 29 PRAWIE OWASP Pułapki escape'owania – zestawy znaków Błędy wykryte w 2006 r. w PostgreSQL i MySQL [1] [2] W niektórych wielobajtowych zestawach znaków pomimo escape’owania można doprowadzić do SQL injection \ zostaje „połknięty” przez wielobajtowy znak Przykład: • • • BF 27 [ ¬ ' ] BF 5C 27 [ ¬ \ ' ] Pierwsze dwa bajty to w charsecie GBK znak ¿ Serwer „zobaczy” ciąg ¿' OWASP 31 Pułapki escape'owania – zestawy znaków Podatne są różne azjatyckie zestawy znaków Na szczęście nie UTF-8! W PostgreSQL zastosowano escape'owanie poprzez '' (zamiast \') W mysql_real_escape_string() zastosowano uwzględnianie bieżącego zestawu znaków • Nie zawsze zadziała! [1] [2] Kontekst to również zestaw znaków OWASP 32 Escape'owanie – nazwy obiektów Nazwy kolumn, tabel, baz • • Nie ma dobrej ogólnej metody na ich escape'owanie W różnych bazach różne listy słów zarezerwowanych, różne długości nazw itp. Jeśli musisz pobierać te nazwy od użytkownika, zastosuj whitelisting (blacklisting w ostateczności) OWASP 33 Escape'owanie – nazwy obiektów cd. Przykład – sortowanie po kolumnie Jest podatność w $order, ale nie możesz użyć escape'owania $cat_id = (int) $_GET['cid']; $order = $_GET['column']; $stmt = $pdo->prepare("SELECT * FROM products WHERE cid = :cid ORDER BY $order"); $stmt->bindParam(':cid', $cat_id, PDO::PARAM_INT); if ($stmt->execute()) { ... } OWASP 34 Escape'owanie – nazwy obiektów cd. Whitelisting $columns = array( // lista dozwolonych kolumn 'product_name','cid','price', ); if (!in_array($order, $columns, true)) $order = 'product_name'; // wartosc domyslna Blacklisting // tylko znaki a-z i _ $order = preg_replace('/[^a-z_]/', '', $order); // max 40 znakow $order = substr($order, 0, 40); OWASP 35 Escape'owanie w PDO PDO::quote($value, $type, $len) Długość i typ bywają ignorowane! • • Liczby najlepiej rzutuj na (int), (float) Teksty – obcinaj ręcznie $quoted = $pdo->quote($input, PDO::PARAM_STR, 40); OWASP 36 Escape'owanie w Doctrine Uwaga na Doctrine'owe quote()! $q = Doctrine_Query::create(); // nie tak!!! $quoted = $q->getConnection()->quote($input, 'text'); $q->update('User')->set('username', $quoted); // quote() zamienia ' na '' - exploit (MySQL): $input = 'anything\\\' where 1=1 -- '; // trzeba escape'owac poprzez PDO - getDbh(): $quoted = $q->getConnection() ->getDbh() ->quote($input, PDO::PARAM_STR); // 'anything \\\\\\\' where 1=1 -- ' OWASP 37 Escape'owanie w Propel Poprzez PDO::quote() $pdo = Propel::getConnection(UserPeer::DATABASE_NAME); $c = new Criteria(); $c->add(UserPeer::PASSWORD, "MD5(".UserPeer::PASSWORD.") " ." = " . $pdo->quote($password), Criteria::CUSTOM); OWASP 38 Escape'owanie w Zend Framework Funkcje quote(), quoteInto() $name = $db->quote("O'Reilly"); // 'O\'Reilly' // uproszczone escape'owanie dla jednej zmiennej $sql = $db->quoteInto("SELECT * FROM products WHERE product_name = ?", 'any string'); OWASP 39 Escape'owanie w MDB Funkcja quote() // funkcja quote()- trzeba określić typ $query = 'INSERT INTO table (id, itemname, saved_time) VALUES (' . $mdb2->quote($id, 'integer') .', ' . $mdb2->quote($name, 'text') .', ' . $mdb2->quote($time, 'timestamp') .')'; $res = $mdb2->exec($query); OWASP 40 Escape'owanie danych - podsumowanie Wydaje się proste – zastępowanie tekstu Niestety, tylko się wydaje • • Skłania do stosowania niebezpiecznych konstrukcji • • Musimy znać kontekst (baza danych, charset) Istnieją błędne implementacje sklejanie poleceń ignorowanie zmiennych numerycznych Stosowanie dopuszczalne tylko, jeśli • • Programujemy pod konkretną bazę Nie ma innej możliwości OWASP 41 Jak się bronić? Procedury składowane OWASP 42 Procedury składowane Polecenie SQL (lub seria poleceń) zostaje przeniesione na serwer bazy danych i zapisane jako procedura Po stronie klienta procedura zostaje wywołana z określonymi parametrami (danymi) wejściowymi i wyjściowymi W parametrach wyjściowych klient otrzymuje wyniki procedury Dane są formalnie oddzielone od kodu To NIE wystarcza OWASP 43 Procedury składowane Przykład w MS SQL – fragment podatnej procedury CREATE PROCEDURE SP_ProductSearch @prodname varchar(400) AS DECLARE @sql nvarchar(4000) SELECT @sql = 'SELECT ProductID, ProductName, Category, Price FROM Product Where ProductName LIKE ''' + @prodname + '''' EXEC (@sql) ... To eval() w kolejnym wcieleniu! OWASP 44 Procedury składowane cd. Przykład tej samej podatności w Oracle CREATE OR REPLACE PROCEDURE SP_ProductSearch(Prodname IN VARCHAR2) AS sqltext VARCHAR2(80); BEGIN sqltext := 'SELECT ProductID, ProductName, Category, Price FROM Product WHERE ProductName LIKE ''' || Prodname || ''''; EXECUTE IMMEDIATE sqltext; ... END; OWASP 45 Procedury składowane – Dynamic SQL Źródło podatności – Dynamic SQL • Dane znów „przemieszane” z kodem w jednej zmiennej, która zostaje wykonana jako polecenie SQL Jak się obronić? • Oddziel kod od danych • Escape'uj OWASP 46 Procedury składowane w MS SQL Oddzielenie danych od kodu • użyj sp_executesql razem z listą parametrów CREATE PROCEDURE SP_ProductSearch @prodname varchar(400) = NULL AS DECLARE @sql nvarchar(4000) SELECT @sql = N'SELECT ProductID, ProductName, Category, Price FROM Product Where ProductName LIKE @p' EXEC sp_executesql @sql, N'@p varchar(400)', @prodname OWASP 47 Procedury składowane w MS SQL cd. Escape'owanie zmiennych tekstowych Nazwa obiektu QUOTENAME(@v) Tekst <= 128 znaków QUOTENAME(@v,'''') Tekst > 128 znaków REPLACE(@v,'''','''''') Przykład: SET @cmd = N'select * from authors where lname=''' + REPLACE(@lname, '''', '''''') + N'''' Escape'uj tylko wtedy, kiedy musisz! (używaj sp_executesql z parametrami) OWASP 48 Procedury składowane w Oracle Oracle - użyj EXECUTE IMMEDIATE .. USING CREATE OR REPLACE PROCEDURE SP_ProductSearch(Prodname IN VARCHAR2) AS sqltext VARCHAR2(80); BEGIN sqltext := 'SELECT ProductID, ProductName, Category, Price WHERE ProductName=:p'; EXECUTE IMMEDIATE sqltext USING Prodname; ... END; Escape'owanie - pakiet DBMS_ASSERT OWASP 49 Procedury składowane w MySQL Wsparcie dla Dynamic SQL tylko poprzez prepared statements Napisanie podatnych procedur jest trudniejsze niż procedur zabezpieczonych! Wystarczy używać placeholderów zamiast doklejać wartości zmiennych OWASP 50 Procedury składowane w MySQL cd. PREPARE / EXECUTE USING / DEALLOCATE PREPARE DELIMITER $$ CREATE PROCEDURE get_users_like ( IN contains VARCHAR(40)) BEGIN SET @like = CONCAT("%", contains, "%"); SET @sql = "SELECT * FROM users WHERE uname LIKE ?"; PREPARE get_users_stmt from @sql; EXECUTE get_users_stmt USING @like; DEALLOCATE PREPARE get_users_stmt; END$$ DELIMITER ; OWASP 51 Procedury składowane w MySQL cd. Lub jeszcze prościej (bezpośrednio) DELIMITER $$ CREATE PROCEDURE get_users_like ( IN contains VARCHAR(40)) BEGIN SET @like = CONCAT("%", contains, "%"); SELECT * FROM users WHERE uname LIKE @like; END$$ DELIMITER ; Escape'owanie – funkcja QUOTE() OWASP 52 Procedury składowane w PHP Różne wsparcie w zależności od RDBMS Wsparcie zależy od konkretnego sterownika Wspólne API (np. PDO) obsługuje tylko najprostsze wywołania • • Różna obsługa (lub brak) bardziej zaawansowanych wywołań • Procedura nic nie zwraca Procedura zwraca prosty rezultat w parametrze OUT np. pobieranie rekordów z procedur, kursory Wsparcie we frameworkach śladowe Wciąż występują błędy OWASP 53 Procedury składowane w PDO Wywołanie procedury // MySQL $sql = "CALL get_users_like(:contains)"; // MS SQL – EXEC get_users_like :contains $stmt = $pdo->prepare($sql); $ret = $stmt->execute(array('contains' => $input)); foreach($stmt->fetchAll() as $users) { var_dump($users); } unset($s); OWASP 54 Procedury składowane w Doctrine/Propel/Zend Framework Doctrine - Brak wsparcia (użyj PDO) $pdo = Doctrine_Manager::connection()->getDbh(); Propel – jw. $pdo = Propel::getConnection(UserPeer::DATABASE_NAME); Zend Framework – jw. $pdo = $db::getConnection(); OWASP 55 Procedury składowane w MDB2 Trzeba własnoręcznie escape'ować wszystkie parametry $mdb2->loadModule('Function'); $multi_query = $mdb2->setOption('multi_query', true); if (!PEAR::isError($multi_query)) { do { $result = $mdb2->executeStoredProc('get_users_like', array($mdb2->quote($contains, 'text'))); while ($row = $result->fetchRow()) { var_dump($row); } } while ($result->nextResult()); } OWASP 56 Procedury składowane - pułapki Długość zmiennych CREATE PROCEDURE change_password @loginname varchar(50), @old varchar(50), @new varchar(50) AS DECLARE @command varchar(120) SET @command= 'UPDATE users SET password=' + QUOTENAME(@new, '''') + ' WHERE loginname=' + QUOTENAME(@loginname, '''') + ' AND password=' + QUOTENAME(@old, '''') EXEC (@command) GO OWASP 57 Procedury składowane - podsumowanie Czasochłonne przenoszenie logiki SQL z aplikacji na serwer Nie są łatwo przenośne pomiędzy RDBMS Napisane bezpiecznych procedur i tak wymaga użycia prepared statements lub escape'owania danych Źle zaimplementowane mogą zwiększyć podatność • Zarówno wywołanie procedury, jak i jej kod jest podatny • Procedura może mieć większe uprawnienia niż kod ją wywołujący Złe wsparcie w PHP i we frameworkach OWASP 58 Procedury składowane - podsumowanie Mają dużo zalet poza naszym obszarem zainteresowania Można precyzyjnie zarządzać uprawnieniami do procedur Przydatne w wypadku stosowania różnych klientów (Java/.NET + PHP) Mogą zwiększyć wydajność I wiele innych... Wnioski: Pozwalają osiągnąć dobre zabezpieczenie przed SQL injection, ale przy dużych kosztach. Niezbędne jest zabezpieczanie samego kodu procedur przed SQL injection. OWASP 59 Jak się bronić? Metody uzupełniające OWASP 60 Walidacja i filtrowanie danych Kontrola poprawności danych zewnętrznych Odbywa się przed przetwarzaniem tych danych Nie myl z escape'owaniem! Filter INPUT - escape OUTPUT Osobne reguły walidacji dla każdego parametru - sprawdzaj m.in. • • • • Typ zmiennej Skalar / tablica Wartości min / max Długość danych tekstowych! [1] OWASP 61 Uzupełniające metody obrony Komplementarne do poprzednich! Zasada najmniejszych uprawnień przy łączeniu się do bazy danych Wyłączenie nieużywanych funkcji, kont, pakietów dostarczanych z bazą danych Regularne aktualizowanie serwera bazy danych Dobra konfiguracja PHP i bazy • magic_quotes_* = false • display_errors = false Dobrze zaprojektowana baza danych OWASP 62 Podsumowanie Zwracaj uwagę na SQL injection - pojedynczy błąd może wiele kosztować! Preferuj rozwiązania kompleksowe - np. frameworki Filtruj wszystkie dane wejściowe Pamiętaj o typach i długościach zmiennych Stosuj whitelisting zamiast blacklistingu - to drugie kiedyś zawiedzie! Stosuj prepared statements wszędzie, gdzie możesz Unikaj escape'owania W procedurach składowanych uważaj na Dynamic SQL OWASP 63 Linki • • • • • • • • • • • • Omawiane projekty sqlmap.sourceforge.net php.net/manual/en/book.pdo.php www.doctrine-project.org propel.phpdb.org/trac framework.zend.com pear.php.net/package/MDB2 O SQL injection www.owasp.org/index.php/SQL_Injection unixwiz.net/techtips/sql-injection.html delicious.com/koto/sql+injection Hack me threats.pl/bezpieczenstwo-aplikacji-internetowych tinyurl.com/webgoat mavensecurity.com/dojo.php [email protected] http://blog.kotowicz.net OWASP 64