Il corretto un modo per evitare attacchi SQL injection, indipendentemente dal database utilizzato, è separare i dati da SQL , in modo che i dati rimangano tali e non verranno mai interpretati come comandi dal parser SQL. È possibile creare istruzioni SQL con parti di dati formattate correttamente, ma se non lo fai completamente comprendere i dettagli, dovresti sempre utilizzare istruzioni preparate e query parametrizzate. Si tratta di istruzioni SQL inviate e analizzate dal server di database separatamente da qualsiasi parametro. In questo modo è impossibile per un utente malintenzionato iniettare SQL dannoso.
Fondamentalmente hai due opzioni per raggiungere questo obiettivo:
-
Utilizzo di DOP (per qualsiasi driver di database supportato):
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name'); $stmt->execute([ 'name' => $name ]); foreach ($stmt as $row) { // Do something with $row }
-
Utilizzo di 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, c'è una seconda opzione specifica del driver a cui puoi fare riferimento (ad esempio, pg_prepare()
e pg_execute()
per PostgreSQL). DOP è l'opzione universale.
Impostazione corretta della connessione
Tieni presente che quando utilizzi DOP per accedere a un database MySQL reale le istruzioni preparate sono non utilizzate per impostazione predefinita . Per risolvere questo problema devi disabilitare l'emulazione delle istruzioni preparate. Un esempio di creazione di una connessione utilizzando PDO è:
$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);
Nell'esempio sopra la modalità errore non è strettamente necessaria, ma si consiglia di aggiungerla . In questo modo lo script non si fermerà con un Fatal Error
quando qualcosa va storto. E offre allo sviluppatore la possibilità di catch
qualsiasi errore che sia throw
n come PDOException
s.
Che cos'è obbligatorio , tuttavia, è il primo setAttribute()
line, che dice a PDO di disabilitare le istruzioni preparate emulate e di utilizzare real dichiarazioni preparate. Ciò assicura che l'istruzione e i valori non vengano analizzati da PHP prima di inviarli al server MySQL (non dando a un possibile aggressore la possibilità di iniettare SQL dannoso).
Sebbene tu possa impostare il charset
nelle opzioni del costruttore, è importante notare che le versioni "precedenti" di PHP (prima della 5.3.6) ignora silenziosamente il parametro charset
nel DSN.
Spiegazione
L'istruzione SQL che passi a prepare
viene analizzato e compilato dal server di database. Specificando i parametri (o un ?
o un parametro denominato come :name
nell'esempio sopra) dici al motore di database dove vuoi filtrare. Quindi quando chiami execute
, l'istruzione preparata viene combinata con i valori dei parametri specificati.
La cosa importante qui è che i valori dei parametri siano combinati con l'istruzione compilata, non con una stringa SQL. SQL injection funziona inducendo lo script a includere stringhe dannose quando crea SQL da inviare al database. Quindi, inviando l'SQL effettivo separatamente dai parametri, limiti il rischio di finire con qualcosa che non volevi.
Tutti i parametri inviati quando si utilizza un'istruzione preparata verranno semplicemente trattati come stringhe (sebbene il motore di database possa eseguire alcune ottimizzazioni, quindi anche i parametri potrebbero finire come numeri, ovviamente). Nell'esempio sopra, se il $name
la variabile contiene 'Sarah'; DELETE FROM employees
il risultato sarebbe semplicemente una ricerca per la stringa "'Sarah'; DELETE FROM employees"
e non ti ritroverai con una tabella vuota
.
Un altro vantaggio dell'utilizzo di istruzioni preparate è che se esegui la stessa istruzione più volte nella stessa sessione, verrà analizzata e compilata solo una volta, offrendoti alcuni guadagni di velocità.
Oh, e poiché hai chiesto come farlo per un inserto, ecco un esempio (usando PDO):
$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');
$preparedStatement->execute([ 'column' => $unsafeValue ]);
Le istruzioni preparate possono essere utilizzate per query dinamiche?
Sebbene sia ancora possibile utilizzare istruzioni preparate per i parametri della query, la struttura della query dinamica stessa non può essere parametrizzata e alcune funzioni della query non possono essere parametrizzate.
Per questi scenari specifici, la cosa migliore da fare è utilizzare un filtro whitelist che limiti i possibili valori.
// Value whitelist
// $dir can only be 'DESC', otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
$dir = 'ASC';
}