SQLi- mit bezpiecznych procedur
Czy Ty jesteś bezpieczny? Przed czym należy się bronić?
O SQL Injection można znaleźć w Internecie wiele materiałów, zakładam więc, że czytelnik ma już podstawową wiedzę z tego zakresu, a mianowicie wie:
- co znaczy zwrot SQL Injection
- jak manipulować danymi w bazie danych za pomocą zapytań SQL przemycanych w przekazywanych danych wejściowych.
Jeżeli terminologia nie jest jeszcze znana,
polecam zapoznanie się z tematyką: http://www.owasp.org/index.php/SQL_injection,
http://www.owasp.org/index.php/Guide_to_SQL_Injection, http://www.governmentsecurity.org/articles/SQLinjectionBasicTutorial.php).
Szczególnie polecam strony OWASP, ponieważ nie powielają one mitu, o którym będę pisał poniżej, a który jest powielany na wielu forach, blogach itp.
Mit, którym chciałbym się zająć, jest o tyle niebezpieczny, że daje pozorne poczucie bezpieczeństwa, a jednocześnie jest ukrytą miną, na którą ktoś wcześniej, czy później trafi. Tyle, że w tym przypadku ofiarą będzie umieszczający tę minę.
Mit ten brzmi: „korzystanie ze Stored Procedures zabezpiecza przed SQL Injection”.
Dla wielu programistów jest oczywiste, że najlepszym zabezpieczeniem przed SQL Injection jest skorzystanie z używanych w większości serwerów bazodanowych procedur przechowywanych w bazie danych, wywoływanych z parametrami, przekazywanymi przez klienta wywołującego procedurę. Jest to prawda w 50% (a więc nie jest to prawda), ponieważ błędnie napisana procedura może się okazać równie niebezpieczna, co konkatenowany skrypt SQL zaszyty w kodzie aplikacji. Może nawet bardziej niebezpieczna, ponieważ zapytanie generowane dynamicznie w kodzie, w przeciwieństwie do procedury, nie sprawia wrażenia bezpiecznego.
W przykładzie posłużę się procedurami składowanymi (stored procedures) oferowanymi przez SQL Server.
Załóżmy, że mamy w bazie danych tabelę AppUsers:
Tabela przechowuje dane użytkowników naszej aplikacji. Jedno z pól (pole IsAdmin) informuje, czy użytkownik jest administratorem. Na podstawie flagi ustawionej w tym polu aplikacja daje dostęp do panelu administracyjnego, w którym zalogowany użytkownik może zarządzać aplikacją, użytkownikami, uprawnieniami itp.
W części publicznej aplikacji mamy jakąś listę użytkowników, którą można przeszukiwać. Dla uproszczenia przykładu przyjmijmy, że będzie to wyszukiwanie według loginu użytkownika. W sytuacji, kiedy w filtrze wyszukiwania nie zostaną wprowadzone żadne dane, wówczas prezentowana jest lista wszystkich użytkowników. W przeciwnym razie w bazie będą wyszukiwani użytkownicy, których login zawiera litery podane w zapytaniu. Programista w tej sytuacji postanawia skorzystać z procedury SQL, ponieważ jest przekonany, że to zabezpieczy go przed problemem SQL Injection. Umieszcza więc w bazie danych procedurę:
CREATE PROCEDURE [dbo].[USP_UsersSearchSafe]
SET @sql = ' SELECT Id, Username, IsAdmin, LastLogon ' +
SET @sql = @sql + ' Where Username LIKE '''+ @username +''''
Wydaje się, że
wszystko jest bezpieczne, ponieważ skorzystał z procedury, natomiast teraz
wyobraźmy sobie, że w polu tekstowym użytkownik o loginie „user1” wprowadzi
poniższy kod SQL:
';update AppUsers set IsAdmin=1 where Username='user1';--
Po przesłaniu powyższego zapytania do mechanizmu wyszukiwawczego, nasz mechanizm wyszukiwawczy „w promocji” dorzuca użytkownikowi „user1” uprawnienia administratora. Gdyby zapytanie było wykonywane 6 grudnia, można by zrezygnować z klauzuli „where” i wówczas wszyscy użytkownicy w prezencie mikołajkowym dostaliby administracyjne uprawnienia.
Nie trzeba wyrafinowanej analizy, żeby odkryć, co jest przyczyną podatności na SQL Injection, mimo zastosowania procedury SQL.
W powyższym przykładzie procedura została napisana dokładnie w taki sam sposób, w jaki byłaby ona zapisana, gdyby programista zaszył ją wewnątrz kodu aplikacji, tzn. zapytanie SQL jest konkatenowane i ewaluowane , natomiast sam przekazany parametr nie jest odseparowany od logiki zapytania. Zapytanie to nie różni się zbytnio od zapytań typu:
„Select * from products where Id=” + Input[„id”]
Przy korzystaniu z procedur SQL ważne jest, żeby uświadomić sobie, że przez zagrożeniami nie chroni sam fakt skorzystania z procedury. Przed wszelkiego rodzaju problemami chroni tylko DOBRZE NAPISANY KOD, niezależnie od tego, czy znajduje się on w aplikacji, czy też w bazie danych.
Przejdźmy więc do tego mitycznego stworzenia o nazwie DOBRZE NAPISANY KOD. Jak powinien wyglądać w naszym przykładzie, żeby wyeliminować podatność na SQL Injection.
CREATE PROCEDURE [dbo].[USP_UsersSearchReallySafe]
@username varchar(400) = NULL AS
SET @sql = ' SELECT Id, Username, IsAdmin, LastLogon ' +
SET @sql = @sql + ' Where Username LIKE @username '
Exec sp_executesql @sql, N'@username varchar(400)',@username
Na czym więc polega różnica. Otóż w tym przypadku skorzystaliśmy możliwości oferowanych przez zapytania parametryzowane, dzięki czemu parametry przekazywane do zapytania SQL (dane wejściowe) zostały całkowicie odseparowane od składni zapytania. Jedyną niedogodnością jest, że w tym przypadku wywołanie przygotowanego zapytania SQL jest trochę bardziej skomplikowane, ponieważ musimy skorzystać ze specjalnej procedury sp_executesql, a także musimy jeszcze raz przekazać parametry do tej procedury. W tym przypadku jednak dużo bardziej przejrzyste jest przygotowanie samego zapytania SQL, dlatego w sumie wychodzimy na remis. Z tą różnicą, że w tym drugim przypadku mamy naprawdę bezpieczną procedurę.
Jaką konkluzję chciałbym więc zawrzeć w podsumowaniu? Chyba taką, że nawet „the best practice” może być przyczyną problemów, jeżeli będzie zastosowana bezrefleksyjnie...
I że ŹLE NAPISANY KOD zawsze jest zagrożeniem, niezależnie od tego, gdzie został umieszczony.
