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.

  • 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*() e render*().
  • 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']);
versione: 4.0