CWE-89: Failure to Preserve SQL Query Structure (aka 'SQL Injection') pochodzi wprost z 2009 CWE/SANS Top 25 Most Dangerous Programming Errors. Osobiście bardzo się cieszę z opublikowania tego typu dokumentu, zalecenia w nim zawarte można wprost wykorzystywać jako tak zwane najlepsze praktyki, co powinno znacznie ukrócić niejednokrotnie uciążliwe dyskusje z devloperami odnośnie tego co jest, a co nie jest najlepszą praktyką. Jak Tomek kiedyś stwierdził, temat sql injection jest tematem dyżurnym na moim blogu. Tym razem trochę na temat tego, jak zmniejszyć szansę pojawienia się podatności w tworzonej aplikacji.
Failure to Preserve SQL Query Structure (aka 'SQL Injection')
Zgodnie z zasadą defence in depth zabezpieczenia powinny mieć wiele warstw. W ramach tego samego dokumentu na pierwszym miejscu znajduje się CWE-20: Improper Input Validation. Osobiście bardzo się z tego cieszę, byłem niepocieszony gdy walidacja danych wyjściowych została "zdegradowana" w OWASP TOP10 2007 (przypominam o zaszczytnym pierwszym miejscu w OWASP TOP10 2004). Tym razem jednak założę, że nie ma już żadnych innych mechanizmów obronnych, lub zostały one przełamane. Pominę też temat odpowiednich uprawnień (a raczej ich ograniczeń) w bazie danych, być może napiszę na ten temat osobny wpis.
Można wymienić trzy podstawowe techniki ochrony przed sql injection:
- encoding danych przed umieszczeniem ich w zapytaniu SQL,
- używanie procedur składowanych,
- używanie zapytań parametryzowanych,
Teoretycznie każda z tych technik jest skuteczna, w praktyce jednak można łatwo wskazać, że tak nie jest. Ma to swoje uzasadnienie - im bardziej skomplikowany jest "mechanizm obronny", tym większa szansa popełnienia w nim błędu.
Najbardziej zawodną techniką jest encoding danych przed umieszczeniem ich w zapytaniu SQL. Programista po prostu skleja łańcuchy znaków (lub używa format string) by zbudować zapytanie SQL. By encoding danych działa, programista musi pamiętać, by każdy parametr wstawiany do zapytania SQL był poddany odpowiedniemu encodingowi. Są już dwie możliwości błędu:
- pominięcie zmiennej przy encodingu,
- użycie nieodpowiedniego encodingu (ze względu na "miejsce użycia" lub bazę danych),
Co gorsze wychwycenie takich błędów przy przeglądzie kodu nie jest do końca trywialne (zwłaszcza przy bałaganiarskim stylu programowania). Nie jest również trywialne stworzenie narzędzi, które przypadki błędów w trakcie sklejania zapytań SQL wychwycą. W skrócie - choć encoding jest rozwiązaniem skutecznym, to zbyt wiele zależy od programisty. Uzasadnioną praktyką jest raczej zakazanie budowania zapytań SQL przez sklejanie łańcuchów znaków. Wprowadzenie takiej prostej zasady znacznie zmniejsza prawdopodobieństwo wystąpienia błędu. Przynajmniej tak powinno być, w zależności od tego, co użyje się "zamiast".
Często zalecanym rozwiązaniem jest wykorzystanie procedur składowanych. Jest to rozwiązanie stosunkowo dobre, choć nie pozbawione swoich wad. Z jednej strony programista otrzymuje konkretne API, z którego może (i musi) korzystać przy dostępie do bazy danych, z drugiej jednak strony czasami wykorzystanie procedur składowanych przenosi sql injection z kodu aplikacji internetowej do kodu procedur składowanych. Co z tego, że procedura składowana przyjmuje określone, mocno typowane parametry, jeśli jednym z tych parametrów jest string, który bezpośrednio (i co ważniejsze - dynamicznie) jest wstawiany w część WHERE zapytania? W przypadku wykorzystania procedur składowanych w trakcie przeglądu kodu (i mam tu na myśli nie tylko przegląd wykonywany w ramach usługi przez firmę trzecią, ale przede wszystkim przez developera w trakcie wewnętrznych procedur QA) trzeba zweryfikować, czy "dynamiczne sklejanie zapytań SQL" nie zostało "zamiecione pod dywan" poprzez przeniesienie go "warstwę niżej". Swoją drogą są ciekawe opracowania na temat sql injection w procedurach składowanych, które nie przyjmują żadnych parametrów.
Wykorzystanie zapytań parametryzowanych może być rozwiązaniem najbardziej skutecznym, zwłaszcza jeśli zapytania te są "mocno typowane". Tutaj to, co musi zrobić programista, to zbudować zapytanie postaci: SELECT * FROM tabela WHERE ownerId=? AND price > ?, a następnie "uzupełnić" je poprzez podanie parametrów zapytania, które zostaną w odpowiedni sposób (w zależności od typu) wstawione do zapytania SQL.
Ponownie polecam lekturę wpisu Giving SQL Injection the Respect it Deserves gdzie wykorzystanie procedur składowanych i zapytań parametryzowanych jest poruszony w kontekście wymagań SDL. Przy okazji polecam ponownie również wpis SDL and the CWE/SANS Top 25.
Swoją drogą ciekawe może być "archeologia kodu źródłowego", można na przykład zobaczyć, że w pewnej chwili programiści zaczęli "się śpieszyć", w związku z czym wcześniej konsekwentnie stosowane zapytania parametryzowane nagle zaczęły "przeplatać się" z dynamicznym sklejaniem stringów. Takie przypadki są jednak dość proste do wychwycenia, w przeciwieństwie do sytuacji, gdy stosowany jest encoding danych. Jak pokazuje również praktyka, takie przypadki z dużym prawdopodobieństwem wprowadzają również podatności.
Reasumując: jeśli ktoś zamawia aplikację internetową, powinien w wymaganiach (i koniecznie - w umowie) zawrzeć wymagania odnośnie stosowania się do najlepszych praktyk. Jako najlepsze praktyki powinien zdefiniować między innymi wymagania/zalecenia przedstawione w dokumencie 2009 CWE/SANS Top 25 Most Dangerous Programming Errors, przy czym warto wprowadzić dodatkowe ograniczenia, jak na przykład "wyłączenie" w przypadku sql injection encodingu danych i dynamicznego budowania zapytań. Oczywiście trzeba sobie zastrzec prawo dostępu do kodu źródłowego (możliwość weryfikacji zastosowania najlepszych praktyk). Później, gdy okaże się, że dostawca systemu zrobił "po swojemu", dysponując takimi zapisami w umowie, łatwiej jest wyegzekwować usunięcie błędu (no właśnie, kwestia dyskusyjna, czy dany przypadek jest już błędem, czy jeszcze nie). Nie powinno być dyskusji o tym, czy w danym przypadku istnieje możliwa do wykorzystania podatność, czy nie - dostarczona aplikacja po prostu nie spełnia warunków umowy. Pacta sunt servanda, umów należy dotrzymywać (co działa zresztą w obie strony).
Tego już nie powinienem pisać: często zdarza się tak, że prezes dogada się z prezesem i wszystkie tego typu zapisy i "punkty kontrolne" można sobie w buty wsadzić (z perspektywy osoby odpowiedzialnej za bezpieczeństwo zamówionej aplikacji). Zawsze lepiej jest "mieć kija" na dostawcę, niż go nie mieć. To, czy się go użyje, jest już kwestią drugorzędną.