Rischi per la sicurezza
Il database contiene spesso dati sensibili e consente di eseguire operazioni pericolose. Per lavorare in sicurezza con Nette Database è fondamentale:
- Comprendere la differenza tra API sicure e non sicure
- Utilizzare query parametrizzate
- Validare correttamente i dati di input
Cos'è SQL Injection?
SQL injection è il rischio di sicurezza più grave quando si lavora con un database. Si verifica quando l'input non trattato di un utente diventa parte di una query SQL. Un attaccante può inserire i propri comandi SQL e quindi:
- Ottenere accesso non autorizzato ai dati
- Modificare o cancellare dati nel database
- Bypassare l'autenticazione
// ❌ CODICE PERICOLOSO - vulnerabile a SQL injection
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");
// L'attaccante può inserire ad esempio il valore: ' OR '1'='1
// La query risultante sarà: SELECT * FROM users WHERE name = '' OR '1'='1'
// Che restituirà 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 SQL injection sono le query parametrizzate. Nette Database offre diversi modi per utilizzarle.
Il modo più semplice è utilizzare placeholder a punto interrogativo:
// ✅ Query parametrizzata sicura
$database->query('SELECT * FROM users WHERE name = ?', $name);
// ✅ Condizione sicura in Explorer
$table->where('name = ?', $name);
Questo vale per tutti gli altri metodi in Database Explorer, che consentono di inserire espressioni con placeholder a punto interrogativo e parametri.
Per i comandi INSERT, UPDATE o 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. Tuttavia, i valori che inseriamo in esse devono passare attraverso diversi livelli di controllo:
Controllo del tipo
La cosa più importante è garantire il tipo di dato corretto dei parametri – questa è una condizione necessaria per l'uso sicuro di Nette Database. Il database presuppone che tutti i dati di input abbiano il tipo di dato corretto corrispondente alla colonna data.
Ad esempio, se $name
negli esempi precedenti fosse inaspettatamente un array invece di una stringa, Nette Database
tenterebbe di inserire tutti i suoi elementi nella query SQL, il che porterebbe a un errore. Pertanto, non utilizzare mai
dati non validati da $_GET
, $_POST
o $_COOKIE
direttamente nelle query del database.
Controllo del formato
Al secondo livello, controlliamo il formato dei dati – ad esempio, se le stringhe sono in codifica UTF-8 e la loro lunghezza corrisponde alla definizione della colonna, o se i valori numerici rientrano nell'intervallo consentito per il tipo di dato della colonna.
A questo livello di validazione, possiamo parzialmente fare affidamento anche sul database stesso: molti database rifiuteranno dati non validi. Tuttavia, il comportamento può variare, alcuni potrebbero silenziosamente troncare stringhe lunghe o tagliare numeri fuori intervallo.
Controllo del dominio
Il terzo livello rappresenta i controlli logici specifici della tua applicazione. Ad esempio, verificare che i valori delle caselle di selezione corrispondano alle opzioni offerte, che i numeri siano nell'intervallo previsto (ad es. età 0–150 anni) o che le dipendenze reciproche tra i valori abbiano senso.
Metodi di validazione consigliati
- Utilizzare Nette Forms, che garantiscono automaticamente la corretta validazione di tutti gli input
- Utilizzare i Presenter e specificare i tipi di dati per i parametri nei
metodi
action*()
erender*()
- Oppure implementare un proprio layer di validazione utilizzando strumenti PHP standard come
filter_var()
Lavoro sicuro con le colonne
Nella sezione precedente, abbiamo mostrato come validare correttamente i valori dei parametri. Tuttavia, quando si utilizzano array nelle query SQL, dobbiamo prestare la stessa attenzione anche alle loro chiavi.
// ❌ CODICE PERICOLOSO - le chiavi nell'array non sono trattate
$database->query('INSERT INTO users', $_POST);
Nei comandi INSERT e UPDATE, questo è un errore di sicurezza critico: un attaccante può inserire o modificare qualsiasi
colonna nel database. Potrebbe, ad esempio, impostare is_admin = 1
o inserire dati arbitrari in colonne sensibili
(la cosiddetta Mass Assignment Vulnerability).
Nelle condizioni WHERE, è ancora più pericoloso, perché possono contenere operatori:
// ❌ CODICE PERICOLOSO - le chiavi nell'array non sono trattate
$_POST['salary >'] = 100000;
$database->query('SELECT * FROM users WHERE', $_POST);
// esegue la query WHERE (`salary` > 100000)
Un attaccante può utilizzare questo approccio per scoprire sistematicamente gli stipendi dei dipendenti. Inizia, ad esempio, con una query sugli stipendi superiori a 100.000, poi inferiori a 50.000 e restringendo gradualmente l'intervallo, può rivelare gli stipendi approssimativi di tutti i dipendenti. Questo tipo di attacco è chiamato SQL enumeration.
I metodi where()
e whereOr()
sono ancora molto più flessibili e supportano espressioni SQL nelle chiavi e
nei valori, inclusi operatori e funzioni. Ciò dà all'attaccante la possibilità di eseguire SQL injection:
// ❌ CODICE PERICOLOSO - l'attaccante può inserire il proprio SQL
$_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 termina la condizione originale con 0)
, aggiunge il proprio SELECT
utilizzando
UNION
per ottenere dati sensibili dalla tabella users
e chiude la query sintatticamente corretta con
WHERE (1)
.
Whitelist delle colonne
Per lavorare in sicurezza con i nomi delle colonne, abbiamo bisogno di 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 questo approccio è inaffidabile: un attaccante può sempre trovare un nuovo modo per scrivere un nome di colonna pericoloso che non avevamo previsto.
Pertanto, è molto più sicuro invertire la logica e definire un elenco esplicito di colonne consentite (whitelist):
// Colonne che l'utente può modificare
$allowedColumns = ['name', 'email', 'active'];
// Rimuoviamo tutte le colonne non consentite dall'input
$filteredData = array_intersect_key($userData, array_flip($allowedColumns));
// ✅ Ora possiamo usarlo in sicurezza nelle query, come ad esempio:
$database->query('INSERT INTO users', $filteredData);
$table->update($filteredData);
$table->where($filteredData);
Identificatori dinamici
Per nomi dinamici di tabelle e colonne, utilizzare il placeholder ?name
. Questo garantisce il corretto escaping
degli identificatori secondo la sintassi del database dato (ad esempio, utilizzando 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: utilizzare il simbolo ?name
solo per valori affidabili definiti nel codice dell'applicazione. Per
i valori provenienti dall'utente, utilizzare nuovamente la whitelist. Altrimenti, ci si
espone a rischi per la sicurezza:
// ❌ PERICOLOSO - non utilizzare mai l'input dell'utente
$database->query('SELECT ?name FROM users', $_GET['column']);