Dlaczego nie sprawić, by zapytania nie sparametryzowane zwróciły błąd?

22

Wstrzykiwanie SQL jest bardzo poważnym problemem bezpieczeństwa, w dużej mierze dlatego, że tak łatwo go pomylić: oczywisty, intuicyjny sposób budowania zapytania zawierającego dane wejściowe użytkownika naraża cię na niebezpieczeństwo, a właściwy sposób jego złagodzenia wymaga znajomości sparametryzowanej najpierw zapytania i wstrzyknięcie SQL.

Wydaje mi się, że oczywistym sposobem na rozwiązanie tego problemu byłoby wyłączenie oczywistej (ale niepoprawnej) opcji: napraw silnik bazy danych, aby każde otrzymane zapytanie, które używa zakodowanych wartości w klauzuli WHERE zamiast parametrów, zwróciło ładny, opisowy komunikat o błędzie instruujący zamiast tego użyć parametrów. Musiałoby to oczywiście wymagać opcji rezygnacji, aby takie rzeczy, jak zapytania ad-hoc z narzędzi administracyjnych nadal działały łatwo, ale powinny być domyślnie włączone.

Takie zamknięcie spowodowałoby zastrzyk SQL na zimno, prawie z dnia na dzień, ale o ile mi wiadomo, żaden RDBMS tak naprawdę nie robi. Czy jest jakiś dobry powód, dlaczego nie?

Mason Wheeler
źródło
22
bad_ideas_sql = 'SELECT title FROM idea WHERE idea.status == "bad" AND idea.user == :mwheeler'miałby zarówno zakodowane na stałe, jak i sparametryzowane wartości w jednym zapytaniu - spróbuj to złapać! Myślę, że istnieją uzasadnione przypadki użycia dla takich mieszanych zapytań.
amon
6
Co powiesz na wybranie rekordów z dzisiajSELECT * FROM jokes WHERE date > DATE_SUB(NOW(), INTERVAL 1 DAY) ORDER BY score DESC;
Jaydee,
10
@MasonWheeler przepraszam, miałem na myśli „spróbuj na to pozwolić”. Pamiętaj, że jest doskonale sparametryzowany i nie cierpi z powodu wstrzyknięcia SQL. Jednak sterownik bazy danych nie jest w stanie stwierdzić, czy literał "bad"jest naprawdę dosłowny, czy też wynika z połączenia łańcucha. Dwa rozwiązania, które widzę, to albo pozbywanie się SQL i innych DSL osadzonych na łańcuchach (tak, proszę), albo promowanie języków, w których łączenie łańcuchów jest bardziej irytujące niż używanie sparametryzowanych zapytań (umm, nie).
amon
4
i w jaki sposób RDBMS wykryje, czy to zrobić? Z dnia na dzień uniemożliwiłby dostęp do RDBMS za pomocą interaktywnego monitu SQL ... Nie można już wprowadzać poleceń DDL ani DML za pomocą jakiegokolwiek narzędzia.
jwenting
8
W pewnym sensie możesz to zrobić: w ogóle nie twórz zapytań SQL w czasie wykonywania, zamiast tego użyj ORM lub innej warstwy abstrakcji, która pozwala uniknąć potrzeby tworzenia zapytań SQL. ORM nie ma funkcji, których potrzebujesz? Zatem SQL jest językiem przeznaczonym dla osób, które chcą pisać SQL, dlatego ogólnie pozwala im pisać SQL. Podstawową kwestią jest to, że dynamiczne generowanie kodu jest trudniejsze niż się wydaje, ale ludzie i tak chcą to robić i będą niezadowoleni z produktów, które im na to nie pozwalają.
Steve Jessop,

Odpowiedzi:

45

Jest zbyt wiele przypadków, w których użycie literału jest właściwym podejściem.

Z punktu widzenia wydajności zdarza się, że w zapytaniach chcesz literałów. Wyobraź sobie, że mam narzędzie do śledzenia błędów, w którym gdy będzie wystarczająco duże, aby martwić się wydajnością, spodziewam się, że 70% błędów w systemie zostanie „zamkniętych”, 20% będzie „otwartych”, 5% będzie „aktywnych”, a 5 % będzie miało inny status. Mogę zasadnie chcieć mieć zapytanie, które zwraca wszystkie aktywne błędy

SELECT *
  FROM bug
 WHERE status = 'active'

zamiast przekazywać statusjako zmienną wiążącą. Chcę inny plan zapytań w zależności od przekazanej wartości status- chciałbym wykonać skanowanie tabeli, aby zwrócić zamknięte błędy i skanowanie indeksustatuskolumna, aby zwrócić aktywne pożyczki. Teraz różne bazy danych i różne wersje mają różne podejścia, aby (mniej lub bardziej skutecznie) pozwolić temu samemu zapytaniu na użycie innego planu zapytań w zależności od wartości zmiennej powiązania. Ale to zwykle wprowadza przyzwoitą złożoność, którą należy zarządzać, aby zrównoważyć decyzję o tym, czy zawracać sobie głowę ponowną analizą zapytania, czy też ponownie wykorzystać istniejący plan dla nowej wartości zmiennej powiązania. Dla programisty rozsądne może być radzenie sobie z tą złożonością. Lub może mieć sens wymuszenie innej ścieżki, gdy mam więcej informacji o tym, jak będą wyglądać moje dane, niż optymalizator.

Z punktu widzenia złożoności kodu jest wiele razy, że literały w instrukcjach SQL mają sens. Na przykład, jeśli masz zip_codekolumnę z 5-znakowym kodem pocztowym, a czasami ma dodatkowe 4 cyfry, warto zrobić coś takiego

SELECT substr( zip_code, 1, 5 ) zip,
       substr( zip_code, 7, 4 ) plus_four

zamiast przekazywać 4 oddzielne parametry dla wartości liczbowych. Nie są to rzeczy, które kiedykolwiek się zmienią, więc ich powiązanie ze zmiennymi służy tylko do tego, że kod jest potencjalnie trudniejszy do odczytania i do stworzenia możliwości, że ktoś będzie wiązał parametry w niewłaściwej kolejności i zakończy się błędem.

Justin Cave
źródło
12

Wstrzykiwanie SQL ma miejsce, gdy zapytanie jest budowane przez połączenie tekstu z niezaufanego i nieważnego źródła z innymi częściami zapytania. Podczas gdy coś takiego najczęściej występuje w literałach łańcuchowych, nie byłby to jedyny sposób, w jaki mogłoby się to zdarzyć. Zapytanie o wartości liczbowe może wymagać łańcucha wprowadzonego przez użytkownika (to znaczy powinien zawierać tylko cyfry) i połączyć się z innym materiałem, aby utworzyć zapytanie bez znaków cudzysłowu zwykle związanych z literałami ciągów; kod, który nadmiernie ufa sprawdzaniu poprawności po stronie klienta, może mieć na przykład nazwy pól pochodzące z ciągu zapytania HTML. Nie ma sposobu, aby kod patrząc na ciąg zapytania SQL mógł zobaczyć, jak został złożony.

Ważne jest nie to, czy instrukcja SQL zawiera literały łańcuchowe, ale raczej, czy łańcuch zawiera sekwencje znaków z niezaufanych źródeł , a sprawdzenie poprawności najlepiej byłoby przeprowadzić w bibliotece, która tworzy zapytania. Zasadniczo w języku C # nie ma sposobu na napisanie kodu, który pozwoli na literałość łańcucha, ale nie pozwoli na inne wyrażenia łańcuchowe, ale można mieć regułę kodowania, która wymaga budowania zapytań za pomocą klasy budującej zapytania, a nie konkatenacja ciągów i każdy, kto przekaże ciąg nieliteralny do konstruktora zapytań, musi uzasadnić takie działanie.

supercat
źródło
1
W przybliżeniu dla „czy to dosłowność” możesz sprawdzić, czy łańcuch jest internowany.
CodesInChaos
1
@CodesInChaos: Prawda, a taki test może być wystarczająco dokładny do tego celu, pod warunkiem, że każdy, kto miał powód do wygenerowania ciągu w czasie wykonywania, użył metody, która zaakceptowała ciąg nieliteralny, zamiast internalizować ciąg wygenerowany w czasie wykonywania i używać że (nadanie metodzie nie-literałów ciągów innej nazwy ułatwi recenzentom kodu sprawdzenie wszystkich jej zastosowań).
supercat
Zauważ, że chociaż nie można tego zrobić w języku C #, niektóre inne języki mają udogodnienia, które to umożliwiają (np. Moduł ciągów skażonych Perla).
Jules
Mówiąc bardziej zwięźle, jest to problem klienta , a nie problem serwera.
Blrfl,
7
SELECT count(ID)
FROM posts
WHERE deleted = false

Jeśli chcesz umieścić wyniki z nich w stopce forum, musisz dodać fikcyjny parametr, aby za każdym razem mówić false. Albo naiwny programista internetowy sprawdza, jak wyłączyć to ostrzeżenie, a następnie kontynuuje.

Teraz możesz powiedzieć, że dodasz wyjątek dla wyliczeń, ale to po prostu ponownie otworzy dziurę (choć mniejszą). Nie wspominając już o tym, że ludzie muszą najpierw nauczyć się nie korzystaćvarchars .

Prawdziwym problemem wstrzykiwania jest programowo konstruowanie ciągu zapytania. Rozwiązaniem tego jest mechanizm procedury składowanej i egzekwowanie jego użycia lub biała lista dozwolonych zapytań.

maniak zapadkowy
źródło
2
Jeśli Twoim rozwiązaniem dla „zbyt łatwo jest zapomnieć - lub w ogóle nie wiedzieć - korzystać z zapytań sparametryzowanych” jest „spraw, aby wszyscy pamiętali - i przede wszystkim - używali przechowywanych procesów”, wtedy brakuje mi sedna pytania.
Mason Wheeler,
5
W mojej pracy widziałem zastrzyk SQL za pomocą procedur przechowywanych. Okazuje się, że mandowanie procedur przechowywanych dla wszystkiego jest ZŁE. Zawsze istnieje 0,5%, które są prawdziwymi zapytaniami dynamicznymi (nie można sparametryzować całej klauzuli where, nie mówiąc już o dołączeniu do tabeli).
Joshua,
W przykładzie z tej odpowiedzi można zastąpić deleted = falsez NOT deleted, co pozwala uniknąć dosłownym. Ale punkt jest ogólnie ważny.
psmears,
5

TL; DR : Musiałbyś ograniczyć wszystkie literały, nie tylko te w WHEREklauzulach. Z powodów, dla których tego nie robią, pozwala to na odłączenie bazy danych od innych systemów.

Po pierwsze, twoje założenie jest wadliwe. Chcesz ograniczyć tylko WHEREklauzule, ale to nie jedyne miejsce, w którym użytkownik może wejść. Na przykład,

SELECT
    COUNT(CASE WHEN item_type = 'blender' THEN 1 END) as type1_count,
    COUNT(CASE WHEN item_type = 'television' THEN 1 END) AS type2_count)
FROM item

Jest to równie podatne na wstrzyknięcie SQL:

SELECT
    COUNT(CASE WHEN item_type = 'blender' THEN 1 END) FROM item; DROP TABLE user_info; SELECT CASE(WHEN item_type = 'blender' THEN 1 END) as type1_count,
    COUNT(CASE WHEN item_type = 'television' THEN 1 END) AS type2_count)
FROM item

Więc nie możesz po prostu ograniczyć literałów w WHEREklauzuli. Musisz ograniczyć wszystkie literały.

Teraz pozostaje pytanie: „Po co w ogóle dopuszczać literałów?” Pamiętaj o tym: chociaż relacyjne bazy danych są używane pod aplikacją napisaną w innym języku przez znaczny procent czasu, nie ma wymagań używania kodu aplikacji do korzystania z bazy danych. I oto odpowiedź: potrzebujesz literałów, aby napisać kod. Jedyną inną alternatywą byłoby wymaganie, aby cały kod był napisany w jakimś języku niezależnym od bazy danych. Dzięki temu masz możliwość pisania „kodu” (SQL) bezpośrednio w bazie danych. Jest to cenne oddzielenie i bez literałów byłoby to niemożliwe. (Spróbuj pisać kiedyś w swoim ulubionym języku bez literałów. Jestem pewien, że możesz sobie wyobrazić, jak trudne byłoby to.)

Jako typowy przykład, literały są często używane w populacji tabel z listą wartości / tablic przeglądowych:

CREATE TABLE user_roles (role_id INTEGER, role_name VARCHAR(50));
INSERT INTO user_roles (1, 'normal');
INSERT INTO user_roles (2, 'admin');
INSERT INTO user_roles (3, 'banned');

Bez nich musisz napisać kod w innym języku programowania, aby wypełnić tę tabelę. Możliwość zrobienia tego bezpośrednio w SQL jest cenna .

Pozostaje nam jeszcze jedno pytanie: dlaczego więc nie robią tego biblioteki klienta języka programowania? I tutaj mamy bardzo prostą odpowiedź: ponownie wdrożyliby cały parser bazy danych dla każdej obsługiwanej wersji bazy danych . Czemu? Ponieważ nie ma innego sposobu, aby zagwarantować, że znalazłeś każdy literał. Wyrażenia regularne to za mało. Na przykład: zawiera 4 oddzielne literały w PostgreSQL:

SELECT $lit1$I'm a literal$lit1$||$lit2$I'm another literal $$ with nested string delimiters$$ $lit2$||'I''m ANOTHER literal'||$$I'm the last literal$$;

Próba zrobienia tego byłaby koszmarem konserwacyjnym, zwłaszcza że poprawna składnia często zmienia się między głównymi wydaniami baz danych.

jpmc26
źródło