W trakcie prezentacji zademonstrujemy szkody, na jesteście narażeni nie myśląc o SQL injection. Dowiecie się, jak się przed nim bronić - zarówno w teorii, jak i na konkretnych przykładach. Nauczymy się pisać bezpiecznie w PHP 5 - sprawdzimy Zend Framework i Symfony, przenalizujemy Propel, Doctrine, PDO i mdb2. Omówimy wszystkie kruczki i różnice między różnymi systemami baz danych (Oracle, MS SQL Server, MySQL) oraz nauczymy się pisać procedury składowane odporne na SQL injection.
2. 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
3. Omawiane bazy danych (RDBMS)
MySQL
Oracle
MS SQL Server
W mniejszym stopniu:
• PostgreSQL
• SQLite
OWASP 3
4. 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
6. 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=a@example.com
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
7. 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
9. 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
10. 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
12. 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
14. Prepared statements – zasada działania
1. Przygotowujemy polecenie SQL (string)
W miejsce danych wstawiamy znaczniki
WHERE a = ? ... WHERE a = :col
2. Przesyłamy polecenie na serwer PREPARE
3. Podajemy zestaw danych do polecenia
4. Wykonujemy polecenie EXECUTE
5. Odbieramy rezultat
3, 4, 5 można powtarzać...
6. Czyścimy polecenie
OWASP 14
15. 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
16. 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
17. 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
18. Prepared statements w Doctrine
Używa PDO (emulacja dla Oracle) i prepared statements
Zamiast SQL używa własnego języka – DQL
$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
19. 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
20. 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
21. 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
22. 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
23. 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
25. 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
26. 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
28. 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
29. 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
31. 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
32. 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
33. 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
34. 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
35. 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
36. 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
37. 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
38. 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
39. 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
41. Escape'owanie danych - podsumowanie
Wydaje się proste – zastępowanie tekstu
Niestety, tylko się wydaje
• Musimy znać kontekst (baza danych, charset)
• Istnieją błędne implementacje
Skłania do stosowania niebezpiecznych konstrukcji
• sklejanie poleceń
• ignorowanie zmiennych numerycznych
Stosowanie dopuszczalne tylko, jeśli
• Programujemy pod konkretną bazę
• Nie ma innej możliwości
OWASP 41
43. 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
44. 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
45. 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
46. 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
47. 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
48. 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
49. 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
50. 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
51. 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
52. 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
53. 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
• Procedura nic nie zwraca
• Procedura zwraca prosty rezultat w parametrze OUT
Różna obsługa (lub brak) bardziej zaawansowanych
wywołań
• np. pobieranie rekordów z procedur, kursory
Wsparcie we frameworkach śladowe
Wciąż występują błędy
OWASP 53
54. 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
56. 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)) {
$result = $mdb2->executeStoredProc('get_users_like',
array($mdb2->quote($contains, 'text')));
do {
while ($row = $result->fetchRow()) {
var_dump($row);
}
} while ($result->nextResult());
}
OWASP 56
57. 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
58. 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
59. 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
61. 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
62. 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
63. 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