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