Che cos’è SQL Injection e come prevenire nelle applicazioni PHP?

Quindi pensi che il tuo database SQL sia performante e al sicuro dalla distruzione istantanea? Bene, SQL Injection non è d’accordo!

Sì, stiamo parlando della distruzione istantanea, perché non voglio aprire questo articolo con la solita terminologia scadente di “rafforzare la sicurezza” e “prevenire l’accesso dannoso”. SQL Injection è un trucco così vecchio nel libro che tutti, ogni sviluppatore, lo conosce molto bene ed è ben consapevole di come prevenirlo. Tranne quella strana volta in cui sbagliano, ei risultati possono essere a dir poco disastrosi.

Se sai già cos’è SQL Injection, sentiti libero di saltare alla seconda metà dell’articolo. Ma per coloro che sono appena entrati nel campo dello sviluppo web e sognano di assumere ruoli più importanti, è necessaria una presentazione.

Cos’è l’iniezione SQL?

La chiave per comprendere SQL Injection è nel suo nome: SQL + Injection. La parola “iniezione” qui non ha alcuna connotazione medica, ma piuttosto è l’uso del verbo “iniettare”. Insieme, queste due parole trasmettono l’idea di inserire SQL in un’applicazione web.

Mettere SQL in un’applicazione web . . . mmmm. . . Non è quello che stiamo facendo comunque? Sì, ma non vogliamo che un utente malintenzionato controlli il nostro database. Capiamolo con l’aiuto di un esempio.

Supponiamo che tu stia costruendo un tipico sito Web PHP per un negozio di e-commerce locale, quindi decidi di aggiungere un modulo di contatto come questo:

<form action="record_message.php" method="POST">
  <label>Your name</label>
  <input type="text" name="name">
  
  <label>Your message</label>
  <textarea name="message" rows="5"></textarea>
  
  <input type="submit" value="Send">
</form>

E supponiamo che il file send_message.php memorizzi tutto in un database in modo che i proprietari del negozio possano leggere i messaggi degli utenti in seguito. Potrebbe avere un codice come questo:

<?php

$name = $_POST['name'];
$message = $_POST['message'];

// check if this user already has a message
mysqli_query($conn, "SELECT * from messages where name = $name");

// Other code here

Quindi stai prima provando a vedere se questo utente ha già un messaggio non letto. La query SELECT * dai messaggi in cui name = $name sembra abbastanza semplice, giusto?

SBAGLIATO!

Nella nostra innocenza, abbiamo aperto le porte alla distruzione istantanea del nostro database. Affinché ciò accada, l’attaccante deve soddisfare le seguenti condizioni:

  • L’applicazione è in esecuzione su un database SQL (oggi quasi tutte le applicazioni lo sono)
  • La connessione al database corrente dispone delle autorizzazioni di “modifica” e “eliminazione” sul database
  • I nomi dei tavoli importanti possono essere indovinati

Il terzo punto significa che ora che l’attaccante sa che stai gestendo un negozio di e-commerce, molto probabilmente stai memorizzando i dati dell’ordine in una tabella degli ordini. Armato di tutto questo, tutto ciò che l’attaccante deve fare è fornire questo come nome:

Joe; troncare gli ordini;? Si signore! Vediamo cosa diventerà la query quando verrà eseguita dallo script PHP:

SELECT * FROM messaggi WHERE nome = Joe; troncare gli ordini;

Ok, la prima parte della query ha un errore di sintassi (nessuna virgoletta intorno a “Joe”), ma il punto e virgola costringe il motore MySQL a iniziare a interpretarne uno nuovo: ordini troncati. Proprio così, in un solo colpo, l’intera cronologia degli ordini è sparita!

Ora che sai come funziona SQL Injection, è il momento di vedere come fermarlo. Le due condizioni che devono essere soddisfatte per una corretta SQL injection sono:

  • Lo script PHP dovrebbe avere privilegi di modifica/cancellazione sul database. Penso che questo sia vero per tutte le applicazioni e non sarai in grado di rendere le tue applicazioni di sola lettura. 🙂 E indovina un po’, anche se rimuoviamo tutti i privilegi di modifica, l’iniezione SQL può comunque consentire a qualcuno di eseguire query SELECT e visualizzare tutto il database, dati sensibili inclusi. In altre parole, la riduzione del livello di accesso al database non funziona e la tua applicazione ne ha comunque bisogno.
  • L’input dell’utente è in fase di elaborazione. L’unico modo in cui l’iniezione SQL può funzionare è quando si accettano dati dagli utenti. Ancora una volta, non è pratico interrompere tutti gli input per la tua applicazione solo perché sei preoccupato per l’iniezione SQL.
  • Prevenire l’iniezione SQL in PHP

    Ora, dato che le connessioni al database, le query e gli input dell’utente fanno parte della vita, come possiamo prevenire l’SQL injection? Per fortuna, è piuttosto semplice e ci sono due modi per farlo: 1) disinfettare l’input dell’utente e 2) usare dichiarazioni preparate.

    Disinfetta l’input dell’utente

    Se stai utilizzando una versione precedente di PHP (5.5 o inferiore, e questo accade spesso sull’hosting condiviso), è consigliabile eseguire tutto l’input dell’utente tramite una funzione chiamata mysql_real_escape_string(). Fondamentalmente, ciò che fa rimuove tutti i caratteri speciali in una stringa in modo che perdano il loro significato quando vengono utilizzati dal database.

    Ad esempio, se hai una stringa come I’m a string, il carattere di virgoletta singola (‘) può essere utilizzato da un utente malintenzionato per manipolare la query del database che viene creata e causare un’iniezione SQL. Eseguendolo attraverso mysql_real_escape_string() produce I’m a string, che aggiunge una barra rovesciata al singolo apice, eseguendo l’escape. Di conseguenza, l’intera stringa ora viene passata come stringa innocua al database, invece di poter partecipare alla manipolazione delle query.

    C’è uno svantaggio con questo approccio: è una tecnica molto, molto vecchia che va di pari passo con le vecchie forme di accesso al database in PHP. A partire da PHP 7, questa funzione non esiste nemmeno più, il che ci porta alla nostra prossima soluzione.

    Usa dichiarazioni preparate

    Le istruzioni preparate sono un modo per rendere le query del database più sicure e affidabili. L’idea è che invece di inviare la query non elaborata al database, per prima cosa diciamo al database la struttura della query che invieremo. Questo è ciò che intendiamo per “preparare” una dichiarazione. Una volta preparata una dichiarazione, passiamo le informazioni come input parametrizzati in modo che il database possa “riempire le lacune” collegando gli input alla struttura della query che abbiamo inviato in precedenza. Questo toglie qualsiasi potere speciale che gli input potrebbero avere, facendoli trattare come semplici variabili (o payload, se vuoi) nell’intero processo. Ecco come appaiono le dichiarazioni preparate:

    <?php
    $servername = "localhost";
    $username = "username";
    $password = "password";
    $dbname = "myDB";
    
    // Create connection
    $conn = new mysqli($servername, $username, $password, $dbname);
    
    // Check connection
    if ($conn->connect_error) {
        die("Connection failed: " . $conn->connect_error);
    }
    
    // prepare and bind
    $stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)");
    $stmt->bind_param("sss", $firstname, $lastname, $email);
    
    // set parameters and execute
    $firstname = "John";
    $lastname = "Doe";
    $email = "[email protected]";
    $stmt->execute();
    
    $firstname = "Mary";
    $lastname = "Moe";
    $email = "[email protected]";
    $stmt->execute();
    
    $firstname = "Julie";
    $lastname = "Dooley";
    $email = "[email protected]";
    $stmt->execute();
    
    echo "New records created successfully";
    
    $stmt->close();
    $conn->close();
    ?>

    So che il processo sembra inutilmente complesso se sei nuovo alle dichiarazioni preparate, ma il concetto vale lo sforzo. Ecco una bella introduzione ad esso.

    Per coloro che hanno già familiarità con l’estensione DOP di PHP e la usano per creare dichiarazioni preparate, ho un piccolo consiglio.

    Avvertenza: prestare attenzione durante l’impostazione di DOP

    Quando si utilizza DOP per l’accesso al database, possiamo essere risucchiati da un falso senso di sicurezza. “Ah, beh, sto usando DOP. Ora non ho bisogno di pensare a nient’altro” — questo è il modo in cui generalmente va il nostro pensiero. È vero che PDO (o dichiarazioni preparate da MySQLi) è sufficiente per prevenire tutti i tipi di attacchi di iniezione SQL, ma devi fare attenzione quando lo imposti. È normale copiare e incollare il codice dai tutorial o dai progetti precedenti e andare avanti, ma questa impostazione può annullare tutto:

    $dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);

    Ciò che fa questa impostazione è dire a PDO di emulare le dichiarazioni preparate piuttosto che utilizzare effettivamente la funzione delle dichiarazioni preparate del database. Di conseguenza, PHP invia semplici stringhe di query al database anche se il tuo codice sembra creare istruzioni preparate e impostare parametri e tutto il resto. In altre parole, sei vulnerabile all’iniezione SQL come prima. 🙂

    La soluzione è semplice: assicurati che questa emulazione sia impostata su false.

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

    Ora lo script PHP è costretto a utilizzare istruzioni preparate a livello di database, impedendo ogni tipo di SQL injection.

    Impedire l’uso di WAF

    Sai che puoi anche proteggere le applicazioni web da SQL injection utilizzando WAF (web application firewall)?

    Bene, non solo SQL injection ma molte altre vulnerabilità di livello 7 come lo scripting cross-site, l’autenticazione interrotta, la contraffazione cross-site, l’esposizione dei dati, ecc.

    SQL injection e moderni framework PHP

    L’iniezione SQL è così comune, così facile, così frustrante e così pericolosa che tutti i moderni framework web PHP sono dotati di contromisure integrate. In WordPress, ad esempio, abbiamo la funzione $wpdb->prepare(), mentre se stai utilizzando un framework MVC, fa tutto il lavoro sporco per te e non devi nemmeno pensare a impedire l’iniezione SQL. È un po’ seccante che in WordPress si debbano preparare dichiarazioni in modo esplicito, ma ehi, stiamo parlando di WordPress. 🙂

    Ad ogni modo, il mio punto è che la razza moderna di sviluppatori web non deve pensare all’iniezione SQL e, di conseguenza, non sono nemmeno consapevoli della possibilità. Pertanto, anche se lasciano una backdoor aperta nella loro applicazione (forse è un parametro di query $ _GET e le vecchie abitudini di lanciare una query sporca entrano in gioco), i risultati possono essere catastrofici. Quindi è sempre meglio prendersi il tempo per immergersi più a fondo nelle fondamenta.

    Conclusione

    SQL Injection è un attacco molto sgradevole a un’applicazione Web, ma è facilmente evitabile. Come abbiamo visto in questo articolo, prestare attenzione durante l’elaborazione dell’input dell’utente (a proposito, SQL Injection non è l’unica minaccia che comporta la gestione dell’input dell’utente) e interrogare il database è tutto ciò che c’è da fare. Detto questo, non sempre lavoriamo nella sicurezza di un framework web, quindi è meglio essere consapevoli di questo tipo di attacco e non caderci.