Domanda Come posso prevenire l'SQL injection in PHP?


Se l'input dell'utente viene inserito senza modifiche in una query SQL, l'applicazione diventa vulnerabile SQL Injection, come nell'esempio seguente:

$unsafe_variable = $_POST['user_input']; 

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

Questo perché l'utente può inserire qualcosa di simile value'); DROP TABLE table;--e la query diventa:

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

Cosa si può fare per evitare che ciò accada?


2781


origine


risposte:


Utilizzare istruzioni preparate e query parametrizzate. Queste sono istruzioni SQL che vengono inviate e analizzate dal server database separatamente da qualsiasi parametro. In questo modo è impossibile per un utente malintenzionato iniettare SQL dannoso.

Fondamentalmente hai due opzioni per ottenere questo:

  1. utilizzando DOP (per qualsiasi driver di database supportato):

    $stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
    
    $stmt->execute(array('name' => $name));
    
    foreach ($stmt as $row) {
        // do something with $row
    }
    
  2. utilizzando MySQLi (per 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
    }
    

Se ti stai connettendo a un database diverso da MySQL, esiste una seconda opzione specifica del driver a cui puoi fare riferimento (ad es. pg_prepare() e pg_execute() per PostgreSQL). DOP è l'opzione universale.

Configurare correttamente la connessione

Si noti che durante l'utilizzo PDO per accedere a un database MySQL vero dichiarazioni preparate sono non usato di default. Per risolvere questo problema è necessario disabilitare l'emulazione delle istruzioni preparate. Un esempio di creazione di una connessione tramite PDO è:

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

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

Nell'esempio precedente la modalità errore non è strettamente necessaria, ma si consiglia di aggiungerlo. In questo modo lo script non si fermerà con a Fatal Error quando qualcosa va storto E dà allo sviluppatore la possibilità di farlo catch qualsiasi errore (s) che sono thrown come PDOExceptionS.

Cosa è obbligatorio, tuttavia, è il primo setAttribute() linea, che dice a PDO di disabilitare le istruzioni preparate emulate e l'uso vero dichiarazioni preparate. Ciò assicura che l'istruzione e i valori non vengano analizzati da PHP prima di inviarli al server MySQL (dando a un possibile utente malintenzionato alcuna possibilità di iniettare SQL malizioso).

Sebbene tu possa impostare il charset nelle opzioni del costruttore, è importante notare che le versioni "precedenti" di PHP (<5.3.6) ha ignorato silenziosamente il parametro charset nel DSN.

Spiegazione

Quello che succede è che l'istruzione SQL si passa a prepare viene analizzato e compilato dal server del database. Specificando i parametri (a ? o un parametro chiamato come :name nell'esempio sopra) comunichi al motore del database dove vuoi filtrare. Quindi quando chiami execute, l'istruzione preparata è combinata con i valori dei parametri specificati.

La cosa importante qui è che i valori dei parametri sono combinati con l'istruzione compilata, non con una stringa SQL. L'iniezione SQL funziona ingannando lo script includendo stringhe dannose quando crea SQL da inviare al database. Quindi, inviando l'SQL effettivo separatamente dai parametri, si limita il rischio di finire con qualcosa che non si intendeva. Tutti i parametri che invii quando usi una dichiarazione preparata saranno trattati come stringhe (sebbene il motore di database possa fare un po 'di ottimizzazione in modo che i parametri possano finire anche come numeri, ovviamente). Nell'esempio sopra, se il $namevariabile contiene 'Sarah'; DELETE FROM employees il risultato sarebbe semplicemente una ricerca per la stringa "'Sarah'; DELETE FROM employees"e non finirai con un tavolo vuoto.

Un altro vantaggio dell'utilizzo di istruzioni preparate è che se si esegue la stessa istruzione più volte nella stessa sessione, verrà analizzata e compilata solo una volta, ottenendo alcuni guadagni di velocità.

Oh, e dal momento che hai chiesto come fare per un inserto, ecco un esempio (usando PDO):

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

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

È possibile utilizzare istruzioni preparate per query dinamiche?

Mentre è ancora possibile utilizzare istruzioni preparate per i parametri di query, la struttura della query dinamica non può essere parametrizzata e alcune funzionalità di query non possono essere parametrizzate.

Per questi scenari specifici, la cosa migliore da fare è utilizzare un filtro di lista bianca che limiti i valori possibili.

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

7938



Avvertimento:   Il codice di esempio di questa risposta (come il codice di esempio della domanda) utilizza PHP mysql estensione, che è stata deprecata in PHP 5.5.0 e completamente rimossa in PHP 7.0.0.

Se stai usando una versione recente di PHP, il mysql_real_escape_string l'opzione delineata di seguito non sarà più disponibile (però mysqli::escape_string è un equivalente moderno). In questi giorni il mysql_real_escape_string l'opzione avrebbe senso solo per il codice legacy su una vecchia versione di PHP.


Hai due opzioni: sfuggire i caratteri speciali nel tuo unsafe_variableo utilizzando una query parametrizzata. Entrambi ti proteggeranno dall'iniezione SQL. La query parametrizzata è considerata la migliore pratica, ma prima di poter essere utilizzata è necessario passare a una nuova estensione mysql in PHP.

Tratteremo prima la stringa di impatto inferiore che sfugge a una.

//Connect

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

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

//Disconnect

Vedi anche, i dettagli del mysql_real_escape_string funzione.

Per utilizzare la query con parametri, è necessario utilizzare MySQLi piuttosto che il MySQL funzioni. Per riscrivere il tuo esempio, avremmo bisogno di qualcosa come il seguente.

<?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();
?>

La funzione chiave che vorrai leggere sopra ci sarebbe mysqli::prepare.

Inoltre, come altri hanno suggerito, potresti trovare utile / più semplice aumentare uno strato di astrazione con qualcosa di simile DOP.

Tieni presente che il caso che hai chiesto è abbastanza semplice e che casi più complessi potrebbero richiedere approcci più complessi. In particolare:

  • Se si desidera modificare la struttura di SQL in base all'input dell'utente, le query con parametri non saranno di aiuto e l'escape richiesto non è coperto da mysql_real_escape_string. In questo tipo di casi, sarebbe meglio passare l'input dell'utente attraverso una whitelist per garantire che solo i valori "sicuri" siano consentiti.
  • Se si utilizzano numeri interi da input dell'utente in una condizione e si prende il mysql_real_escape_string approccio, soffrirai del problema descritto da Polinomio nei commenti qui sotto. Questo caso è più complicato perché gli interi non sono racchiusi tra virgolette, quindi è possibile gestirli convalidando che l'input dell'utente contenga solo cifre.
  • Ci sono probabilmente altri casi di cui non sono a conoscenza. Potresti trovarlo Questo è una risorsa utile su alcuni dei problemi più sottili che puoi incontrare.

1509



Ogni risposta qui copre solo una parte del problema.
In effetti, ci sono quattro diverse parti di query che possiamo aggiungere dinamicamente:

  • una stringa
  • un numero
  • un identificatore
  • una parola chiave di sintassi.

e le dichiarazioni preparate coprono solo 2 di loro

Ma a volte dobbiamo rendere la nostra query ancora più dinamica, aggiungendo anche operatori o identificatori.
Quindi, avremo bisogno di diverse tecniche di protezione.

In generale, tale approccio di protezione è basato su whitelisting. In questo caso, ogni parametro dinamico dovrebbe essere hardcoded nel tuo script e scelto da quel set.
Ad esempio, per fare l'ordinamento dinamico:

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

Tuttavia, esiste un altro modo per proteggere gli identificatori: l'escape. Finché hai un identificatore quotato, puoi sfuggire gli apici inversamente raddoppiandoli.

Come ulteriore passo, possiamo prendere in prestito un'idea davvero geniale dell'uso di un segnaposto (un proxy per rappresentare il valore effettivo nella query) dalle dichiarazioni preparate e inventare un segnaposto di un altro tipo - un segnaposto identificatore.

Quindi, per farla breve: è a segnapostono discorso preparato può essere considerato un proiettile d'argento.

Quindi, una raccomandazione generale può essere formulata come
Finché si aggiungono parti dinamiche alla query utilizzando segnaposti (e questi segnaposto elaborati correttamente, ovviamente), si può essere sicuri che la query sia sicura.

Tuttavia, c'è un problema con le parole chiave di sintassi SQL (come ad esempio AND, DESC e così) ma l'elenco in bianco sembra l'unico approccio in questo caso.

Aggiornare

Anche se esiste un accordo generale sulle migliori pratiche relative alla protezione dell'iniezione SQL, ci sono ancora molte cattive pratiche. E alcuni di loro sono troppo radicati nelle menti degli utenti di PHP. Ad esempio, in questa stessa pagina ci sono (anche se invisibili alla maggior parte dei visitatori) più di 80 risposte cancellate - tutti rimossi dalla comunità a causa di cattiva qualità o promozione di pratiche cattive e obsolete. Peggio ancora, alcune delle cattive risposte non vengono cancellate ma piuttosto prosperose.

Per esempio, ci (1)  sono (2)  ancora (3)  molti (4)  risposte (5), Compreso la seconda risposta più votata suggerendoti di sfuggire manualmente alla stringa: un approccio obsoleto che si è dimostrato insicuro.

Oppure c'è una risposta leggermente migliore che suggerisce semplicemente un altro metodo per la formattazione delle stringhe e persino lo vanta come panacea definitiva. Mentre, naturalmente, non lo è. Questo metodo non è migliore della normale formattazione di stringhe, ma conserva tutti i suoi svantaggi: è applicabile solo alle stringhe e, come qualsiasi altra formattazione manuale, è essenzialmente facoltativa, non obbligatoria, soggetta a errori umani di qualsiasi tipo.

Penso che tutto questo a causa di una superstizione molto antica, supportata da autorità come OWASP o Manuale PHP, che proclama l'uguaglianza tra qualunque "fuga" e protezione dalle iniezioni SQL.

Indipendentemente da ciò che il manuale PHP ha detto per età, *_escape_string in nessun modo rende i dati sicuri e mai è stato progettato per. Oltre a essere inutilizzabile per qualsiasi parte SQL diversa dalla stringa, l'escape manuale è errato perché è manuale come opposto all'automazione.

E OWASP lo rende ancora peggio, sottolineando la fuga input dell'utente che è un'assoluta assurdità: non dovrebbero esserci parole del genere nel contesto della protezione dell'iniezione. Ogni variabile è potenzialmente pericolosa, indipendentemente dalla fonte! O, in altre parole, ogni variabile deve essere formattata correttamente per essere inserita in una query, indipendentemente dalla fonte. È la destinazione che conta. Nel momento in cui uno sviluppatore inizia a separare le pecore dalle capre (pensando se qualche variabile particolare è "sicura" o meno) fa il suo primo passo verso il disastro. Per non parlare del fatto che anche la formulazione suggerisce la fuga di massa al punto di ingresso, simile alla funzione di virgolette stesse - già disprezzata, deprecata e rimossa.

Quindi, a differenza di qualunque "fuga", dichiarazioni preparate è la misura che protegge effettivamente dall'iniezione SQL (quando applicabile).

Se non sei ancora convinto, ecco una spiegazione passo passo che ho scritto, La guida di Hitchhiker alla prevenzione di SQL Injection, dove ho spiegato tutti questi argomenti in dettaglio e ho anche compilato una sezione interamente dedicata alle cattive pratiche e alla loro divulgazione.


940



Consiglierei l'utilizzo DOP (PHP Data Objects) per eseguire query SQL parametrizzate.

Non solo protegge da SQL injection, ma velocizza anche le query.

E usando PDO piuttosto che mysql_, mysqli_, e pgsql_ funzioni, rendi la tua app un po 'più astratta dal database, nella rara eventualità che devi cambiare fornitore di database.


768



Uso PDO e domande preparate.

($conn è un PDO oggetto)

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

565



Come puoi vedere, le persone suggeriscono di utilizzare al massimo dichiarazioni preparate. Non è sbagliato, ma quando viene eseguita la tua query solo una volta per processo, ci sarebbe una leggera penalizzazione delle prestazioni.

Stavo affrontando questo problema, ma penso di averlo risolto molto modo sofisticato - il modo in cui gli hacker usano per evitare di usare le virgolette. L'ho usato in congiunzione con dichiarazioni preparate emulate. Lo uso per prevenire tutti tipi di possibili attacchi di SQL injection.

Il mio approccio:

  • Se ti aspetti che l'input sia intero, assicurati che lo sia veramente numero intero. In un linguaggio di tipo variabile come PHP è questo molto importante. Puoi ad esempio utilizzare questa soluzione molto semplice ma potente: sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input); 

  • Se ti aspetti altro dal numero intero hex it. Se lo esagoni, sfuggirai perfettamente a tutti gli input. In C / C ++ c'è una funzione chiamata mysql_hex_string(), in PHP puoi usare bin2hex().

    Non preoccuparti che la stringa con escape avrà una dimensione 2x della sua lunghezza originale, anche se la usi mysql_real_escape_string, PHP deve allocare la stessa capacità ((2*input_length)+1), che è lo stesso.

  • Questo metodo esadecimale viene spesso utilizzato quando si trasferiscono dati binari, ma non vedo alcun motivo per non utilizzarlo su tutti i dati per evitare attacchi di SQL injection. Nota che devi anteporre i dati con 0x o usare la funzione MySQL UNHEX anziché.

Quindi, ad esempio, la query:

SELECT password FROM users WHERE name = 'root'

Diventerà:

SELECT password FROM users WHERE name = 0x726f6f74

o

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

Hex è la fuga perfetta. Nessun modo di iniettare.

Differenza tra la funzione UNHEX e il prefisso 0x

C'è stata una discussione nei commenti, quindi alla fine voglio chiarire. Questi due approcci sono molto simili, ma in qualche modo sono leggermente diversi:

Il prefisso ** 0x ** può essere utilizzato solo per colonne di dati come char, varchar, text, block, binary, ecc.
Inoltre, il suo uso è un po 'complicato se stai per inserire una stringa vuota. Dovrai sostituirlo interamente con '', o riceverai un errore.

UNHEX () funziona qualunque colonna; non devi preoccuparti della stringa vuota.


I metodi esadecimali sono spesso usati come attacchi

Si noti che questo metodo esadecimale viene spesso utilizzato come un attacco di SQL injection in cui gli interi sono come le stringhe e sono fuggiti solo con mysql_real_escape_string. Quindi puoi evitare l'uso di virgolette.

Ad esempio, se fai solo qualcosa del genere:

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

un attacco può iniettarti molto facilmente. Considera il seguente codice iniettato restituito dallo script:

SELEZIONA ... DOVE id = -1 unione seleziona tutti table_name da information_schema.tables

e ora basta estrarre la struttura della tabella:

SELEZIONA ... WHERE id = -1 union seleziona select column_name da information_schema.column dove table_name = 0x61727469636c65

E poi seleziona solo i dati che vuoi. Non è bello?

Ma se il programmatore di un sito iniettabile lo maledisse, nessuna iniezione sarebbe possibile perché la query sarebbe simile a questa: SELECT ... WHERE id = UNHEX('2d312075...3635')


498



IMPORTANTE

Il modo migliore per impedire l'uso di SQL Injection Dichiarazioni preparate  invece di scappare, come la risposta accettata dimostra.

Ci sono biblioteche come Aura.Sql e easyDB che consentono agli sviluppatori di utilizzare le dichiarazioni preparate più facilmente. Per saperne di più sul motivo per cui le dichiarazioni preparate sono migliori interrompere l'iniezione SQL, fare riferimento a Questo mysql_real_escape_string() circonvallazione e vulnerabilità Unicode SQL Injection risolte di recente in WordPress.

Prevenzione dell'iniezione - mysql_real_escape_string ()

PHP ha una funzione appositamente creata per prevenire questi attacchi. Tutto quello che devi fare è usare il boccone di una funzione, mysql_real_escape_string.

mysql_real_escape_string prende una stringa che verrà utilizzata in una query MySQL e restituirà la stessa stringa con tutti i tentativi di SQL injection evitati in modo sicuro. Fondamentalmente, sostituirà quelle citazioni fastidiose (') che un utente potrebbe inserire con un sostituto sicuro di MySQL, una citazione di escape \'.

NOTA: devi essere connesso al database per usare questa funzione!

// Connetti a 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;

Puoi trovare maggiori dettagli in MySQL - SQL Injection Prevention.


452



Avviso di sicurezza: Questa risposta non è in linea con le best practice sulla sicurezza. L'escaping è inadeguato per prevenire l'iniezione SQL, uso dichiarazioni preparate anziché. Utilizzare la strategia descritta di seguito a proprio rischio. (Anche, mysql_real_escape_string() è stato rimosso in PHP 7.)

Potresti fare qualcosa di base come questo:

$safe_variable = mysql_real_escape_string($_POST["user-input"]);
mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

Questo non risolverà tutti i problemi, ma è un ottimo trampolino di lancio. Ho omesso elementi ovvi come controllare l'esistenza della variabile, il formato (numeri, lettere, ecc.).


410



Qualsiasi cosa tu faccia finisca di usare, assicurati di controllare che il tuo input non sia già stato storpiato da magic_quotes o qualche altra spazzatura ben intenzionata e, se necessario, eseguirla stripslashes o qualunque cosa per sanitizzarlo.


345



La query parametrizzata e la convalida dell'input sono la strada da seguire. Esistono molti scenari in cui può verificarsi l'iniezione SQL, anche se mysql_real_escape_string() è stato usato.

Questi esempi sono vulnerabili all'iniezione 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");

o

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

In entrambi i casi, non è possibile utilizzare ' per proteggere l'incapsulamento.

fonte: L'Inaspettata SQL Injection (quando la fuga non è sufficiente)


328