Rischi di sicurezza
I database contengono spesso dati sensibili e permettono di eseguire operazioni pericolose. Per lavorare in sicurezza con Nette Database è fondamentale:
- Capire la differenza tra API sicure e insicure
- Usare query parametrizzate
- Validare correttamente i dati in ingresso
Che cos'è la SQL injection?
La SQL injection è il rischio di sicurezza più grave quando si lavora con i database. Nasce quando un input dell'utente non trattato diventa parte di una query SQL. L'attaccante può inserire comandi SQL propri e così:
- Ottenere un accesso non autorizzato ai dati
- Modificare o cancellare dati nel database
- Aggirare l'autenticazione
// ❌ CODICE PERICOLOSO - vulnerabile a SQL injection
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");
// l'attaccante può inserire per esempio il valore: ' OR '1'='1
// e la query risultante sarà: SELECT * FROM users WHERE name = '' OR '1'='1'
// il che restituisce tutti gli utenti
Lo stesso vale per Database Explorer:
// ❌ CODICE PERICOLOSO - vulnerabile a SQL injection
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");
Query parametrizzate
La difesa fondamentale contro la SQL injection sono le query parametrizzate. Nette Database offre diversi modi per usarle.
Il più semplice è usare i segnaposto con punto interrogativo:
// ✅ query parametrizzata sicura
$database->query('SELECT * FROM users WHERE name = ?', $name);
// ✅ condizione sicura in Explorer
$table->where('name = ?', $name);
Lo stesso vale per tutti gli altri metodi di Database Explorer che permettono di inserire espressioni con segnaposto e parametri.
Per i comandi INSERT, UPDATE o per la clausola WHERE possiamo passare i valori in un array:
// ✅ INSERT sicuro
$database->query('INSERT INTO users', [
'name' => $name,
'email' => $email,
]);
// ✅ INSERT sicuro in Explorer
$table->insert([
'name' => $name,
'email' => $email,
]);
Validazione dei valori dei parametri
Le query parametrizzate sono la pietra angolare del lavoro sicuro con il database. I valori che vi inseriamo devono però superare più livelli di controllo:
Controllo dei tipi
La cosa più importante è garantire il tipo di dato corretto dei parametri: è una condizione necessaria per un uso sicuro di Nette Database. Il database presuppone che tutti i dati in ingresso abbiano il tipo di dato corretto, corrispondente alla colonna in questione.
Se per esempio negli esempi precedenti $name fosse inaspettatamente un array invece di una stringa, Nette Database
proverebbe a inserire tutti i suoi elementi nella query SQL, il che porterebbe a un errore. Perciò non usate mai dati non
validati da $_GET, $_POST o $_COOKIE direttamente nelle query al database.
Validazione del formato
Al secondo livello controlliamo il formato dei dati: per esempio se le stringhe sono nella codifica UTF-8 e la loro lunghezza corrisponde alla definizione della colonna, oppure se i valori numerici rientrano nell'intervallo consentito per il tipo di dato della colonna.
A questo livello di validazione possiamo in parte affidarci al database stesso: molti database rifiutano i dati non validi. Il comportamento però può variare, alcuni possono troncare silenziosamente le stringhe lunghe o tagliare i numeri fuori intervallo.
Validazione specifica del dominio
Il terzo livello riguarda i controlli logici specifici della vostra applicazione. Per esempio verificare che i valori dei select box corrispondano alle opzioni offerte, che i numeri rientrino nell'intervallo atteso (per esempio l'età 0–150 anni) oppure che le dipendenze reciproche tra i valori abbiano senso.
Modi consigliati di validare
- Usate i form di Nette, che garantiscono automaticamente la corretta validazione di tutti gli input.
- Usate i presenter e indicate i tipi di dato dei parametri nei metodi
action*()erender*(). - Oppure realizzate un vostro livello di validazione usando gli strumenti standard di PHP come
filter_var().
Lavoro sicuro con le colonne
Nella sezione precedente abbiamo mostrato come validare correttamente i valori dei parametri. Quando però usiamo gli array nelle query SQL, dobbiamo prestare la stessa attenzione anche alle loro chiavi.
// ❌ CODICE PERICOLOSO - le chiavi dell'array non sono trattate
$database->query('INSERT INTO users', $_POST);
Nel caso dei comandi INSERT e UPDATE si tratta di una grave falla di sicurezza: l'attaccante può inserire o modificare una
colonna qualsiasi del database. Potrebbe per esempio impostare is_admin = 1 oppure inserire dati arbitrari in colonne
sensibili (la cosiddetta Mass Assignment Vulnerability).
Nelle condizioni WHERE è ancora più pericoloso, perché possono contenere degli operatori:
// ❌ CODICE PERICOLOSO - le chiavi dell'array non sono trattate
$_POST['salary >'] = 100000;
$database->query('SELECT * FROM users WHERE', $_POST);
// esegue la query WHERE (`salary` > 100000)
Con questo approccio l'attaccante può scoprire sistematicamente gli stipendi dei dipendenti. Può cominciare per esempio con una query sugli stipendi sopra 100.000, poi sotto 50.000 e restringendo via via l'intervallo può rivelare gli stipendi approssimativi di tutti i dipendenti. Questo tipo di attacco si chiama SQL enumeration.
I metodi where() e whereOr() sono poi molto più flessibili e supportano nelle chiavi e nei valori
espressioni SQL, compresi operatori e funzioni. Questo dà all'attaccante la possibilità di eseguire una SQL injection:
// ❌ CODICE PERICOLOSO - l'attaccante può inserire SQL proprio
$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1'];
$table->where($_POST);
// esegue la query WHERE (0) UNION SELECT name, salary FROM users WHERE (1)
Questo attacco chiude la condizione originale con 0), aggiunge con UNION un proprio
SELECT per ottenere dati sensibili dalla tabella users e con WHERE (1) chiude la query
rendendola sintatticamente corretta.
Whitelist delle colonne
Per lavorare in sicurezza con i nomi delle colonne ci serve un meccanismo che garantisca che l'utente possa lavorare solo con le colonne consentite e non possa aggiungerne di proprie. Potremmo provare a rilevare e bloccare i nomi di colonna pericolosi (blacklist), ma è un approccio inaffidabile: l'attaccante può sempre inventare un nuovo modo di scrivere un nome di colonna pericoloso che non avevamo previsto.
È perciò molto più sicuro ribaltare la logica e definire un elenco esplicito di colonne consentite (whitelist):
// colonne che l'utente può modificare
$allowedColumns = ['name', 'email', 'active'];
// rimuoviamo dall'input tutte le colonne non autorizzate
$filteredData = array_intersect_key($userData, array_flip($allowedColumns));
// ✅ ora si può usare in sicurezza nelle query, per esempio:
$database->query('INSERT INTO users', $filteredData);
$table->update($filteredData);
$table->where($filteredData);
Identificatori dinamici
Per i nomi dinamici di tabelle e colonne usate il segnaposto ?name. Garantisce il corretto escaping degli
identificatori secondo la sintassi del database in uso (per esempio con i backtick in MySQL):
// ✅ uso sicuro di identificatori affidabili
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name', $column, $table);
// risultato in MySQL: SELECT `name` FROM `users`
Importante: usate il simbolo ?name solo per valori affidabili definiti nel codice dell'applicazione. Per i valori
provenienti dall'utente usate di nuovo una whitelist. Altrimenti vi esponete a rischi
di sicurezza:
// ❌ PERICOLOSO - non usate mai l'input dell'utente
$database->query('SELECT ?name FROM users', $_GET['column']);