SQL Injection

Vediamo come una pagina web apparentemente funzionante può nascondere gravi falle di sicurezza che rendono un attaccante in grado di effettuare operazioni non autorizzate.
La tipologia di vulnerabilità che andremo ad analizzare è quella della SQL Injection che permette ad un attaccante di eseguire query (quasi) arbitrarie e senza controllo sul nostro database..
Assumo che abbiate già dimestichezza con il linguaggio SQL e cercherò di mostrarvi un approccio più malizioso all'argomento.

ATTENZIONE: quanto indicato di seguito ha lo scopo di istruirvi su come scrivere codice non vulnerabile.
Potete provare questa tecnica sui vostri sistemi, ma vi ricordo che hackerare un sistema altrui senza autorizzazione è reato.

Vediamo un esempio pratico, considerando un database MySQL con varie tabelle tra le quali queste:


    CREATE TABLE prodotti (
        id_prodotto INT UNSIGNED NOT NULL AUTO_INCREMENT,
        nome VARCHAR(64) NOT NULL,
        descrizione VARCHAR(255) DEFAULT NULL,
        PRIMARY KEY (`id`)
    );

    CREATE TABLE utenti (
        id INT UNSIGNED NOT NULL AUTO_INCREMENT,
        username VARCHAR(64) NOT NULL,
        password VARCHAR(255) DEFAULT NULL,
        PRIMARY KEY (id),
        UNIQUE(username)
    );

(mettete in queste tabelle qualche record a caso per fare le prove)

Scriviamo una pagina "prodotto.php" che prende come parametro l'ID del prodotto e da in output il record corrispondente:


    <?php
    $mysqli = new mysqli("localhost", "nomeUtente", "password", "nomeDatabase");
    $result = $mysqli->query("SELECT * FROM prodotti WHERE id_prodotto=".$_REQUEST['id']);
    $obj = $result->fetch_object();
    echo json_encode($obj);
    $result->free();
    $mysqli->close();

Rendiamo la pagina operativa: tramite terminale, lanciamo un webserver andando nella directory che contiene il file php e digitando il comando:


    php -t . -S localhost:8080

La pagina prodotto.php, nonostante sia funzionante, contiene una vulnerabilità. Cerchiamo di capire il perché.
Quello che fa è eseguire questa query (mettendo al posto di id il parametro che viene passato tramite url):


    SELECT * FROM prodotti WHERE id_prodotto=id

Normalmente chiameremmo la pagina con un url del genere:

    http://localhost:8080/prodotto.php?id=1

Quindi sostituendo ad id il parametro 1 verrebbe eseguita la query

    SELECT * FROM prodotti WHERE id_prodotto=1

ed otterremo un risultato simile a questo:

    {"id_prodotto":"1","nome":"prodotto1","descrizione":"prodotto di test"}

Fino a qui tutto regolare, abbiamo chiesto alla pagina prodotto.php di mostrarci il prodotto numero 1 e così è stato fatto.
Ma attenzione, se al posto di un semplice numero passassimo come parametro un pezzo di query cosa succederebbe?
Ad esempio come parametro "id" proviamo a passare questo pezzo di query:


1 and 1=2
union 
select id, username, password
from utenti
limit 1

Ecco l'attacco: invochiamo tramite browser l'url


    http://localhost:8080/prodotto.php?id=1%20and%201=2%20union%20select%20id,%20username,%20passwd%20from%20utenti%20limit%201

ottenendo un risultato del tipo

    {"id_prodotto":"1","nome":"lorenzo","descrizione":"lamiapassword"}

Avete notato cosa è successo? Abbiamo (ab)usato di una pagina che doveva servire solo a mostrare i prodotti, costringendola a rivelarci dati presenti invece in un'altra tabella; in questo caso quella con gli utenti e le password! Questo perchè inviando quel parametro malformato la query finale che è stata eseguita è questa:


SELECT * 
FROM prodotti 
WHERE id_prodotto=1 and 1=2
UNION 
SELECT id, username, password
FROM utenti
LIMIT 1

Intuirete che con la giusta fantasia possiamo costringere prodotto.php ad eseguire un gran numero di query diverse da quelle per la quale era stato creato ed estrarre dati ai quali, come utenti, non dovremmo aver accesso.

Come va scritto il codice per scongiurare l'sql injection? Si devono avere queste due accortezze:

  • validare sempre tutti gli input
  • non concatenare stringhe per comporre le query dinamicamente, ma piuttosto utilizzare i prepared statement
Questa è la versione immune all'sql injection di prodotto.php

    <?php
    // validazione del parametro "id" tramite espressione regolae
    if(!isset($_REQUEST['id']) || !preg_match("/^[0-9]+$/", $_REQUEST['id'])) {
        http_response_code(400); // 400=bad request
        die();
    }
    $mysqli = new mysqli("localhost", "nomeUtente", "password", "nomeDatabase");
    // invece di utilizzare il metodo query(), utilizzo un prepared statement
    $stmt = $mysqli->prepare("SELECT id_prodotto, nome, descrizione FROM prodotti WHERE id=?");
    $stmt->bind_param("i", $_REQUEST['id']);
    $stmt->execute();
    $result = $stmt->get_result();
    $obj = $result->fetch_object();
    echo json_encode($obj);
    $result->free();
    $stmt->close();
    $mysqli->close();

Se provate infatti ad eseguire l'attacco indicato in precedenza su quest'ultima versione di prodotto.php esso non funzionerà.

In conclusione, l'argomento è molto vasto e scrivere applicazioni con un alto livello di sicurezza non è certo facile. Spero almeno di aver reso l'idea di cosa sia una vulnerabilità e di come sia possibile sfruttarla (o meglio correggerla!)
Per maggiori informazioni su sql injection ed altri tipi di vulnerabilità comuni nelle applicazioni web potete approfondire visitando il sito del progetto OWASP