Rischi per la sicurezza
I database contengono spesso dati sensibili e consentono di eseguire operazioni pericolose. Per lavorare in sicurezza con Nette Database, gli aspetti chiave sono:
- Comprendere la differenza tra API sicure e non sicure.
- Utilizzare query parametrizzate
- Convalidare correttamente i dati in ingresso
Che cos'è l'iniezione SQL?
L'iniezione SQL è il rischio di sicurezza più grave quando si lavora con i database. Si verifica quando l'input dell'utente non filtrato diventa parte di una query SQL. Un aggressore può inserire i propri comandi SQL e quindi:
- estrarre dati non autorizzati
- Modificare o cancellare dati nel database
- bypassare l'autenticazione
// CODICE PERICOLOSO - vulnerabile a un'iniezione SQL
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");
// Un utente malintenzionato potrebbe inserire un valore come: ' OR '1'='1
// La query risultante sarebbe: SELECT * FROM users WHERE name = '' OR '1'='1'
// Che restituisce tutti gli utenti
Lo stesso vale per Database Explorer:
// CODICE PERICOLOSO - vulnerabile a un'iniezione SQL
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");
Query parametriche sicure
La difesa fondamentale contro le SQL injection è rappresentata dalle query parametrizzate. Nette Database offre diversi modi per utilizzarle.
Il modo più semplice è quello di utilizzare i segnaposto dei punti interrogativi:
// Query parametriche sicure
$database->query('SELECT * FROM users WHERE name = ?', $name);
// Condizione sicura in Explorer
$table->where('name = ?', $name);
Questo vale per tutti gli altri metodi di Database Explorer che consentono di inserire espressioni con segnaposto e parametri con punto interrogativo.
Per le clausole INSERT
, UPDATE
, o WHERE
è possibile passare i valori in una
matrice:
// Inserimento sicuro
$database->query('INSERT INTO users', [
'name' => $name,
'email' => $email,
]);
// Inserimento sicuro in Explorer
$table->insert([
'name' => $name,
'email' => $email,
]);
Convalida dei valori dei parametri
Le query parametrizzate sono la pietra miliare del lavoro sicuro sui database. Tuttavia, i valori passati in esse devono essere sottoposti a diversi livelli di convalida:
Controllo del tipo
Assicurare il tipo di dati corretto dei parametri è fondamentale: è una condizione necessaria per utilizzare in modo sicuro Nette Database. Il database presuppone che tutti i dati in ingresso abbiano il tipo di dati corretto corrispondente alla colonna.
Ad esempio, se $name
negli esempi precedenti diventasse inaspettatamente un array anziché una stringa, Nette
Database tenterebbe di inserire tutti i suoi elementi nella query SQL, dando luogo a un errore. Pertanto, non utilizzare mai**
dati non validati da $_GET
, $_POST
o $_COOKIE
direttamente nelle query del database.
Convalida del formato
Il secondo livello verifica il formato dei dati, ad esempio assicurandosi che le stringhe siano codificate UTF-8 e che la loro lunghezza corrisponda alla definizione della colonna, oppure verificando che i valori numerici rientrino nell'intervallo consentito per il tipo di dati della colonna.
A questo livello, si può fare parzialmente affidamento sul database stesso: molti database rifiutano i dati non validi. Tuttavia, il comportamento può variare: alcuni possono troncare silenziosamente le stringhe lunghe o tagliare i numeri fuori dall'intervallo.
Convalida specifica del dominio
Il terzo livello prevede controlli logici specifici per l'applicazione. Ad esempio, la verifica che i valori delle caselle di selezione corrispondano alle opzioni disponibili, che i numeri rientrino in un intervallo previsto (ad esempio, età 0–150 anni) o che le relazioni tra i valori abbiano senso.
Metodi di convalida raccomandati
- Utilizzare Nette Forms, che gestisce automaticamente la convalida di tutti gli input.
- Utilizzare i Presenter e dichiarare i tipi di dati dei parametri nei
metodi
action*()
erender*()
. - Oppure implementare un livello di validazione personalizzato usando strumenti PHP standard come
filter_var()
.
Lavorare in sicurezza con le colonne
Nella sezione precedente è stato illustrato come convalidare correttamente i valori dei parametri. Tuttavia, quando si utilizzano gli array nelle query SQL, occorre prestare la stessa attenzione alle loro chiavi.
// CODICE PERICOLOSO - le chiavi degli array non vengono sanificate
$database->query('INSERT INTO users', $_POST);
Per i comandi INSERT e UPDATE, questa è una grave falla nella sicurezza: un utente malintenzionato può inserire
o modificare qualsiasi colonna del database. Potrebbe, ad esempio, impostare is_admin = 1
o inserire dati arbitrari
in colonne sensibili (nota come Mass Assignment Vulnerability).
Le condizioni WHERE sono ancora più pericolose perché possono contenere operatori:
// CODICE PERICOLOSO - le chiavi dell'array non vengono sanificate
$_POST['salary >'] = 100000;
$database->query('SELECT * FROM users WHERE', $_POST);
// esegue la query WHERE (`salario` > 100000)
Un utente malintenzionato può utilizzare questo approccio per scoprire sistematicamente gli stipendi dei dipendenti. Potrebbe iniziare con una query per stipendi superiori a 100.000, poi inferiori a 50.000 e, restringendo gradualmente l'intervallo, potrebbe rivelare gli stipendi approssimativi di tutti i dipendenti. Questo tipo di attacco è chiamato enumerazione SQL.
I metodi where()
e whereOr()
sono ancora più flessibili e supportano espressioni SQL, compresi operatori e
funzioni, sia nelle chiavi che nei valori. Ciò consente a un aggressore di eseguire complesse iniezioni SQL:
// CODICE PERICOLOSO - l'aggressore 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 nome, stipendio FROM utenti WHERE (1)
Questo attacco termina la condizione originale con 0)
, aggiunge la propria SELECT
utilizzando
UNION
per ottenere dati sensibili dalla tabella users
e chiude con una query sintatticamente corretta
utilizzando WHERE (1)
.
Whitelist delle colonne
Per lavorare in sicurezza con i nomi delle colonne, è necessario un meccanismo che garantisca che gli utenti possano interagire solo con le colonne consentite e non possano aggiungerne di proprie. Il tentativo di individuare e bloccare i nomi di colonna pericolosi (blacklist) è inaffidabile: un aggressore può sempre trovare un nuovo modo di scrivere un nome di colonna pericoloso che non è stato previsto.
Pertanto, è molto più sicuro invertire la logica e definire un elenco esplicito di colonne consentite (whitelisting):
// Colonne che l'utente può modificare
$allowedColumns = ['name', 'email', 'active'];
// Rimuovere tutte le colonne non autorizzate dall'input
$filteredData = array_intersect_key($userData, array_flip($allowedColumns));
// Ora è sicuro da usare nelle query, come ad esempio:
$database->query('INSERT INTO users', $filteredData);
$table->update($filteredData);
$table->where($filteredData);
Identificatori dinamici
Per i nomi dinamici di tabelle e colonne, utilizzare il segnaposto ?name
. Questo assicura il corretto escape
degli identificatori secondo la sintassi del database (ad esempio, usando i backtick in MySQL):
// Utilizzo sicuro degli identificatori di fiducia
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name', $column, $table);
// Risultato in MySQL: SELECT `nome` FROM `users`
Importante: utilizzare il simbolo ?name
solo per i valori attendibili definiti nel codice dell'applicazione. Per
i valori forniti dall'utente, utilizzare nuovamente una whitelist. In caso contrario, si
rischiano vulnerabilità di sicurezza:
// ❌ PERICOLOSO - non utilizzare mai l'input dell'utente
$database->query('SELECT ?name FROM users', $_GET['column']);