Jak mogę zapobiec iniekcji SQL w PHP?


2773

Jeśli dane wejściowe użytkownika zostaną wstawione bez modyfikacji w zapytaniu SQL, aplikacja stanie się podatna na wstrzyknięcie SQL , jak w poniższym przykładzie:

$unsafe_variable = $_POST['user_input']; 

mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");

Jest tak, ponieważ użytkownik może wprowadzić coś takiego value'); DROP TABLE table;--, a zapytanie staje się:

INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')

Co można zrobić, aby temu zapobiec?

Odpowiedzi:


8955

Używaj przygotowanych instrukcji i sparametryzowanych zapytań. Są to instrukcje SQL wysyłane i analizowane przez serwer bazy danych niezależnie od jakichkolwiek parametrów. W ten sposób atakujący nie może wstrzyknąć złośliwego kodu SQL.

Zasadniczo masz dwie opcje, aby to osiągnąć:

  1. Za pomocą PDO (dla dowolnego obsługiwanego sterownika bazy danych):

    $stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
    
    $stmt->execute([ 'name' => $name ]);
    
    foreach ($stmt as $row) {
        // Do something with $row
    }
    
  2. Za pomocą MySQLi (dla MySQL):

    $stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
    $stmt->bind_param('s', $name); // 's' specifies the variable type => 'string'
    
    $stmt->execute();
    
    $result = $stmt->get_result();
    while ($row = $result->fetch_assoc()) {
        // Do something with $row
    }
    

Jeśli łączysz się z bazą danych innych niż MySQL, istnieje druga opcja kierowca specyficzne, które mogą odnosić się do (na przykład, pg_prepare()i pg_execute()PostgreSQL). ChNP jest opcją uniwersalną.


Prawidłowe skonfigurowanie połączenia

Zauważ, że podczas PDOuzyskiwania dostępu do bazy danych MySQL rzeczywiste przygotowane instrukcje niedomyślnie używane . Aby to naprawić, musisz wyłączyć emulację przygotowanych instrukcji. Przykładem utworzenia połączenia za pomocą PDO jest:

$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'password');

$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

W powyższym przykładzie tryb błędu nie jest absolutnie konieczny, ale zaleca się jego dodanie . W ten sposób skrypt nie zatrzyma się, Fatal Errorgdy coś pójdzie nie tak. I daje to deweloperowi szansę na catchkażdy błąd (błędy), które są throwrówne n PDOException.

Obowiązkowa jest jednak pierwsza setAttribute()linia, która mówi PDO o wyłączeniu emulowanych przygotowanych instrukcji i użyciu rzeczywistych przygotowanych instrukcji. Dzięki temu instrukcja i wartości nie są analizowane przez PHP przed wysłaniem ich na serwer MySQL (nie dając potencjalnemu napastnikowi szansy na wstrzyknięcie złośliwego SQL).

Chociaż możesz ustawić charsetopcje w konstruktorze, ważne jest, aby pamiętać, że „starsze” wersje PHP (przed 5.3.6) cicho ignorowały parametr charset w DSN.


Wyjaśnienie

Przekazana instrukcja SQL preparejest analizowana i kompilowana przez serwer bazy danych. Określając parametry (albo ?parametr nazwany, jak :namew powyższym przykładzie), informujesz aparat bazy danych, gdzie chcesz filtrować. Następnie podczas połączenia executeprzygotowana instrukcja jest łączona z określonymi wartościami parametrów.

Ważne jest tutaj to, że wartości parametrów są łączone ze skompilowaną instrukcją, a nie ciągiem SQL. Wstrzykiwanie SQL polega na oszukiwaniu skryptu w celu uwzględnienia złośliwych ciągów podczas tworzenia kodu SQL w celu wysłania go do bazy danych. Wysyłając rzeczywisty kod SQL oddzielnie od parametrów, ograniczasz ryzyko, że skończysz z czymś, czego nie zamierzałeś.

Wszelkie parametry wysyłane podczas korzystania z przygotowanej instrukcji będą traktowane jak łańcuchy (chociaż silnik bazy danych może przeprowadzić pewną optymalizację, więc parametry mogą oczywiście również zostać przekształcone w liczby). W powyższym przykładzie, jeśli $namezmienna zawiera 'Sarah'; DELETE FROM employeeswynik, byłoby po prostu wyszukiwaniem łańcucha "'Sarah'; DELETE FROM employees"i nie skończy się pustą tabelą .

Kolejną zaletą korzystania z przygotowanych instrukcji jest to, że jeśli wykonasz tę samą instrukcję wiele razy w tej samej sesji, zostanie ona przeanalizowana i skompilowana tylko raz, co da ci pewne przyspieszenie.

Aha, a skoro zapytałeś, jak to zrobić dla wstawki, oto przykład (przy użyciu PDO):

$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');

$preparedStatement->execute([ 'column' => $unsafeValue ]);

Czy przygotowane zapytania mogą być używane do zapytań dynamicznych?

Chociaż nadal można używać przygotowanych instrukcji dla parametrów zapytania, nie można sparametryzować samej struktury zapytania dynamicznego i nie można sparametryzować niektórych funkcji zapytania.

W tych konkretnych scenariuszach najlepiej jest użyć filtru białej listy, który ogranicza możliwe wartości.

// Value whitelist
// $dir can only be 'DESC', otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
   $dir = 'ASC';
}

86
Wystarczy dodać, ponieważ nie widziałem go nigdzie indziej, inną linią obrony jest zapora ogniowa aplikacji sieci Web (WAF), w której można ustawić reguły wyszukiwania ataków typu sql injection:
jkerak

37
Ponadto, oficjalna dokumentacja mysql_query pozwala na wykonanie tylko jednego zapytania, a więc każdego innego oprócz; jest ignorowany. Nawet jeśli jest to już przestarzałe, w PHP 5.5.0 jest wiele systemów, które mogą korzystać z tej funkcji. php.net/manual/en/function.mysql-query.php
Randall Valenciano

12
Jest to zły nawyk, ale stanowi rozwiązanie problemu: Nie tylko w przypadku wstrzykiwania SQL, ale w przypadku wszelkiego rodzaju zastrzyków (na przykład w F3 Framework v2 istniała dziura do wstrzykiwania szablonu widoku), jeśli masz już gotową starą witrynę lub aplikację od wad wtryskowych, jednym z rozwiązań jest ponowne przypisanie wartości swoich predefiniowanych zmiennych supperglobalnych, takich jak $ _POST, z wartościami ucieczki podczas ładowania początkowego. Przez PDO nadal można uciec (także dla dzisiejszych frameworków): substr ($ pdo-> quote ($ str, \ PDO :: PARAM_STR), 1, -1)
Alix

15
W tej odpowiedzi nie ma wyjaśnienia, co to jest przygotowane oświadczenie - jedno - jest to uderzenie wydajnościowe, jeśli używasz wielu przygotowanych oświadczeń podczas żądania, a czasami stanowi 10-krotny spadek wydajności. Lepszym rozwiązaniem byłoby użycie PDO z wyłączonym wiązaniem parametrów, ale bez przygotowania instrukcji.
donis,

6
Korzystanie z PDO jest lepsze, jeśli używasz bezpośredniego zapytania, upewnij się, że używasz mysqli :: escape_string
Kassem Itani,

1652

Przestarzałe ostrzeżenie: Przykładowy kod tej odpowiedzi (podobnie jak przykładowy kod pytania) korzysta z MySQLrozszerzenia PHP , które zostało przestarzałe w PHP 5.5.0 i całkowicie usunięte w PHP 7.0.0.

Ostrzeżenie bezpieczeństwa : ta odpowiedź jest niezgodna z najlepszymi praktykami bezpieczeństwa. Ucieczka jest nieodpowiednia, aby zapobiec wstrzyknięciu SQL , zamiast tego użyj przygotowanych instrukcji . Skorzystaj ze strategii przedstawionej poniżej na własne ryzyko. (Również mysql_real_escape_string()został usunięty w PHP 7.)

Jeśli używasz najnowszej wersji PHP, mysql_real_escape_stringopcja opisana poniżej nie będzie już dostępna (choć mysqli::escape_stringjest nowoczesnym odpowiednikiem). Obecnie mysql_real_escape_stringopcja ta miałaby sens tylko w przypadku starszych wersji kodu PHP.


Masz dwie opcje - unikanie znaków specjalnych unsafe_variablelub użycie sparametryzowanego zapytania. Oba chroniłyby cię przed iniekcją SQL. Sparametryzowane zapytanie jest uważane za lepszą praktykę, ale będzie wymagać zmiany na nowsze rozszerzenie MySQL w PHP, zanim będzie można go użyć.

Najpierw zakryjemy strunę udarową, która ucieka przed nią.

//Connect

$unsafe_variable = $_POST["user-input"];
$safe_variable = mysql_real_escape_string($unsafe_variable);

mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

//Disconnect

Zobacz także szczegóły mysql_real_escape_stringfunkcji.

Aby użyć sparametryzowanego zapytania, musisz użyć MySQLi zamiast funkcji MySQL . Aby przepisać twój przykład, potrzebowalibyśmy czegoś takiego.

<?php
    $mysqli = new mysqli("server", "username", "password", "database_name");

    // TODO - Check that connection was successful.

    $unsafe_variable = $_POST["user-input"];

    $stmt = $mysqli->prepare("INSERT INTO table (column) VALUES (?)");

    // TODO check that $stmt creation succeeded

    // "s" means the database expects a string
    $stmt->bind_param("s", $unsafe_variable);

    $stmt->execute();

    $stmt->close();

    $mysqli->close();
?>

Oto kluczowa funkcja, którą chcesz przeczytać mysqli::prepare.

Ponadto, jak sugerują inni, może okazać się użyteczne / łatwiejsze zwiększenie warstwy abstrakcji za pomocą czegoś takiego jak PDO .

Pamiętaj, że sprawa, o którą pytałeś, jest dość prosta i że bardziej złożone sprawy mogą wymagać bardziej skomplikowanego podejścia. W szczególności:

  • Jeśli chcesz zmienić strukturę SQL na podstawie danych wprowadzonych przez użytkownika, sparametryzowane zapytania nie pomogą, a wymagane zmiany nie są objęte mysql_real_escape_string. W takim przypadku lepiej byłoby przekazać dane użytkownika przez białą listę, aby zapewnić, że dozwolone są tylko „bezpieczne” wartości.
  • Jeśli użyjesz liczb całkowitych z danych wejściowych użytkownika w danym stanie i mysql_real_escape_stringzastosujesz podejście, będziesz cierpieć z powodu problemu opisanego przez Polynomial w komentarzach poniżej. Ten przypadek jest trudniejszy, ponieważ liczby całkowite nie będą otoczone cudzysłowami, więc można sobie z tym poradzić, sprawdzając, czy dane wejściowe użytkownika zawierają tylko cyfry.
  • Są prawdopodobnie inne przypadki, o których nie wiem. Może się okazać, że jest to przydatne źródło informacji o niektórych bardziej subtelnych problemach, jakie możesz napotkać.

1
przy użyciu mysql_real_escape_stringwystarczy czy muszę używać parametryzowane też?
Peiman F.,

5
@peimanF. utrzymuj dobrą praktykę korzystania ze sparametryzowanych zapytań, nawet w lokalnym projekcie. Dzięki parametryzowanym zapytaniom masz gwarancję, że nie nastąpi zastrzyk SQL. Pamiętaj jednak, że należy zdezynfekować dane, aby uniknąć fałszywego pobierania (tj. Wstrzyknięcia XSS, takiego jak wstawianie kodu HTML w tekście), htmlentitiesna przykład
Goufalite

2
@peimanF. Dobra praktyka w parametryzowaniu zapytań i wiązaniu wartości, ale prawdziwy ciąg znaków ucieczki jest na razie dobry
Richard

Rozumiem włączenie mysql_real_escape_string()kompletności, ale nie jestem fanem wymieniania najpierw najbardziej podatnego na błędy podejścia. Czytelnik może po prostu szybko złapać pierwszy przykład. Dobrze, że teraz jest przestarzałe :)
Steen Schütt

1
@ SteenSchütt - Wszystkie mysql_*funkcje są przestarzałe. Zostały one zastąpione podobnymi mysqli_* funkcjami, takimi jak mysqli_real_escape_string.
Rick James

1073

Każda odpowiedź tutaj obejmuje tylko część problemu. W rzeczywistości istnieją cztery różne części zapytania, które możemy dynamicznie dodawać do SQL: -

  • sznurek
  • numer
  • identyfikator
  • słowo kluczowe składni

A przygotowane oświadczenia obejmują tylko dwa z nich.

Ale czasami musimy uczynić nasze zapytanie jeszcze bardziej dynamicznym, dodając również operatory lub identyfikatory. Będziemy potrzebować różnych technik ochrony.

Ogólnie rzecz biorąc, takie podejście do ochrony opiera się na białej liście .

W takim przypadku każdy parametr dynamiczny powinien być zakodowany na stałe w skrypcie i wybrany z tego zestawu. Na przykład, aby wykonać dynamiczne porządkowanie:

$orders  = array("name", "price", "qty"); // Field names
$key = array_search($_GET['sort'], $orders)); // if we have such a name
$orderby = $orders[$key]; // If not, first one will be set automatically. 
$query = "SELECT * FROM `table` ORDER BY $orderby"; // Value is safe

Aby ułatwić ten proces, napisałem funkcję pomocnika na białej liście, która wykonuje całą pracę w jednym wierszu:

$orderby = white_list($_GET['orderby'], "name", ["name","price","qty"], "Invalid field name");
$query  = "SELECT * FROM `table` ORDER BY `$orderby`"; // sound and safe

Istnieje inny sposób zabezpieczenia identyfikatorów - ucieczka, ale raczej trzymam się białej listy jako bardziej niezawodnego i wyraźnego podejścia. Jednak dopóki masz podany identyfikator, możesz uciec od znaku cytatu, aby był bezpieczny. Na przykład domyślnie dla mysql musisz podwoić znak cudzysłowu, aby go uciec . W przypadku innych innych reguł ucieczki DBMS byłyby inne.

Mimo to, istnieje problem ze składni SQL słów kluczowych (takich jak AND, DESCi takie), ale biało-wymieniając tylko podejście wydaje się w tym przypadku.

Tak więc ogólne zalecenie można sformułować jako

  • Każda zmienna reprezentująca literał danych SQL (lub, mówiąc prosto - ciąg SQL lub liczbę) musi zostać dodana za pomocą przygotowanej instrukcji. Bez wyjątków.
  • Wszelkie inne części zapytania, takie jak słowo kluczowe SQL, tabela lub nazwa pola lub operator - muszą zostać przefiltrowane przez białą listę.

Aktualizacja

Chociaż istnieje ogólna zgoda co do najlepszych praktyk dotyczących ochrony przed wstrzykiwaniem SQL, nadal istnieje również wiele złych praktyk. A niektóre z nich są zbyt głęboko zakorzenione w umysłach użytkowników PHP. Na przykład na tej samej stronie znajduje się (choć niewidoczna dla większości odwiedzających) ponad 80 usuniętych odpowiedzi - wszystkie usunięte przez społeczność z powodu złej jakości lub promujące złe i nieaktualne praktyki. Co gorsza, niektóre złe odpowiedzi nie są usuwane, ale mają się dobrze.

Na przykład (1) jest (2) wciąż (3) wiele (4) odpowiedzi (5) , w tym druga najbardziej pozytywna odpowiedź sugerująca ręczne ucieczkę łańcucha - przestarzałe podejście, które okazało się niepewne.

Lub istnieje nieco lepsza odpowiedź, która sugeruje tylko inną metodę formatowania łańcucha, a nawet chwali się nią jako ostatecznym panaceum. Oczywiście tak nie jest. Ta metoda nie jest lepsza niż zwykłe formatowanie ciągów, ale zachowuje wszystkie swoje wady: ma zastosowanie tylko do ciągów i, jak każde inne ręczne formatowanie, jest zasadniczo opcjonalna, nieobowiązkowa, podatna na wszelkiego rodzaju błędy ludzkie.

Myślę, że wszystko to z powodu jednego bardzo starego przesądu, wspieranego przez takie autorytety, jak OWASP lub podręcznik PHP , który głosi równość między „ucieczką” a ochroną przed wstrzyknięciami SQL.

Niezależnie od tego, co od dawna mówi PHP, *_escape_stringw żadnym wypadku nie zapewnia bezpieczeństwa danych i nigdy nie było zamierzone. Oprócz tego, że jest bezużyteczny dla jakiejkolwiek części SQL innej niż łańcuch, ręczne zmiany znaczenia są niepoprawne, ponieważ są ręczne, w przeciwieństwie do automatycznych.

A OWASP czyni go jeszcze gorszym, kładąc nacisk na ucieczkę od danych wejściowych użytkownika, co jest całkowitym nonsensem: nie powinno być takich słów w kontekście ochrony przed wstrzyknięciem. Każda zmienna jest potencjalnie niebezpieczna - bez względu na źródło! Innymi słowy - każda zmienna musi być odpowiednio sformatowana, aby mogła zostać wprowadzona do zapytania - bez względu na źródło ponownie. Liczy się cel. W momencie, gdy programista zaczyna oddzielać owce od kóz (zastanawiając się, czy jakaś konkretna zmienna jest „bezpieczna”, czy nie), stawia swój pierwszy krok w kierunku katastrofy. Nie wspominając już o tym, że nawet sformułowanie sugeruje masową ucieczkę w punkcie wejścia, przypominając bardzo magiczną funkcję cytatów - już pogardzaną, przestarzałą i usuniętą.

Tak więc, w przeciwieństwie do „uciekających”, przygotowanych instrukcji jest miarą, która rzeczywiście chroni przed wstrzyknięciem SQL (jeśli dotyczy).


848

Polecam używanie PDO (PHP Data Objects) do uruchamiania sparametryzowanych zapytań SQL.

Nie tylko chroni to przed iniekcją SQL, ale także przyspiesza zapytania.

I za pomocą PDO zamiast mysql_, mysqli_i pgsql_funkcji, należy złożyć trochę bardziej oderwane od bazy danych, w związku z rzadkim występowaniem, że masz do baz danych dostawców przełączników.


619

Użyj PDOi przygotowane zapytania.

( $connjest PDOprzedmiotem)

$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)");
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
$stmt->execute();

549

Jak widać, ludzie sugerują, abyś używał co najwyżej przygotowanych wypowiedzi. Nie jest to złe, ale gdy zapytanie jest wykonywane tylko raz na proces, może wystąpić niewielka utrata wydajności.

Miałem do czynienia z tym problemem, ale myślę, że rozwiązałem go w bardzo wyrafinowany sposób - w sposób, w jaki hakerzy używają, aby uniknąć używania cudzysłowów. Użyłem tego w połączeniu z emulowanymi przygotowanymi oświadczeniami. Używam go, aby zapobiegać wszelkim możliwym atakom typu SQL injection.

Moje podejście:

  • Jeśli spodziewasz się, że wejście będzie liczbą całkowitą, upewnij się, że naprawdę jest liczbą całkowitą. W języku zmiennym, takim jak PHP, jest to bardzo ważne. Możesz na przykład użyć tego bardzo prostego, ale potężnego rozwiązania:sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input);

  • Jeśli oczekujesz czegoś innego od liczby całkowitej, użyj heksa . Jeśli przeklniesz go, całkowicie unikniesz wprowadzania danych. W C / C ++ istnieje funkcja o nazwie mysql_hex_string(), w PHP można użyć bin2hex().

    Nie martw się, że łańcuch znaków będzie miał rozmiar 2x oryginalnej długości, ponieważ nawet jeśli użyjesz mysql_real_escape_string, PHP musi przydzielić taką samą pojemność ((2*input_length)+1), która jest taka sama.

  • Ta metoda heksadecymalna jest często używana podczas przesyłania danych binarnych, ale nie widzę powodu, dla którego miałaby nie używać jej do wszystkich danych, aby zapobiec atakom iniekcyjnym SQL. Pamiętaj, że musisz dodać dane za 0xpomocą funkcji MySQL lub UNHEXzamiast tego.

Na przykład zapytanie:

SELECT password FROM users WHERE name = 'root'

Stanie się:

SELECT password FROM users WHERE name = 0x726f6f74

lub

SELECT password FROM users WHERE name = UNHEX('726f6f74')

Hex to idealna ucieczka. Nie ma możliwości wstrzyknięcia.

Różnica między funkcją UNHEX a prefiksem 0x

W komentarzach była dyskusja, więc w końcu chcę to wyjaśnić. Te dwa podejścia są bardzo podobne, ale pod pewnymi względami są nieco inne:

Prefiksu ** 0x ** można używać tylko w przypadku kolumn danych, takich jak char, varchar, text, block, binary itp .
Również jego użycie jest nieco skomplikowane, jeśli masz zamiar wstawić pusty ciąg. Musisz całkowicie go zastąpić '', w przeciwnym razie pojawi się błąd.

UNHEX () działa na dowolnej kolumnie; nie musisz się martwić o pusty ciąg.


Metody szesnastkowe są często używane jako ataki

Zauważ, że ta metoda heksadecymalna jest często używana jako atak iniekcyjny SQL, w którym liczby całkowite są jak łańcuchy i są poprzedzane znakami ucieczki mysql_real_escape_string. Następnie możesz uniknąć stosowania cytatów.

Na przykład, jeśli po prostu zrobisz coś takiego:

"SELECT title FROM article WHERE id = " . mysql_real_escape_string($_GET["id"])

atak może cię bardzo łatwo wstrzyknąć . Rozważ następujący wstrzyknięty kod zwrócony ze skryptu:

WYBIERZ ... GDZIE id = -1 unia wszystkie wybierz nazwa_tabeli z tabeli_informacji

a teraz po prostu wyodrębnij strukturę tabeli:

WYBIERZ ... GDZIE id = -1 unia wszystkie wybierz nazwa_kolumny ze schematu_informacji.kolumna, gdzie nazwa_tabeli = 0x61727469636c65

A następnie wybierz dane, które chcesz. Czy to nie fajne?

Ale jeśli koder witryny do wstrzykiwania by ją przeklął, nie byłoby to możliwe, ponieważ zapytanie wyglądałoby tak: SELECT ... WHERE id = UNHEX('2d312075...3635')


6
@SumitGupta Tak, zrobiłeś. MySQL nie łączy się z, +ale z CONCAT. I do wydajności: nie sądzę, że wpływa to na wydajność, ponieważ mysql musi analizować dane i nie ma znaczenia, czy pochodzenie jest łańcuchem, czy szesnastkiem
Zaffy

11
@YourCommonSense Nie rozumiesz pojęcia ... Jeśli chcesz mieć ciąg w mysql, zacytuj go w ten sposób 'root'lub możesz go przekreślić, 0x726f6f74ALE jeśli chcesz numer i wysłać go jako ciąg, prawdopodobnie napiszesz „42”, a nie CHAR (42 ) ... '42' w hex byłoby 0x3432nie0x42
Zaffy

7
@ YourCommonSense Nie mam nic do powiedzenia ... po prostu lol ... jeśli nadal chcesz wypróbować szesnastkę na polach numerycznych, zobacz drugi komentarz. Założę się, że to zadziała.
Zaffy

2
Odpowiedź jasno stwierdza, że ​​nie zadziała w ten sposób z wartościami całkowitymi, ponieważ bin2hex konwertuje przekazaną wartość na ciąg (a zatem bin2hex (0) ma wartość 0x30, a nie 0x03) - prawdopodobnie jest to część, która myli cię . Jeśli to zrobisz, działa idealnie (przynajmniej na mojej stronie, przetestowane z 4 różnymi wersjami mysql na maszynach debian, od 5.1.x do 5.6.x). W końcu szesnastkowy jest tylko sposobem reprezentacji, a nie wartością;)
griffin

5
@ YourCommonSense, którego nadal nie rozumiesz? Nie można użyć 0x i concat, ponieważ jeśli łańcuch jest pusty, zakończy się błąd. Jeśli chcesz prostą alternatywę dla swojego zapytania, spróbuj tegoSELECT title FROM article WHERE id = UNHEX(' . bin2hex($_GET["id"]) . ')
Zaffy

499

Przestarzałe ostrzeżenie: Przykładowy kod tej odpowiedzi (podobnie jak przykładowy kod pytania) korzysta z MySQLrozszerzenia PHP , które zostało przestarzałe w PHP 5.5.0 i całkowicie usunięte w PHP 7.0.0.

Ostrzeżenie bezpieczeństwa : ta odpowiedź jest niezgodna z najlepszymi praktykami bezpieczeństwa. Ucieczka jest nieodpowiednia, aby zapobiec wstrzyknięciu SQL , zamiast tego użyj przygotowanych instrukcji . Skorzystaj ze strategii przedstawionej poniżej na własne ryzyko. (Również mysql_real_escape_string()został usunięty w PHP 7.)

WAŻNY

Najlepszym sposobem zapobiegania iniekcji SQL jest użycie przygotowanych instrukcji zamiast ucieczki , jak pokazuje zaakceptowana odpowiedź .

Istnieją biblioteki takie jak Aura.Sql i EasyDB, które pozwalają programistom łatwiej korzystać z przygotowanych instrukcji. Aby dowiedzieć się więcej o tym, dlaczego przygotowane instrukcje lepiej powstrzymują wstrzykiwanie SQL , zapoznaj się z tym mysql_real_escape_string()obejściem i ostatnio naprawionymi lukami w zabezpieczeniach SQLode w Unicode w WordPress .

Zapobieganie wstrzyknięciom - mysql_real_escape_string ()

PHP ma specjalnie przygotowaną funkcję, która zapobiega takim atakom. Wszystko, co musisz zrobić, to użyć łyk z funkcji mysql_real_escape_string.

mysql_real_escape_stringpobiera ciąg znaków, który będzie używany w zapytaniu MySQL i zwraca ten sam ciąg przy bezpiecznej ucieczce wszystkich prób wstrzyknięcia SQL. Zasadniczo zastąpi te kłopotliwe cytaty ('), które użytkownik może wprowadzić, zastępując je cytatem bezpiecznym dla MySQL, cytatem \.

UWAGA: aby korzystać z tej funkcji, musisz być podłączony do bazy danych!

// Połącz się z MySQL

$name_bad = "' OR 1'"; 

$name_bad = mysql_real_escape_string($name_bad);

$query_bad = "SELECT * FROM customers WHERE username = '$name_bad'";
echo "Escaped Bad Injection: <br />" . $query_bad . "<br />";


$name_evil = "'; DELETE FROM customers WHERE 1 or username = '"; 

$name_evil = mysql_real_escape_string($name_evil);

$query_evil = "SELECT * FROM customers WHERE username = '$name_evil'";
echo "Escaped Evil Injection: <br />" . $query_evil;

Więcej informacji można znaleźć w MySQL - Zapobieganie iniekcji SQL .


30
Jest to najlepsze, co możesz zrobić ze starszym rozszerzeniem mysql. W przypadku nowego kodu zaleca się przejście na mysqli lub PDO.
Álvaro González

7
Nie zgadzam się z tą „specjalną funkcją zapobiegającą tym atakom”. Myślę, że mysql_real_escape_stringcelem jest umożliwienie zbudowania poprawnego zapytania SQL dla każdego wejściowego ciągu danych. Zapobieganie iniekcji sql jest efektem ubocznym tej funkcji.
Sekcja

4
nie używasz funkcji do zapisywania poprawnych ciągów danych wejściowych. Po prostu piszesz poprawne, które nie wymagają ucieczki lub już uciekły. mysql_real_escape_string () mógł zostać zaprojektowany w celu, o którym wspominasz, ale jego jedyną wartością jest zapobieganie iniekcji.
Nazca

17
OSTRZEŻENIE! mysql_real_escape_string() nie jest nieomylny .
eggyal

9
mysql_real_escape_stringjest teraz przestarzałe, więc nie jest już realną opcją. W przyszłości zostanie usunięty z PHP. Najlepiej przejść do tego, co polecają ludzie PHP lub MySQL.
jww

461

Możesz zrobić coś takiego podstawowego:

$safe_variable = mysqli_real_escape_string($_POST["user-input"], $dbConnection);
mysqli_query($dbConnection, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

To nie rozwiąże każdego problemu, ale jest to bardzo dobry odskocznia. Pominąłem oczywiste elementy, takie jak sprawdzenie istnienia zmiennej, jej formatu (cyfry, litery itp.).


28
Wypróbowałem twój przykład i działa dla mnie dobrze. Czy możesz wyjaśnić, że „to nie rozwiąże każdego problemu”
Chinook

14
Jeśli nie zacytujesz łańcucha, nadal można go wstrzykiwać. Weźmy $q = "SELECT col FROM tbl WHERE x = $safe_var";na przykład. Ustawienie $safe_vardo 1 UNION SELECT password FROM usersprac w tym przypadku, ze względu na brak cytatów. Możliwe jest również wstrzyknięcie ciągów do zapytania za pomocą CONCATi CHR.
Wielomian

4
@Polynomial Całkowicie słuszne, ale uznałbym to za niewłaściwe użycie. Tak długo, jak używasz go poprawnie, na pewno będzie działać.
glglgl

22
OSTRZEŻENIE! mysql_real_escape_string() nie jest nieomylny .
eggyal

8
mysql_real_escape_stringjest teraz przestarzałe, więc nie jest już realną opcją. W przyszłości zostanie usunięty z PHP. Najlepiej przejść do tego, co polecają ludzie PHP lub MySQL.
jww

380

Cokolwiek zrobisz, skorzystaj z tego, upewnij się, że sprawdziłeś, czy dane wejściowe nie zostały już zniekształcone przez magic_quotesinne śmieci o dobrym znaczeniu, i jeśli to konieczne, przejrzyj je stripslasheslub cokolwiek, aby je zdezynfekować.


11
W rzeczy samej; bieganie z włączonymi magic_quotes tylko zachęca do złej praktyki. Czasami jednak nie zawsze można kontrolować środowisko do tego poziomu - albo nie masz dostępu do zarządzania serwerem, albo twoja aplikacja musi współistnieć z aplikacjami, które (dreszcz) zależą od takiej konfiguracji. Z tych powodów dobrze jest pisać aplikacje przenośne - chociaż oczywiście wysiłek jest marnowany, jeśli kontrolujesz środowisko wdrażania, np. Ponieważ jest to aplikacja wewnętrzna lub będzie używana tylko w określonym środowisku.
Rob

24
Począwszy od PHP 5.4, obrzydliwość zwana „magicznymi cytatami” została zabita martwa . I dobry brak złych śmieci.
BryanH

363

Przestarzałe ostrzeżenie: Przykładowy kod tej odpowiedzi (podobnie jak przykładowy kod pytania) korzysta z MySQLrozszerzenia PHP , które zostało przestarzałe w PHP 5.5.0 i całkowicie usunięte w PHP 7.0.0.

Ostrzeżenie bezpieczeństwa : ta odpowiedź jest niezgodna z najlepszymi praktykami bezpieczeństwa. Ucieczka jest nieodpowiednia, aby zapobiec wstrzyknięciu SQL , zamiast tego użyj przygotowanych instrukcji . Skorzystaj ze strategii przedstawionej poniżej na własne ryzyko. (Również mysql_real_escape_string()został usunięty w PHP 7.)

Sparametryzowane zapytanie ORAZ sprawdzanie poprawności danych wejściowych jest właściwą drogą. Istnieje wiele scenariuszy, w których może wystąpić zastrzyk SQL, mimo że mysql_real_escape_string()został użyty.

Te przykłady są podatne na wstrzyknięcie SQL:

$offset = isset($_GET['o']) ? $_GET['o'] : 0;
$offset = mysql_real_escape_string($offset);
RunQuery("SELECT userid, username FROM sql_injection_test LIMIT $offset, 10");

lub

$order = isset($_GET['o']) ? $_GET['o'] : 'userid';
$order = mysql_real_escape_string($order);
RunQuery("SELECT userid, username FROM sql_injection_test ORDER BY `$order`");

W obu przypadkach nie można użyć 'do ochrony enkapsulacji.

Źródło : Nieoczekiwany zastrzyk SQL (gdy ucieczka nie jest wystarczająca)


2
Możesz zapobiec iniekcji SQL, jeśli zastosujesz technikę sprawdzania poprawności danych wejściowych, w której dane wejściowe użytkownika są uwierzytelniane na podstawie zestawu zdefiniowanych reguł dotyczących długości, typu i składni, a także reguł biznesowych.
Josip Ivic

312

Moim zdaniem najlepszym sposobem, aby ogólnie zapobiec iniekcji SQL w twojej aplikacji PHP (lub dowolnej innej aplikacji internetowej) jest przemyślenie architektury aplikacji. Jeśli jedynym sposobem ochrony przed wstrzykiwaniem SQL jest pamiętanie o zastosowaniu specjalnej metody lub funkcji, która wykonuje Prawidłowe działanie za każdym razem, gdy rozmawiasz z bazą danych, robisz to źle. W ten sposób kwestia czasu zajmuje zapomnienie o prawidłowym sformatowaniu zapytania w pewnym momencie kodu.

Przyjęcie wzorca MVC i frameworka, takiego jak CakePHP lub CodeIgniter, jest prawdopodobnie właściwą drogą: typowe zadania, takie jak tworzenie bezpiecznych zapytań do bazy danych zostały rozwiązane i centralnie zaimplementowane w takich frameworkach. Pomagają one zorganizować aplikację internetową w rozsądny sposób i sprawiają, że myślisz więcej o ładowaniu i zapisywaniu obiektów niż o bezpiecznym tworzeniu pojedynczych zapytań SQL.


5
Myślę, że twój pierwszy akapit jest ważny. Zrozumienie jest kluczowe. Ponadto, wszyscy nie pracują dla firmy. W przypadku dużej grupy ludzi ramy faktycznie są sprzeczne z ideą zrozumienia . Zapoznanie się z podstawami może nie być cenione podczas pracy w wyznaczonym terminie, ale majsterkowicze chętnie brudzą sobie ręce. Twórcy frameworków nie są tak uprzywilejowani, że wszyscy inni muszą się kłaniać i zakładać, że nigdy nie popełniają błędów. Moc podejmowania decyzji jest nadal ważna. Kto powiedział, że mój framework nie zastąpi w przyszłości żadnego innego programu?
Anthony Rutledge

@AnthonyRutledge Masz absolutną rację. Bardzo ważne jest, aby zrozumieć, co się dzieje i dlaczego. Jednak szansa, że ​​sprawdzona i aktywnie używana i opracowana platforma napotkała i rozwiązała wiele problemów i załatała już wiele luk w zabezpieczeniach, jest dość duża. Warto przyjrzeć się źródłu, aby poczuć jakość kodu. Jeśli jest to niesprawdzony bałagan, prawdopodobnie nie jest bezpieczny.
Johannes Fahrenkrug

3
Tutaj. Tutaj. Słuszne uwagi. Zgadzasz się jednak, że wiele osób może uczyć się i nauczyć się adoptować system MVC, ale nie wszyscy mogą go reprodukować ręcznie (sterowniki i serwer). Z tym punktem można posunąć się za daleko. Czy muszę zrozumieć moją kuchenkę mikrofalową przed podgrzaniem ciasteczek z masłem orzechowym i orzechami pekan, które zrobiła mi moja przyjaciółka? ;-)
Anthony Rutledge

3
@AnthonyRutledge Zgadzam się! Wydaje mi się, że przypadek użycia ma również znaczenie: czy tworzę galerię zdjęć dla mojej osobistej strony głównej, czy też buduję aplikację bankowości internetowej? W tym drugim przypadku bardzo ważne jest zrozumienie szczegółów bezpieczeństwa i tego, w jaki sposób radzi sobie z nimi struktura, której używam.
Johannes Fahrenkrug

3
Ach, wyjątek bezpieczeństwa zrób to sam, następstwo. Widzisz, jestem skłonny zaryzykować wszystko i pójść na całość. :-) Żart. Mając wystarczająco dużo czasu, ludzie mogą nauczyć się tworzyć cholernie bezpieczną aplikację. Zbyt wielu ludzi śpieszy się. Podnoszą ręce i zakładają, że ramy są bezpieczniejsze . W końcu nie mają wystarczająco dużo czasu na przetestowanie i rozszyfrowanie. Co więcej, bezpieczeństwo to dziedzina, która wymaga specjalnych badań. Nie jest to coś, co programiści wiedzą dogłębnie dzięki zrozumieniu algorytmów i wzorców projektowych.
Anthony Rutledge

298

Preferuję procedury przechowywane ( MySQL obsługuje procedury przechowywane od 5.0 ) z punktu widzenia bezpieczeństwa - zalety to -

  1. Większość baz danych (w tym MySQL ) umożliwia ograniczenie dostępu użytkowników do wykonywania procedur przechowywanych. Precyzyjna kontrola dostępu do zabezpieczeń jest przydatna, aby zapobiec eskalacji ataków przywilejów. Uniemożliwia to zagrożonym aplikacjom uruchomienie SQL bezpośrednio z bazą danych.
  2. Wyodrębniają one surowe zapytanie SQL z aplikacji, więc aplikacja ma mniej informacji o strukturze bazy danych. Utrudnia to ludziom zrozumienie podstawowej struktury bazy danych i zaprojektowanie odpowiednich ataków.
  3. Akceptują tylko parametry, więc istnieją zalety sparametryzowanych zapytań. Oczywiście - IMO nadal wymaga dezynfekcji danych wejściowych - szczególnie jeśli używasz dynamicznego SQL wewnątrz procedury składowanej.

Wady to -

  1. Są (procedury przechowywane) trudne do utrzymania i mają tendencję do bardzo szybkiego mnożenia. To sprawia, że ​​zarządzanie nimi jest problemem.
  2. Nie są one bardzo odpowiednie dla zapytań dynamicznych - jeśli są zbudowane tak, aby akceptować kod dynamiczny jako parametry, wówczas wiele zalet jest negowanych.

297

Istnieje wiele sposobów zapobiegania zastrzykom SQL i innym włamaniom SQL. Możesz go łatwo znaleźć w Internecie (wyszukiwarka Google). Oczywiście PDO jest jednym z dobrych rozwiązań. Ale chciałbym zasugerować dobre zapobieganie linkom przed wstrzyknięciem SQL.

Co to jest wstrzyknięcie SQL i jak temu zapobiec

Podręcznik PHP do wstrzykiwania SQL

Wyjaśnienie Microsoft iniekcji SQL i zapobiegania w PHP

I niektóre inne, takie jak Zapobieganie iniekcji SQL z MySQL i PHP .

Teraz, dlaczego trzeba zrobić, aby zapobiec zapytanie z SQL injection?

Chciałbym poinformować: Dlaczego staramy się zapobiegać wstrzykiwaniu SQL za pomocą krótkiego przykładu poniżej:

Zapytanie dotyczące uwierzytelnienia logowania:

$query="select * from users where email='".$_POST['email']."' and password='".$_POST['password']."' ";

Teraz, jeśli ktoś (haker) umieści

$_POST['email']= admin@emali.com' OR '1=1

i hasło cokolwiek ....

Zapytanie zostanie przeanalizowane w systemie tylko do:

$query="select * from users where email='admin@emali.com' OR '1=1';

Druga część zostanie odrzucona. Co się stanie? Nieautoryzowany użytkownik (haker) będzie mógł zalogować się jako administrator bez hasła. Teraz on / ona może zrobić wszystko, co administrator / osoba e-mail może zrobić. Zobacz, to bardzo niebezpieczne, jeśli nie zapobiega się iniekcji SQL.


267

Myślę, że jeśli ktoś chce użyć PHP i MySQL lub innego serwera dataBase:

  1. Pomyśl o nauce PDO (PHP Data Objects) - jest to warstwa dostępu do bazy danych zapewniająca jednolitą metodę dostępu do wielu baz danych.
  2. Pomyśl o nauce MySQLi
  3. Używaj natywnych funkcji PHP, takich jak: strip_tags , mysql_real_escape_string lub jeśli zmienna numeryczna, tylko (int)$foo. Przeczytaj więcej o typie zmiennych w PHP tutaj . Jeśli korzystasz z bibliotek takich jak PDO lub MySQLi, zawsze używaj PDO :: quote () i mysqli_real_escape_string () .

Przykłady bibliotek:

---- ChNP

----- Brak symboli zastępczych - gotowe do wstrzyknięcia SQL! To jest złe

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values ($name, $addr, $city)");

----- Bezimienne symbole zastępcze

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values (?, ?, ?);

----- Nazwane symbole zastępcze

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) value (:name, :addr, :city)");

--- MySQLi

$request = $mysqliConnection->prepare('
       SELECT * FROM trainers
       WHERE name = ?
       AND email = ?
       AND last_login > ?');

    $query->bind_param('first_param', 'second_param', $mail, time() - 3600);
    $query->execute();

PS :

PDO wygrywa tę bitwę z łatwością. Dzięki obsłudze dwunastu różnych sterowników baz danych i nazwanych parametrów możemy zignorować niewielki spadek wydajności i przyzwyczaić się do jego interfejsu API. Z punktu widzenia bezpieczeństwa oba są bezpieczne, o ile deweloper używa ich tak, jak powinny

Ale podczas gdy zarówno PDO, jak i MySQLi są dość szybkie, MySQLi działa nieznacznie szybciej w testach porównawczych - ~ 2,5% w przypadku nieprzygotowanych instrukcji i ~ 6,5% w przypadku przygotowanych instrukcji.

I proszę przetestować każde zapytanie do bazy danych - to lepszy sposób, aby zapobiec wstrzyknięciu.


że mysqli jest niepoprawny. Pierwszy parametr wyraża typy danych.
mickmackusa

257

Jeśli to możliwe, rzutuj typy swoich parametrów. Ale działa tylko na prostych typach, takich jak int, bool i float.

$unsafe_variable = $_POST['user_id'];

$safe_variable = (int)$unsafe_variable ;

mysqli_query($conn, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

3
Jest to jeden z niewielu przypadków, w których użyłbym „wartości ucieczki” zamiast przygotowanej instrukcji. Konwersja typu na liczby całkowite jest niezwykle wydajna.
HoldOffHunger,

233

Jeśli chcesz skorzystać z mechanizmów pamięci podręcznej, takich jak Redis lub Memcached , być może DALMP może być wyborem. Używa czystego MySQLi . Sprawdź to: Warstwa abstrakcji bazy danych DALMP dla MySQL przy użyciu PHP.

Ponadto można „przygotować” argumenty przed przygotowaniem zapytania, aby można było tworzyć zapytania dynamiczne, a na końcu mieć w pełni przygotowane zapytanie. Warstwa abstrakcji bazy danych DALMP dla MySQL przy użyciu PHP.


224

Dla tych, którzy nie są pewni, jak używać PDO (pochodzącego z mysql_funkcji), stworzyłem bardzo, bardzo proste opakowanie PDO, które jest pojedynczym plikiem. Istnieje, aby pokazać, jak łatwo jest wykonać wszystkie typowe czynności, które muszą być wykonane przez aplikacje. Współpracuje z PostgreSQL, MySQL i SQLite.

Zasadniczo, przeczytaj ją podczas czytania instrukcji , aby zobaczyć, jak umieścić funkcje PDO do wykorzystania w prawdziwym życiu, aby to proste do przechowywania i pobierania wartości w formacie ty chcesz.

Chcę pojedynczą kolumnę

$count = DB::column('SELECT COUNT(*) FROM `user`);

Chcę wyniki tablicy (klucz => wartość) (tj. Do utworzenia pola wyboru)

$pairs = DB::pairs('SELECT `id`, `username` FROM `user`);

Chcę wynik w jednym wierszu

$user = DB::row('SELECT * FROM `user` WHERE `id` = ?', array($user_id));

Chcę tablicę wyników

$banned_users = DB::fetch('SELECT * FROM `user` WHERE `banned` = ?', array(TRUE));

222

Za pomocą tej funkcji PHP mysql_escape_string()można szybko uzyskać dobre zapobieganie.

Na przykład:

SELECT * FROM users WHERE name = '".mysql_escape_string($name_from_html_form)."'

mysql_escape_string - Ucieka ciąg znaków do użycia w mysql_query

Aby więcej zapobiegać, możesz dodać na końcu ...

wHERE 1=1   or  LIMIT 1

Wreszcie otrzymujesz:

SELECT * FROM users WHERE name = '".mysql_escape_string($name_from_html_form)."' LIMIT 1

220

Kilka wskazówek dotyczących zmiany znaczenia znaków specjalnych w instrukcjach SQL.

Nie używaj MySQL . To rozszerzenie jest przestarzałe. Zamiast tego użyj MySQLi lub PDO .

MySQLi

Do ręcznego zmiany znaków specjalnych w ciągu można użyć funkcji mysqli_real_escape_string . Ta funkcja nie będzie działać poprawnie, chyba że w zestawie mysqli_set_charset zostanie ustawiony prawidłowy zestaw znaków .

Przykład:

$mysqli = new mysqli('host', 'user', 'password', 'database');
$mysqli->set_charset('charset');

$string = $mysqli->real_escape_string($string);
$mysqli->query("INSERT INTO table (column) VALUES ('$string')");

Do automatycznego zmiany wartości przygotowanych instrukcji użyj mysqli_prepare i mysqli_stmt_bind_param, gdzie należy podać typy odpowiednich zmiennych powiązania dla odpowiedniej konwersji:

Przykład:

$stmt = $mysqli->prepare("INSERT INTO table (column1, column2) VALUES (?,?)");

$stmt->bind_param("is", $integer, $string);

$stmt->execute();

Bez względu na to, czy używasz przygotowanych instrukcji mysqli_real_escape_string, czy zawsze musisz znać typ danych wejściowych, z którymi pracujesz.

Więc jeśli używasz przygotowanej instrukcji, musisz określić typy zmiennych dla mysqli_stmt_bind_paramfunkcji.

I używa mysqli_real_escape_stringsię go, jak sama nazwa wskazuje, do unikania znaków specjalnych w ciągu, więc nie zapewni to bezpieczeństwa liczb całkowitych. Celem tej funkcji jest zapobieganie zerwaniu ciągów w instrukcjach SQL i uszkodzeniu bazy danych, które mogłoby to spowodować. mysqli_real_escape_stringjest użyteczną funkcją, jeśli jest właściwie używana, szczególnie w połączeniu z sprintf.

Przykład:

$string = "x' OR name LIKE '%John%";
$integer = '5 OR id != 0';

$query = sprintf( "SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string($string), $integer);

echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 5

$integer = '99999999999999999999';
$query = sprintf("SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string($string), $integer);

echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 2147483647

3
Pytanie jest bardzo ogólne. Kilka świetnych odpowiedzi powyżej, ale większość sugeruje przygotowane wypowiedzi. Asynchronizacja MySQLi nie obsługuje przygotowanych instrukcji, więc sprintf wygląda na doskonałą opcję w tej sytuacji.
Dustin Graham

183

Prostą alternatywę dla tego problemu można rozwiązać, przyznając odpowiednie uprawnienia w samej bazie danych. Na przykład: jeśli korzystasz z bazy danych MySQL, wejdź do bazy danych za pośrednictwem terminalu lub interfejsu użytkownika i wykonaj następujące polecenie:

 GRANT SELECT, INSERT, DELETE ON database TO username@'localhost' IDENTIFIED BY 'password';

Ograniczy to użytkownika do ograniczenia tylko do określonego zapytania. Usuń uprawnienie do usuwania, aby dane nigdy nie zostały usunięte z zapytania uruchomionego ze strony PHP. Drugą rzeczą do zrobienia jest opróżnienie uprawnień, aby MySQL odświeżył uprawnienia i aktualizacje.

FLUSH PRIVILEGES; 

więcej informacji na temat koloru .

Aby zobaczyć bieżące uprawnienia użytkownika, uruchom następujące zapytanie.

select * from mysql.user where User='username';

Dowiedz się więcej o GRANT .


25
Ta odpowiedź jest zasadniczo błędna , ponieważ nie pomaga w zapobieganiu iniekcji, a jedynie próbuje złagodzić konsekwencje. Na próżno.
Twój zdrowy rozsądek

1
Racja, nie zapewnia rozwiązania, ale jest to, co możesz zrobić wcześniej, aby uniknąć rzeczy.
Apurv Nerlekar,

1
@Apurv Jeśli moim celem jest odczytanie prywatnych informacji z bazy danych, brak uprawnienia DELETE nic nie znaczy.
Alex Holsgrove,

1
@AlexHolsgrove: Spokojnie, właśnie sugerowałem dobre praktyki łagodzenia konsekwencji.
Apurv Nerlekar

2
@Apurv Nie chcesz „łagodzić konsekwencji”, chcesz zrobić wszystko, co możliwe, aby się przed tym chronić. Szczerze mówiąc, ustawienie prawidłowego dostępu użytkownika jest ważne, ale nie tak naprawdę, o co prosi OP.
Alex Holsgrove

177

Jeśli chodzi o wiele przydatnych odpowiedzi, mam nadzieję, że dodam trochę wartości temu wątkowi.

Wstrzyknięcie SQL to atak, który można wykonać za pomocą danych wejściowych użytkownika (danych wejściowych, które użytkownik wypełnił, a następnie wykorzystał w zapytaniach). Wzorce wstrzykiwania SQL są poprawną składnią zapytań, którą możemy nazwać: złe zapytania z niewłaściwych powodów i zakładamy, że może być zła osoba, która próbuje uzyskać tajne informacje (omijając kontrolę dostępu), które wpływają na trzy zasady bezpieczeństwa (poufność) , integralność i dostępność).

Teraz naszym celem jest zapobieganie zagrożeniom bezpieczeństwa, takim jak ataki polegające na wstrzykiwaniu SQL, zadawanie pytań (jak zapobiegać atakom polegającym na wstrzykiwaniu SQL za pomocą PHP), być bardziej realistycznym, filtrowanie lub usuwanie danych wejściowych ma miejsce w przypadku korzystania z danych wprowadzanych przez użytkownika wewnątrz takie zapytanie, użycie PHP lub innego języka programowania nie jest prawdą lub, jak zaleca więcej osób, aby korzystać z nowoczesnych technologii, takich jak przygotowane oświadczenie lub inne narzędzia, które obecnie obsługują zapobieganie wstrzykiwaniu SQL, uważają, że te narzędzia nie są już dostępne? Jak zabezpieczyć aplikację?

Moje podejście przeciwko iniekcji SQL to: czyszczenie danych wejściowych użytkownika przed wysłaniem ich do bazy danych (przed użyciem ich w dowolnym zapytaniu).

Filtrowanie danych (konwersja niebezpiecznych danych na bezpieczne dane)

Weź pod uwagę, że PDO i MySQLi nie są dostępne. Jak możesz zabezpieczyć swoją aplikację? Czy zmuszasz mnie do korzystania z nich? Co z innymi językami innymi niż PHP? Wolę przedstawiać ogólne pomysły, ponieważ można je wykorzystać dla szerszych granic, a nie tylko dla konkretnego języka.

  1. Użytkownik SQL (ograniczenie uprawnień użytkownika): najczęstsze operacje SQL to (WYBIERZ, AKTUALIZACJA, WSTAW), po co więc nadawać uprawnienia UPDATE użytkownikowi, który go nie wymaga? Na przykład strony logowania i wyszukiwania używają tylko SELECT, więc po co używać użytkowników DB na tych stronach z wysokimi uprawnieniami?

ZASADA: nie twórz jednego użytkownika bazy danych dla wszystkich uprawnień. Dla wszystkich operacji SQL możesz utworzyć swój schemat, taki jak (deluser, selectuser, updateuser) jako nazwy użytkownika dla łatwego użycia.

Zobacz zasadę najmniejszych uprawnień .

  1. Filtrowanie danych: przed zbudowaniem dowolnego zapytania użytkownika należy je zweryfikować i przefiltrować. Dla programistów ważne jest zdefiniowanie niektórych właściwości dla każdej zmiennej wprowadzanej przez użytkownika: typ danych, wzorzec danych i długość danych . Pole, które jest liczbą pomiędzy (xiy), musi być dokładnie sprawdzone przy użyciu ścisłej reguły, a dla pola, które jest łańcuchem (tekstem): przypadek ma wzór, na przykład nazwa użytkownika musi zawierać tylko niektóre znaki, powiedz [a-zA-Z0-9_-.]. Długość waha się między (x i n), gdzie x i n (liczby całkowite, x <= n). Reguła: tworzenie dokładnych filtrów i reguł sprawdzania poprawności to dla mnie najlepsze praktyki.

  2. Użyj innych narzędzi: tutaj również zgodzę się z tobą, że przygotowane oświadczenie (sparametryzowane zapytanie) i procedury składowane. Wadą jest to, że te sposoby wymagają zaawansowanych umiejętności, które nie istnieją dla większości użytkowników. Podstawową ideą jest tutaj rozróżnienie między zapytaniem SQL a danymi używanymi w środku. Oba podejścia można zastosować nawet w przypadku niebezpiecznych danych, ponieważ dane wprowadzone przez użytkownika tutaj nie dodają niczego do pierwotnego zapytania, takiego jak (dowolny lub x = x).

Aby uzyskać więcej informacji, przeczytaj OWASP SQL Injection Prevention Cheat Sheet .

Teraz, jeśli jesteś zaawansowanym użytkownikiem, zacznij używać tej obrony, jak chcesz, ale dla początkujących, jeśli nie mogą szybko wdrożyć procedury składowanej i przygotowali instrukcję, lepiej filtrować dane wejściowe, na ile mogą.

Wreszcie, rozważmy, że użytkownik wysyła ten tekst poniżej, zamiast wpisywać swoją nazwę użytkownika:

[1] UNION SELECT IF(SUBSTRING(Password,1,1)='2',BENCHMARK(100000,SHA1(1)),0) User,Password FROM mysql.user WHERE User = 'root'

Dane wejściowe można sprawdzić wcześniej bez przygotowanej instrukcji i procedur przechowywanych, ale dla pewności korzystanie z nich rozpoczyna się po filtrowaniu i weryfikacji danych użytkownika.

Ostatnim punktem jest wykrycie nieoczekiwanego zachowania, które wymaga większego wysiłku i złożoności; nie jest to zalecane w przypadku zwykłych aplikacji internetowych.

Nieoczekiwane zachowanie w powyższych danych wejściowych użytkownika to SELECT, UNION, IF, SUBSTRING, BENCHMARK, SHA i root. Po wykryciu tych słów możesz uniknąć wprowadzania danych.

AKTUALIZACJA 1:

Użytkownik skomentował, że ten post jest bezużyteczny, OK! Oto, co zapewnił OWASP.ORG :

Podstawowa obrona:

Opcja nr 1: Wykorzystanie przygotowanych instrukcji (sparametryzowane zapytania)
Opcja nr 2: Zastosowanie procedur przechowywanych
Opcja nr 3: Ucieczka od wszystkich danych dostarczonych przez użytkownika

Dodatkowe zabezpieczenia:

Wymuszenie: Najmniejszy przywilej
Wykonaj również: Sprawdzanie poprawności danych na białej liście

Jak zapewne wiesz, roszczenie do artykułu powinno być poparte ważnym argumentem, przynajmniej jednym odniesieniem! W przeciwnym razie jest to uważane za atak i złe roszczenie!

Aktualizacja 2:

Z podręcznika PHP, PHP: Przygotowane instrukcje - Podręcznik :

Ucieczka i wstrzyknięcie SQL

Związane zmienne będą automatycznie usuwane przez serwer. Serwer wstawia wartości specjalne w odpowiednich miejscach do szablonu instrukcji przed wykonaniem. Na serwerze należy podać wskazówkę dotyczącą typu zmiennej powiązanej, aby utworzyć odpowiednią konwersję. Aby uzyskać więcej informacji, zobacz funkcję mysqli_stmt_bind_param ().

Automatyczne zmiany wartości na serwerze są czasem uważane za funkcję bezpieczeństwa zapobiegającą wstrzykiwaniu SQL. Taki sam poziom bezpieczeństwa można uzyskać przy użyciu nieprzygotowanych instrukcji, jeśli wartości wejściowe są odpowiednio zabezpieczone znakami ucieczki.

Aktualizacja 3:

Utworzyłem przypadki testowe, aby dowiedzieć się, w jaki sposób PDO i MySQLi wysyłają zapytanie do serwera MySQL podczas korzystania z przygotowanej instrukcji:

ChNP:

$user = "''1''"; // Malicious keyword
$sql = 'SELECT * FROM awa_user WHERE userame =:username';
$sth = $dbh->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
$sth->execute(array(':username' => $user));

Dziennik zapytań:

    189 Query SELECT * FROM awa_user WHERE userame ='\'\'1\'\''
    189 Quit

MySQLi:

$stmt = $mysqli->prepare("SELECT * FROM awa_user WHERE username =?")) {
$stmt->bind_param("s", $user);
$user = "''1''";
$stmt->execute();

Dziennik zapytań:

    188 Prepare   SELECT * FROM awa_user WHERE username =?
    188 Execute   SELECT * FROM awa_user WHERE username ='\'\'1\'\''
    188 Quit

Oczywiste jest, że przygotowane oświadczenie ucieka również od danych, nic więcej.

Jak wspomniano również w powyższym oświadczeniu,

Automatyczne zmiany wartości na serwerze są czasem uważane za funkcję bezpieczeństwa zapobiegającą wstrzykiwaniu SQL. Taki sam poziom bezpieczeństwa można osiągnąć przy użyciu nieprzygotowanych instrukcji, jeśli wartości wejściowe są odpowiednio zabezpieczone znakami ucieczki

Dlatego dowodzi to, że sprawdzanie poprawności danych, takie jak, intval()jest dobrym pomysłem na wartości całkowite przed wysłaniem dowolnego zapytania. Ponadto zapobieganie złośliwym użytkownikom przed wysłaniem zapytania jest poprawnym i prawidłowym podejściem .

Proszę zobaczyć to pytanie, aby uzyskać więcej szczegółów: PDO wysyła surowe zapytanie do MySQL, podczas gdy Mysqli wysyła przygotowane zapytanie, oba dają ten sam wynik

Bibliografia:

  1. Ściągawka SQL Injection
  2. Zastrzyk SQL
  3. Bezpieczeństwo informacji
  4. Zasady bezpieczeństwa
  5. Walidacji danych

175

Ostrzeżenie bezpieczeństwa : ta odpowiedź jest niezgodna z najlepszymi praktykami bezpieczeństwa. Ucieczka jest nieodpowiednia, aby zapobiec wstrzyknięciu SQL , zamiast tego użyj przygotowanych instrukcji . Skorzystaj ze strategii przedstawionej poniżej na własne ryzyko. (Również mysql_real_escape_string()został usunięty w PHP 7.)

Przestarzałe Ostrzeżenie : Obecnie rozszerzenie mysql jest nieaktualne. zalecamy użycie rozszerzenia PDO

Używam trzech różnych sposobów, aby zapobiec podatności mojej aplikacji internetowej na wstrzykiwanie SQL.

  1. Zastosowanie mysql_real_escape_string(), które jest z góry określone funkcje w PHP , a to code Dodaj backslashe do następujących znaków: \x00, \n, \r, \, ', "i \x1a. Przekaż wartości wejściowe jako parametry, aby zminimalizować ryzyko wstrzyknięcia SQL.
  2. Najbardziej zaawansowanym sposobem jest użycie PDO.

Mam nadzieję, że to Ci pomoże.

Rozważ następujące zapytanie:

$iId = mysql_real_escape_string("1 OR 1=1"); $sSql = "SELECT * FROM table WHERE id = $iId";

mysql_real_escape_string () nie chroni tutaj. Jeśli użyjesz pojedynczych cudzysłowów ('') wokół zmiennych w zapytaniu, to cię przed tym chroni. Oto rozwiązanie tego problemu:

$iId = (int) mysql_real_escape_string("1 OR 1=1"); $sSql = "SELECT * FROM table WHERE id = $iId";

To pytanie ma na to dobre odpowiedzi.

Sugeruję, że używanie PDO jest najlepszą opcją.

Edytować:

mysql_real_escape_string()jest przestarzałe od PHP 5.5.0. Użyj mysqli lub PDO.

Alternatywą dla mysql_real_escape_string () jest

string mysqli_real_escape_string ( mysqli $link , string $escapestr )

Przykład:

$iId = $mysqli->real_escape_string("1 OR 1=1");
$mysqli->query("SELECT * FROM table WHERE id = $iId");

169

Prostym sposobem byłoby użycie frameworka PHP, takiego jak CodeIgniter lub Laravel, który ma wbudowane funkcje, takie jak filtrowanie i aktywny zapis, dzięki czemu nie musisz się martwić o te niuanse.


7
Myślę, że sedno tego pytania polega na tym, aby zrobić to bez użycia takich ram.
Sanke

147

Ostrzeżenie: podejście opisane w tej odpowiedzi dotyczy tylko bardzo specyficznych scenariuszy i nie jest bezpieczne, ponieważ ataki polegające na wstrzykiwaniu SQL polegają nie tylko na możliwości wstrzykiwania X=Y.

Jeśli atakujący próbują włamać się do formularza za pomocą $_GETzmiennej PHP lub ciągu zapytania adresu URL, będziesz w stanie je złapać, jeśli nie będą bezpieczne.

RewriteCond %{QUERY_STRING} ([0-9]+)=([0-9]+)
RewriteRule ^(.*) ^/track.php

Ponieważ 1=1, 2=2, 1=2, 2=1, 1+1=2, itp ... są wspólne pytania do bazy danych SQL atakującego. Być może jest także używany przez wiele aplikacji hakerskich.

Musisz jednak uważać, aby nie przepisać bezpiecznego zapytania ze swojej witryny. Powyższy kod daje ci wskazówkę, aby przepisać lub przekierować (zależnie od ciebie) ten dynamiczny ciąg zapytania specyficzny dla hakowania na stronie, która będzie przechowywać adres IP osoby atakującej lub NAWET ICH PLIKI COOKIE, historię, przeglądarkę lub dowolną inną wrażliwą informacje, abyś mógł zająć się nimi później, blokując ich konto lub kontaktując się z władzami.


Co się dzieje z 1-1=0? :)
Rápli András

@ RápliAndrás Jakiś rodzaj ([0-9\-]+)=([0-9]+).
5

127

Istnieje wiele odpowiedzi na PHP i MySQL , ale oto kod dla PHP i Oracle, aby zapobiec wstrzykiwaniu SQL, a także regularnemu używaniu sterowników oci8:

$conn = oci_connect($username, $password, $connection_string);
$stmt = oci_parse($conn, 'UPDATE table SET field = :xx WHERE ID = 123');
oci_bind_by_name($stmt, ':xx', $fieldval);
oci_execute($stmt);

Proszę wyjaśnić parametry oci_bind_by_name.
Jahanzeb Awan

127

Dobrym pomysłem jest użycie odwzorowania obiektowo-relacyjnego, takiego jak Idiorm :

$user = ORM::for_table('user')
->where_equal('username', 'j4mie')
->find_one();

$user->first_name = 'Jamie';
$user->save();

$tweets = ORM::for_table('tweet')
    ->select('tweet.*')
    ->join('user', array(
        'user.id', '=', 'tweet.user_id'
    ))
    ->where_equal('user.username', 'j4mie')
    ->find_many();

foreach ($tweets as $tweet) {
    echo $tweet->text;
}

Oszczędza to nie tylko zastrzyków SQL, ale również błędów składniowych! Obsługuje również kolekcje modeli z łączeniem metod w celu filtrowania lub stosowania działań do wielu wyników jednocześnie i wielu połączeń.


124

Przestarzałe ostrzeżenie: Przykładowy kod tej odpowiedzi (podobnie jak przykładowy kod pytania) korzysta z MySQLrozszerzenia PHP , które zostało przestarzałe w PHP 5.5.0 i całkowicie usunięte w PHP 7.0.0.

Ostrzeżenie bezpieczeństwa : ta odpowiedź jest niezgodna z najlepszymi praktykami bezpieczeństwa. Ucieczka jest nieodpowiednia, aby zapobiec wstrzyknięciu SQL , zamiast tego użyj przygotowanych instrukcji . Skorzystaj ze strategii przedstawionej poniżej na własne ryzyko. (Również mysql_real_escape_string()został usunięty w PHP 7.)

Używanie PDO i MYSQLi jest dobrą praktyką, aby zapobiec wstrzykiwaniu SQL, ale jeśli naprawdę chcesz pracować z funkcjami i zapytaniami MySQL, lepiej byłoby użyć

mysql_real_escape_string

$unsafe_variable = mysql_real_escape_string($_POST['user_input']);

Jest więcej możliwości, aby temu zapobiec: na przykład identyfikacja - jeśli dane wejściowe to ciąg, liczba, znak lub tablica, istnieje tak wiele wbudowanych funkcji do wykrywania tego. Lepiej byłoby również użyć tych funkcji do sprawdzenia danych wejściowych.

is_string

$unsafe_variable = (is_string($_POST['user_input']) ? $_POST['user_input'] : '');

is_numeric

$unsafe_variable = (is_numeric($_POST['user_input']) ? $_POST['user_input'] : '');

I o wiele lepiej jest używać tych funkcji do sprawdzania danych wejściowych mysql_real_escape_string.


10
Poza tym absolutnie nie ma sensu sprawdzanie elementów tablicy $ _POST za pomocą is_string ()
Your Common Sense

21
OSTRZEŻENIE! mysql_real_escape_string() nie jest nieomylny .
eggyal

10
mysql_real_escape_stringjest teraz przestarzałe, więc nie jest już realną opcją. W przyszłości zostanie usunięty z PHP. Najlepiej przejść do tego, co polecają ludzie PHP lub MySQL.
jww

2
Temat: Nie ufaj przesłanym przez użytkownika danym. Czego oczekujesz, to śmieciowe dane ze znakami specjalnymi lub logika logiczna, które same powinny stać się częścią zapytania SQL, które możesz wykonywać. Zachowaj wartości $ _POST tylko jako dane, a nie część SQL.
Bimal Poudel,

88

Kilka lat temu napisałem tę małą funkcję:

function sqlvprintf($query, $args)
{
    global $DB_LINK;
    $ctr = 0;
    ensureConnection(); // Connect to database if not connected already.
    $values = array();
    foreach ($args as $value)
    {
        if (is_string($value))
        {
            $value = "'" . mysqli_real_escape_string($DB_LINK, $value) . "'";
        }
        else if (is_null($value))
        {
            $value = 'NULL';
        }
        else if (!is_int($value) && !is_float($value))
        {
            die('Only numeric, string, array and NULL arguments allowed in a query. Argument '.($ctr+1).' is not a basic type, it\'s type is '. gettype($value). '.');
        }
        $values[] = $value;
        $ctr++;
    }
    $query = preg_replace_callback(
        '/{(\\d+)}/', 
        function($match) use ($values)
        {
            if (isset($values[$match[1]]))
            {
                return $values[$match[1]];
            }
            else
            {
                return $match[0];
            }
        },
        $query
    );
    return $query;
}

function runEscapedQuery($preparedQuery /*, ...*/)
{
    $params = array_slice(func_get_args(), 1);
    $results = runQuery(sqlvprintf($preparedQuery, $params)); // Run query and fetch results.   
    return $results;
}

Pozwala to na uruchamianie instrukcji w jednowierszowym łańcuchu C # -ish. Formatowanie takie jak:

runEscapedQuery("INSERT INTO Whatever (id, foo, bar) VALUES ({0}, {1}, {2})", $numericVar, $stringVar1, $stringVar2);

Ucieka, biorąc pod uwagę typ zmiennej. Jeśli spróbujesz sparametryzować nazwy tabel, kolumn, nie powiedzie się, ponieważ umieszcza każdy ciąg w cudzysłowach, co jest niepoprawną składnią.

AKTUALIZACJA BEZPIECZEŃSTWA: Poprzednia str_replacewersja pozwalała na zastrzyki, dodając {#} tokeny do danych użytkownika. Ta preg_replace_callbackwersja nie powoduje problemów, jeśli zamiennik zawiera te tokeny.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.