W jaki sposób przygotowane instrukcje mogą chronić przed atakami typu SQL injection?

172

W jaki sposób przygotowane instrukcje pomagają nam zapobiegać atakom typu SQL injection ?

Wikipedia mówi:

Przygotowane instrukcje są odporne na iniekcję SQL, ponieważ wartości parametrów, które są przesyłane później przy użyciu innego protokołu, nie muszą być poprawnie chronione. Jeśli oryginalny szablon instrukcji nie pochodzi z danych wejściowych zewnętrznych, nie można wykonać wstrzyknięcia SQL.

Nie widzę dobrze powodu. Jakie byłoby proste wyjaśnienie w prostym języku angielskim i kilka przykładów?

Aan
źródło

Odpowiedzi:

290

Pomysł jest bardzo prosty - zapytanie i dane przesyłane są osobno na serwer bazy danych .
To wszystko.

Źródłem problemu z iniekcją SQL jest mieszanie kodu i danych.

W rzeczywistości nasze zapytanie SQL jest legalnym programem . A my tworzymy taki program dynamicznie, dodając pewne dane w locie. W związku z tym dane mogą zakłócać kod programu, a nawet go zmieniać, co pokazuje każdy przykład wstrzyknięcia SQL (wszystkie przykłady w PHP / Mysql):

$expected_data = 1;
$query = "SELECT * FROM users where id=$expected_data";

wygeneruje zwykłe zapytanie

SELECT * FROM users where id=1

podczas gdy ten kod

$spoiled_data = "1; DROP TABLE users;"
$query        = "SELECT * FROM users where id=$spoiled_data";

wygeneruje złośliwą sekwencję

SELECT * FROM users where id=1; DROP TABLE users;

Działa, ponieważ dodajemy dane bezpośrednio do treści programu i stają się one częścią programu, więc dane mogą zmienić program, aw zależności od przekazanych danych będziemy mieli albo zwykłe dane wyjściowe, albo usersusuniętą tabelę .

O ile w przypadku przygotowanych wypowiedzi nie zmieniamy naszego programu, to pozostaje on nienaruszony, o
to chodzi.

Najpierw wysyłamy program na serwer

$db->prepare("SELECT * FROM users where id=?");

gdzie dane są zastępowane przez jakąś zmienną zwaną parametrem lub symbolem zastępczym.

Zwróć uwagę, że do serwera wysyłane jest dokładnie to samo zapytanie, bez żadnych danych! Następnie wysyłamy dane z drugim żądaniem, zasadniczo oddzielonym od samego zapytania:

$db->execute($data);

więc nie może zmienić naszego programu i wyrządzić żadnej szkody.
Całkiem proste - prawda?

Jedyne co muszę dodać, że zawsze pomijane w każdej instrukcji:

Przygotowane instrukcje mogą chronić tylko literały danych , ale nie mogą być używane z żadną inną częścią zapytania.
Kiedy więc musimy dodać, powiedzmy, dynamiczny identyfikator - na przykład nazwę pola - przygotowane zestawienia nie mogą nam pomóc. Mam wyjaśnił sprawę niedawno , więc nie będę powtarzać.

Twój zdrowy rozsądek
źródło
2
„np. domyślnie PDO nie używają przygotowanych zestawień” - to nie do końca prawda, ponieważ PDO emuluje przygotowane zestawienia tylko dla sterowników, które takiej funkcji nie obsługują.
pinepain
3
@ zaq178miami: "PDO emuluje przygotowane instrukcje tylko dla sterowników, które nie obsługują tej funkcji" - nie jest do końca prawdą. MySQL od dłuższego czasu obsługuje przygotowane instrukcje. Sterownik PDO też. Jednak zapytania MySQL były nadal domyślnie przygotowywane przez PDO, ostatnio sprawdzałem.
cHao
9
Czym różni się $spoiled_data = "1; DROP TABLE users;"-> $query = "SELECT * FROM users where id=$spoiled_data";od: $db->prepare("SELECT * FROM users where id=?");-> $data = "1; DROP TABLE users;"-> $db->execute($data);. Czy nie zrobią tego samego?
Juha Untinen
14
@Juha Untinen Dane mogą być dowolne. Nie przeanalizuje danych. To jest DANE, a nie polecenie. Więc nawet jeśli $ data zawiera polecenia sql, nie zostanie ono wykonane. Ponadto, jeśli id ​​jest liczbą, zawartość ciągu wygeneruje raport lub wartość zero.
Soley,
21

Oto SQL do skonfigurowania przykładu:

CREATE TABLE employee(name varchar, paymentType varchar, amount bigint);

INSERT INTO employee VALUES('Aaron', 'salary', 100);
INSERT INTO employee VALUES('Aaron', 'bonus', 50);
INSERT INTO employee VALUES('Bob', 'salary', 50);
INSERT INTO employee VALUES('Bob', 'bonus', 0);

Klasa Inject jest podatna na iniekcje SQL. Zapytanie jest dynamicznie wklejane wraz z danymi wejściowymi użytkownika. Celem zapytania było pokazanie informacji o Robercie. Wynagrodzenie lub premia na podstawie danych wejściowych użytkownika. Jednak złośliwy użytkownik manipuluje danymi wejściowymi, uszkadzając zapytanie, dołączając odpowiednik „lub prawda” do klauzuli where, tak aby wszystko zostało zwrócone, w tym informacje o Aaronie, które miały być ukryte.

import java.sql.*;

public class Inject {

    public static void main(String[] args) throws SQLException {

        String url = "jdbc:postgresql://localhost/postgres?user=user&password=pwd";
        Connection conn = DriverManager.getConnection(url);

        Statement stmt = conn.createStatement();
        String sql = "SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType='" + args[0] + "'";
        System.out.println(sql);
        ResultSet rs = stmt.executeQuery(sql);

        while (rs.next()) {
            System.out.println(rs.getString("paymentType") + " " + rs.getLong("amount"));
        }
    }
}

Po uruchomieniu tego pierwszy przypadek dotyczy normalnego użytkowania, a drugi złośliwego wstrzyknięcia:

c:\temp>java Inject salary
SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType='salary'
salary 50

c:\temp>java Inject "salary' OR 'a'!='b"
SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType='salary' OR 'a'!='b'
salary 100
bonus 50
salary 50
bonus 0

Nie należy budować instrukcji SQL z konkatenacją łańcuchów danych wejściowych użytkownika. Jest nie tylko podatny na wstrzyknięcie, ale ma również wpływ na buforowanie na serwerze (instrukcja zmienia się, więc prawdopodobieństwo trafienia w pamięć podręczną instrukcji SQL jest mniejsze, podczas gdy przykład wiązania zawsze uruchamia tę samą instrukcję).

Oto przykład Binding, aby uniknąć tego rodzaju zastrzyków:

import java.sql.*;

public class Bind {

    public static void main(String[] args) throws SQLException {

        String url = "jdbc:postgresql://localhost/postgres?user=postgres&password=postgres";
        Connection conn = DriverManager.getConnection(url);

        String sql = "SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType=?";
        System.out.println(sql);

        PreparedStatement stmt = conn.prepareStatement(sql);
        stmt.setString(1, args[0]);

        ResultSet rs = stmt.executeQuery();

        while (rs.next()) {
            System.out.println(rs.getString("paymentType") + " " + rs.getLong("amount"));
        }
    }
}

Uruchomienie tego z tymi samymi danymi wejściowymi, co w poprzednim przykładzie, pokazuje, że złośliwy kod nie działa, ponieważ żaden typ płatności nie pasuje do tego ciągu:

c:\temp>java Bind salary
SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType=?
salary 50

c:\temp>java Bind "salary' OR 'a'!='b"
SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType=?
Glenn
źródło
Czy użycie przygotowanej instrukcji z programu łączącego się z bazą danych daje taki sam efekt jak użycie przygotowanej instrukcji będącej częścią bazy danych? Na przykład Postgres ma przygotowaną własną instrukcję i czy jej użycie zapobiegnie iniekcji SQL? postgresql.org/docs/9.2/static/sql-prepare.html
Celeritas
@Celeritas Nie mam ostatecznej odpowiedzi na Postgresql. Patrząc na dokumenty, wydaje się, że efekt jest taki sam. PREPAREtworzy ustaloną nazwaną instrukcję, która jest już przeanalizowana (tj. instrukcja nie zmieni się już bez względu na dane wejściowe), podczas gdy EXECUTEuruchomi nazwaną instrukcję wiążącą parametry. Ponieważ PREPAREma tylko czas trwania sesji, naprawdę wygląda na to, że jest przeznaczony dla wydajności, a nie do zapobiegania wstrzykiwaniu przez skrypty psql. Aby uzyskać dostęp do psql, może nadać uprawnienia do procedur składowanych i powiązać parametry w ramach procs.
Glenn
@Celeritas Wypróbowałem powyższy kod za pomocą PostgreSQL 11.1 na x86_64 i powyższy przykład SQLi działał.
Krishna Pandey
15

Zasadniczo, dzięki przygotowanym instrukcjom dane pochodzące od potencjalnego hakera są traktowane jako dane - i nie ma możliwości, aby można je było wymieszać z SQL aplikacji i / lub zinterpretować jako SQL (co może się zdarzyć, gdy przekazane dane są umieszczane bezpośrednio w SQL aplikacji).

Dzieje się tak dlatego, że przygotowane instrukcje najpierw „przygotowują” zapytanie SQL, aby znaleźć efektywny plan zapytania, a później wysyłają rzeczywiste wartości, które przypuszczalnie pochodzą z formularza - wtedy zapytanie jest faktycznie wykonywane.

Więcej świetnych informacji tutaj:

Przygotowane instrukcje i iniekcja SQL

Jose
źródło
6

Przeczytałem odpowiedzi i nadal czułem potrzebę podkreślenia kluczowego punktu, który wyjaśnia istotę przygotowanych oświadczeń. Rozważ dwa sposoby wykonywania zapytań w bazie danych, w przypadku których wymagane są dane wejściowe użytkownika:

Podejście naiwne

Jeden łączy dane wejściowe użytkownika z częściowym ciągiem znaków SQL w celu wygenerowania instrukcji SQL. W takim przypadku użytkownik może osadzić złośliwe polecenia SQL, które zostaną następnie przesłane do bazy danych w celu wykonania.

String SQLString = "SELECT * FROM CUSTOMERS WHERE NAME='"+userInput+"'"

Na przykład złośliwe dane wejściowe użytkownika mogą prowadzić do SQLStringrówności"SELECT * FROM CUSTOMERS WHERE NAME='James';DROP TABLE CUSTOMERS;'

Ze względu na złośliwego użytkownika SQLStringzawiera 2 instrukcje, z których druga ( "DROP TABLE CUSTOMERS") spowoduje szkodę.

Przygotowane wyciągi

W tym przypadku, ze względu na oddzielenie zapytania i danych, dane wejściowe użytkownika nigdy nie są traktowane jako instrukcja SQL, a zatem nigdy nie są wykonywane . Z tego powodu każdy wstrzyknięty złośliwy kod SQL nie wyrządziłby żadnej szkody. Więc "DROP TABLE CUSTOMERS"nigdy nie zostałby wykonany w powyższym przypadku.

Krótko mówiąc, przy przygotowanych instrukcjach złośliwy kod wprowadzony przez użytkownika nie zostanie wykonany!

N.Vegeta
źródło
Naprawdę? Przyjęta odpowiedź nie mówi dokładnie tego?
Twój zdrowy rozsądek
@ Your Common Sense Zaakceptowana odpowiedź zawiera wiele cennych informacji, ale zastanawiałem się, na czym polegają szczegóły implementacji oddzielenia danych i zapytania. Natomiast skupianie się na tym, że złośliwie wstrzyknięte dane (jeśli takie istniały) nigdy nie zostałyby wykonane, trafia w sedno.
N.Vegeta,
A jakich „szczegółów implementacji” podałeś w swojej odpowiedzi, a których tam nie ma?
Your Common Sense
jeśli spróbujesz zobaczyć, skąd pochodzę, zdasz sobie sprawę, że mój punkt widzenia jest następujący: Krótkie pragnienie zobaczenia szczegółów implementacji wynikało z potrzeby zrozumienia wyraźnego powodu, dla którego dane wejściowe złośliwego użytkownika nie spowodują zaszkodzić. Nie tyle potrzeba zobaczenia szczegółów implementacji. Dlatego zdając sobie sprawę, że szczegóły implementacji były takie, że w żadnym momencie złośliwie wprowadzony kod SQL nie zostanie wykonany, wysłano wiadomość do domu. Twoja odpowiedź odpowiada na pytanie, jak (zgodnie z prośbą)?, Ale wyobrażam sobie, że inni ludzie (tacy jak ja) byliby usatysfakcjonowani zwięzłą odpowiedzią na pytanie, dlaczego?
N.Vegeta,
Potraktuj to jako wzbogacenie, które wyjaśnia sedno, a nie jako dorozumianą krytykę (ponownie zdałem sobie sprawę, kto był autorem zaakceptowanej odpowiedzi).
N.Vegeta,
5

Kiedy tworzysz i wysyłasz przygotowaną instrukcję do DBMS, jest ona przechowywana jako zapytanie SQL do wykonania.

Później łączysz swoje dane z zapytaniem w taki sposób, że DBMS używa tych danych jako parametrów zapytania do wykonania (parametryzacji). DBMS nie używa danych, które łączysz jako uzupełnienie już skompilowanego zapytania SQL; to po prostu dane.

Oznacza to, że zasadniczo niemożliwe jest wykonanie wstrzyknięcia SQL przy użyciu przygotowanych instrukcji. Sam charakter przygotowanych instrukcji i ich związek z DBMS temu zapobiega.

wulfgarpro
źródło
4

W SQL Server użycie przygotowanej instrukcji jest zdecydowanie odporne na wstrzyknięcie, ponieważ parametry wejściowe nie tworzą zapytania. Oznacza to, że wykonane zapytanie nie jest zapytaniem dynamicznym. Przykład instrukcji podatnej na iniekcję SQL.

string sqlquery = "select * from table where username='" + inputusername +"' and password='" + pass + "'";

Teraz, jeśli wartość w zmiennej inoutusername jest czymś w rodzaju 'lub 1 = 1 -, to zapytanie staje się teraz:

select * from table where username='a' or 1=1 -- and password=asda

A reszta jest komentowana później --, więc nigdy nie jest wykonywana i pomijana, jak przy użyciu przygotowanego przykładu instrukcji, jak poniżej.

Sqlcommand command = new sqlcommand("select * from table where username = @userinput and password=@pass");
command.Parameters.Add(new SqlParameter("@userinput", 100));
command.Parameters.Add(new SqlParameter("@pass", 100));
command.prepare();

W efekcie nie możesz wysłać kolejnego parametru, unikając w ten sposób iniekcji SQL ...

lloydom
źródło
3

Kluczową frazą jest need not be correctly escaped. Oznacza to, że nie musisz się martwić, że ludzie będą próbować wstawiać myślniki, apostrofy, cytaty itp.

Wszystko załatwiamy za Ciebie.

Zadziorna Mango
źródło
2
ResultSet rs = statement.executeQuery("select * from foo where value = " + httpRequest.getParameter("filter");

Załóżmy, że masz to w serwlecie, który masz rację. Jeśli złośliwa osoba przekaże złą wartość dla „filtra”, możesz zhakować bazę danych.

MeBigFatGuy
źródło
0

Podstawowa przyczyna nr 1 - problem z separatorem

Wstrzyknięcie Sql jest możliwe, ponieważ używamy cudzysłowów do oddzielania ciągów, a także jako części ciągów, co czasami uniemożliwia ich interpretację. Gdybyśmy mieli ograniczniki, których nie można użyć w danych łańcuchowych, wstrzyknięcie sql nigdy by się nie wydarzyło. Rozwiązanie problemu z separatorem eliminuje problem z wtryskiem sql. Robią to zapytania strukturalne.

Przyczyna nr 2 - Ludzka natura, ludzie są przebiegli, niektórzy przebiegli ludzie są złośliwi i wszyscy popełniają błędy

Inną główną przyczyną iniekcji sql jest natura ludzka. Ludzie, w tym programiści, popełniają błędy. Gdy popełnisz błąd w zapytaniu strukturalnym, nie narazi to systemu na wstrzyknięcie sql. Jeśli nie używasz zapytań strukturalnych, błędy mogą generować lukę w zabezpieczeniach sql injection.

Jak zapytania strukturalne rozwiązują podstawowe przyczyny iniekcji SQL

Zapytania strukturalne rozwiązują problem separatora, umieszczając polecenia sql w jednej instrukcji i umieszczając dane w oddzielnej instrukcji programowania. Instrukcje programowania tworzą potrzebną separację.

Zapytania strukturalne pomagają zapobiegać powstawaniu krytycznych luk w zabezpieczeniach przez błędy ludzkie. Jeśli chodzi o ludzi popełniających błędy, wstrzyknięcie sql nie może nastąpić, gdy używane są zapytania strukturalne. Istnieją sposoby zapobiegania wstrzykiwaniu sql, które nie obejmują zapytań strukturalnych, ale normalny błąd ludzki w takich podejściach zwykle prowadzi do przynajmniej częściowego narażenia na wstrzyknięcie sql. Zapytania strukturalne są odporne na awarie po wstrzyknięciu sql. Możesz popełnić wszystkie błędy świata, prawie za pomocą zapytań strukturalnych, tak samo jak w przypadku każdego innego programowania, ale żadnego, który możesz zrobić, nie można przekształcić w ssstem przejęty przez wstrzyknięcie sql. Dlatego ludzie lubią mówić, że jest to właściwy sposób zapobiegania wstrzykiwaniu sql.

Więc masz to, przyczyny wstrzyknięcia sql i kwerendy strukturalne natury, które uniemożliwiają ich użycie.

DanAllen
źródło