Come prevenire SQL injection in PHP

Il modo corretto per evitare attacchi SQL injection, indipendentemente dal database utilizzato, consiste nel separare i dati delle istruzioni SQL, in modo che i dati rimangano tali e non vengano mai interpretati come comandi dal parser SQL. Conviene quindi utilizzare sempre istruzioni preparate (prepared statements) 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:

  1. Utilizzando PDO (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
 }
  1. 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, c’è una seconda opzione specifica del driver a cui puoi fare riferimento (ad esempio, pg_prepare() e pg_execute() per PostgreSQL). PDO è l’opzione universale.

Impostare correttamente la connessione

Quando si utilizza PDO per accedere a un database MySQL, le vere istruzioni preparate NON vengono utilizzate per impostazione predefinita. Per risolvere questo problema devi 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', '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. Intolre offre la possibilità di rilevare eventuali errori generati come PDOExceptions.

Ciò che è obbligatorio, tuttavia, è la prima riga setAttribute(), che dice a PDO di disabilitare le istruzioni preparate emulate e di utilizzare istruzioni preparate reali. Ciò assicura che l’istruzione e i valori non vengano analizzati da PHP prima di inviarli al server MySQL (non dando la possibilità di iniettare SQL dannoso a un possibile aggressore).

Sebbene sia possibile impostare il charset nelle opzioni del costruttore, è importante notare che le versioni “precedenti” di PHP (prima della 5.3.6) ignoravano silenziosamente il parametro charset nel DSN.

Spiegazione

L’istruzione SQL passata per la preparazione viene analizzata e compilata dal server di database. Specificando i parametri (un ? o un parametro denominato come :name nell’esempio sopra) dici al motore di database su 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 separato dai parametri, limiti il rischio di finire con qualcosa che non volevi.

Tutti i parametri inviati quando si utilizza un’istruzione preparata verranno trattati solo come stringhe (sebbene il motore di database possa eseguire alcune ottimizzazioni, quindi i parametri potrebbero anche diventare numeri, ovviamente). Nell’esempio sopra, se la variabile $name 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à.

Ecco invece un esempio di INSERT (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.

Lascia un commento