Laboratorio di Sistemi Informativi
Sicurezza per le applicazioni web
Il fatto di utilizzare degli input dall'esterno può costituire un pericolo tremendo per la sicurezza del sito web. In effetti, nella maggior parte dei sistemi, il PHP è configurato automaticamente in modo tale da alleviare questo problema. Nella distribuzione di Linux usata in laboratorio, tuttavia, la configurazione standard di PHP non abilita queste funzionalità di "sicurezza" che, comunque, sono da sole insufficcienti. Ne approfittiamo per vedere in che modo gli input dall'esterno mettano in crisi la sicurezza della nostra applicazione, e le possibili contromisure da adottare.
SQL Injection
Consideriamo lo
script per il login in php visto la lezione scorsa ma leggermente
modificato: login-bug.php.
I cambiamenti consistono
essenzialmente nel fatto che visualizziamo la query prima di passarla a
MySQL, in modo da renderci conto di quello che succede.
Se immettiamo, come username e password, delle stringhe contenenti
apici, queste vengono inserite tali
e quali nella query che segue
select * from utenti where
username='$_POST[user]' and passwd=password('$_POST[passwd]')
al posto di $_POST[user]
e $_POST[passwd]. Questo
comporta
vari problemi: ad esempio, lo script potrebbe fallire, restituendo
degli errori
all'utente, in quanto la query SQL risulta mal formata. Ma peggio
ancora, è possibile autenticarsi con
successo senza sapere nè la password nè la username!!!!!!
Esperimento. Scoprire (senza sbirciare sotto) che valori si possono inserire in
login e password per ottenere l'accesso all'aera protetta (ovviamente
escludendo le login e passwd vere presenti nella tabella
airdb.utenti).
Se so che uno degli utenti privilegiati ha la login ut1, ma non ne conosco la password, posso comunque ottenere l'accesso alla sezione riservata specificando una password a caso, ma con il segunte valore di login: ut1' or ''='.
Così facendo, la query che viene passata a MySQL è
la seguente:
select * from utenti where username='ut1' or ''='' and passwd='hashed password'
che restituisce tutte le righe della tabella utenti con username pari ad ut1, in quanto il controllo della password è sempre in "or" con la condizione username='ut1'
. In questo modo, il controllo successivo sul valore di mysql_num_rows() ha successo e ciò consente l'accesso alle pagine web riservate. Questo tipo di attacco prende il nome di SQL Injection, in quanto del codice SQL viene "iniettato" in un punto dove dovrebbero esserci soltanto dei dati. Come fare a proteggersi?
Consideriamo un istante come vengono inserite le stringhe in SQL.
Queste sono delle sequenze di caratteri racchiuse tra apici. Ad esempio
'str1' e 'prova' sono esempi di
stringhe.
In realtà MySQL permette di usare anche i doppi apici
(virgolette) invece degli apici singoli, ma questa è una
estensione rispetto allo standard SQL, quindi è bene non
abusarne. Alcune combinazioni di caratteri dentro una stringa vengono
trattate di maniera speciale. Alcune di queste sequenze, dette sequenze di escape sono:
- \n : indica il
carattere New Line (codice ASCII 10)
- \r: indica il
carattere Carriage Return (codice ASCII 13)
- \t: indica il
carattere di tabulazione (codice ASCII 8)
- \\: indica il
carattere \ (backslash)
- \': indica l'apice
singole
- \": indica le
virgolette
- \0: indica il
caratter NUL (codice ASCII 0)
Notare che sono essenzialmente le stesse sequenze di caratteri speciali
che sono interpretate anche da PHP per le sue strighe. Tuttavia, mentre
PHP interpreta queste sequenze solo quando la stringa delimitata
da virgolette, MySQL le interpreta anche quando la stringa è
delimitata da doppi apici. Ad esempio SELECT 'a\t\\b'
restituisce: a
b.
Ricordiamo che queste sequenze sono utili qualora si voglia inserire un
apice o
una virgoletta dentro una stringa. Non si può inserire un apice
direttamente, perchè verrebbe interpretato come indicazione di
fine della stringa. Così, la parola po' viene rappresentata dalla
stringa 'po\'' e non da 'po''.
Quello che occorre fare è, prima di passare la stringa di
input alla query SQL, trasformarla in modo da eliminare tutti gli apici
e le virgolette e sostituirli con \' e \". In questo modo, il MySQL li
interpreta correttamente come caratteri che fanno parte della
stringa, e non che la delimitano. Questa trasformazione
è nota con il termine quoting (che si
potrebbe tradurre col verbo quotare in italiano).
Per quotare una stringa, si può utilizzare la funzione:
- string mysql_real_escape_string(string
unescaped_string [, resource link_identifier]): prende una
stringa in input e restituisce una stringa quotata in modo da
sostituire i caratteri \, ', " e NUL con
\\, \', \" e \0. Al solito, l'ultimo parametro opzionale è
il risultato della mysql_connect.
Se modifichiamo di poco lo
script di prima per ottenere login-nobug.php,
non sarà
più possibile aggirare il sistema di protezione (almeno con
questo trucco).
ATTENZIONE! Negli esempi di
sopra, sia login-bug.php
che login-nobug.php
visualizzano la query
prima di mandarla al server MySQL. Mentre questa cosa è stata
fatta allo scopo di rendere più chiaro in che modo la procedura
di login può essere attaccabile, l'idea di visualizzare la query
prima di mandarla a MySQL può essere utilissima in fase di
debugging.
Quando qualche operazione non ha l'effetto desiderato, e l'operazione
coinvolge una query a MySQL, provate a visualizzare la query sullo
schermo. Magari potreste scoprire che non ha la forma che vi
aspettavate perchè, ad esempio, avete scritto male il nome di
qualche variabile.
L'opzione magic_quotes_gpc
Una possibilità alternativa all'uso di mysql_real_escape_string è quella di configurare PHP in modo tale che tutte le stringhe provenienti dall'utente (con i metodi GET e POST) vengano automaticamente quotate. Per attivare questa modalità, occorre recuperare un nostro vecchio amico, il file .htaccess, utilizzato in passato per attivare la visualizzazione dei messaggi di errore da parte di PHP e per disabilitare il buffering delle pagine. Se in questo file aggiungiamo la riga
php_value magic_quotes_gpc On
tutte le stringhe in $_GET e $_POST sono automaticamente quotate alla fonte. Se l'utente immette la stringa 'prova' (apici compresi) in un campo di nome input, la variabile $_GET['input'] conterrà il valore \'prova\'.
Se proviamo login-bug.php con la configurazione magic_quotes_gpc = On, tutto funziona correttamente come se avessimo quotato le stringhe con mysql_real_escape_string. Purtroppo, non sempre il file .htaccess è utilizzabile, perchè l'amministratore del server web ne potrebbe aver inibito il funzionamento. Se vi ricordate, nelle prime lezioni di PHP abbiamo visto la funzione ini_set, che consente di modificare a run-time (cioè durante l'esecuzione) un parametro di PHP. Purtroppo, non tutti i parametri si possono modificare con ini_set, e magic_quotes_gpc è uno di quelli non modificabili.
Bisogna tuttavia stare attenti a utilizzare o la funzione mysql_real_escape_string() o la configurazione magic_quotes_gpc = On, ma non tutte e due assieme, altrimenti i caratteri speciali verranno quotati due volte producendo un risultato errato (anche se non pericoloso dal punto di vista della sicurezza).
Un altro problema creato da magic_quotes_gpc = On
è che, comunque, al PHP non viene passato direttamente quello che ha scritto l'utente. Quando il dato in input non deve essere usato come argomento di una query SQL ma in altri contesti, non è bene che sia stato modificato in questo modo. Ad esempio, quando magic_quotes_gpc = On
, è impossibile effettuare con successo un login dalla nostra pagina login.php
se la password che si vuole utilizzare contiene un apice. Questo perchè la stringa che viene sottoposta alla funzione sha1
non è la vera password immessa dal'utente, ma sempre la versione quotata.
A seguito di questi incovenienti, il consiglio è quello di tenere disabilitato magic_quotes_gpc quando questo è possibile. Se non lo è, si può rimediare utilizzando le seguenti funzioni:
int get_magic_quotes_gpc(void)
: restituisce il valore corrente (0 per Off, 1 per On) di magic_quotes_gpc
;
string stripslashes (string str)
: prende la stringa in input ed elimina tutti i backslash \, tranne la coppia \\ che viene rimpiazzata da \.
In questo modo è possibile controllare se magic_quotes_gpc
è abilitato e, in caso affermativo, usare stripslashes
per annullarne gli effetti. A quel punto si continua normalmente come se magic_quotes_gpc
fosse disabilitato.
Esercizio 1
Si crei una nuova voce nella tabella utenti con login D'Angelo e password prova. Si verifichi che è possibile fare il login con questo utente sia usando login-bug.php
, quando magic_quotes_gpc
è abilitato, sia con login-nobug.php
, quando magic_quotes_gpc
è disabilitato. Controllare che non è possibile effettuare il login con la pagina logic-nobug.php
se magic_quotes_gpc
è abilitato.
Esercizio 2
Modificare login-nobug.php
in modo che funzioni (in maniera sicura) indipendentemente dal valore di magic_quotes_gpc
, anche nel caso dell'utente D'Angelo dell'esercizio precedente o nel caso di password contenenti apici.
Dati di tipo numerico
Supponiamo di avere degli identificatori utente di tipo numerico (come su ICQ ad esempio). Ad esempio, creiamo una tabella nel database test, contenente gli identificatori dei vari utenti e le informazioni correlate. Qualcosa del tipo:
CREATE TABLE identificatori (
uid int,
nome varchar(30)
);
INSERT INTO identificatori VALUES (86386,'utente prova');
Supponiamo di utilizzare lo script login-numerico.php per controllare se un utente è valido oppure no. In questo caso è possibile sempre fornire, al momento del login, un valore del tipo 0 or 0=0 che verrà autenticato correttamente indipendetemente dal contenuto della tabella identificatori. E questo nonostante abbiamo usato la funzione mysql_clean. Il problema è che la query utilizzata è la seguente:
select nome from identificatori where uid=$user
dove $user viene rimpiazzato con il valore inserito dall'utente. Visto che $user non è racchiuso tra apici (in quanto ci aspettiamo un valore numerico) è possibile modificare il significato della query anche con input che non contengono apici o altri caratteri strani, e che quindi non sono alterati dal quoting. Il modo più semplice per risolvere il problema è mettere tra apici il valore di $user, in questo modo:
select nome from identificatori where uid='$user'
Se adesso $user valesse 0 or 0=0, otterremmo la query
select nome from identificatori where uid='0 or 0=0'
che ovviamente restituisce un insieme vuoto di tuple. Il sistema continua a funzionare correttamente quando l'utente immette valori numerici, perchè MySQL esegue una conversione automatica da stringhe a interi.
Un'altra possibilità è quella di convertire esplicitamente i parametri di ingresso in valori numerici. A questo si può usare la seguente funzione:
int intval(mixed var)
: converte il dato in input in un tipo intero, e restituisce 0 in caso di errore. Se l'input è una stringa che inizia con un numero e poi ha dei caratteri alfanumerici, la parte iniziale verrà convertita in intero è restituita. Ad esempio intval("44 gatti")
restituirà il valore 44;
Basta sostituire la riga $user=mysql_real_escape_string($_POST['user']);
con $user=intval($_POST['user']);
e lo script sarà esente da guai, sia nel caso si usi la query modificata select nome from identificatori where uid='$user'
che nel caso della query originale select nome from identificatori where uid=$user
.
Per concludere, la morale di tutto questo discorso è: mai fidarsi dei dati che provengono dall'utente quando li si passa ad una query SQL. Prima di utilizzarli, controllare che non sia possibile inserire dei dati volutamente senza senso, allo scopo di eseguire istruzioni SQL non previste.
Buffer overflow
Talvolta il nostro programma è scritto in modo da aspettarsi in
input stringhe di una certa lunghezza massima. Cosa succede se gli
arriva una stringa più grande? Sui linguaggi evoluti come
PHP o Java, questo non è un problema particolarmente
grave, ma quando i programmi sono scritti in linguaggi come il C, la
cosa si fa seria. In C, quando si usa una stringa, bisogna dichiararne
la lunghezza. Se poi si prova a metterci dentro una stringa più
lunga di quella massima, i dati "sforano" e sovrascrivono la memoria al
di fuori dello spazio riservato... a questo punto tutto può
succedere. Questo, effettivamente, è uno degli attacchi
più comuni, che prende il nome di buffer overflow.
In PHP e Java la memoria viene gestita dal linguaggio e non
direttamente dal programmatore, per cui non ci dovrebbero essere
problemi. Però, c'è sempre il rischio di un bug nell'interprete PHP,
Apache o MySQL (i programmi coinvolti nelle nostre applicazion), per
cui è meglio, prima di usare delle stringhe prese in input,
troncarle ad una dimensione accettabile. Ad esempio, se la variabile $_GET['nome']
verrà ad essere inserita in un campo del database lungo 100
caratteri, possiamo tranquillamente troncarla a questa dimensione.
Possiamo allora perfezionare la nostra protezione, facendo in modo che la variabile $_POST['user']
, oltre ad essere quotata, sia anche troncata ai primi 10 caratteri. Analogamente, possiamo troncare anche $_POST['passwd']
, nel caso la funzione sha1
di PHP abbia qualche bug. Nel caso di $_POST['passwd']
non dobbiamo fare riferimento alla lunghezza dell'omonimo campo nella tabella utenti, perchè quel campo è pensato per contenere la firma dell password, e non la password stessa. Qualunque dimensione ragionevole va bene, ma diciamo che la tronchiamo ai primi 100 caratteri. Per troncare le strighe, possiamo usare la funzione predefina substr
:
- string substr ( string stringa, int start [, int
length] ): restituisce la porzione di stringa che parte alla
posizione start ed
è lunga length
caratteri. La prima posizione è la posizione 0, e se length non viene fornito, la
funzione restituisce tutta la stringa a partire dalla posizione start.
Quello che dovremmo fare è quindi rimpiazzare la istruzione
$user=mysql_real_escape_string($_POST['user']);
in login-nobug.php con
$user=mysql_real_escape_string(substr($_POST['user'],0,10));
e in maniera simile per $hpw. L
Otteniamo una nuova versione della procedura di login, la login-nobug2.php.
Per chi fosse interessato, una definizione più estesa di buffer
overflow si può trovare in questo articolo di searchSecurity.com,
mentre una descrizione molto più tecnica si trova su questo articolo di LinuxJournal.
Cross-Site Scripting
Un attacco di tipo SQL Injection si ottiene sfruttando il fatto che un dato può contenere dei caratteri che vengono interpretati in maniera "semanticamente errata" all'interno di una query SQL.Un tipo di attacco simile è il Cross-Site Scripting (XSS) che, però, usa HTML come "veicolo": cosa succede se dei dati che vengono visalizzati sullo schermo contengono dei tag HTML? Ovviamente, i tag vengono interpretati dal browser.
Consideriamo lo script inserimento-aeroporto.php per l'inserimento di un nuovo aeroporto. Se viene inserito un nuovo aeroporto con codice XYZ e nome <b>prova<b>, il nome prova
verrà visualizzato in grassetto, ad esempio, nella pagine di conferma inserimento nuovo volo o nuovo aeroporto. Notare che le formattazioni HTML non vengono eseguite all'interno di un tag OPTION
, per cui il nome non compare in grassetto nei campi di input a risposta multipla.
Il problema non è tanto che è possibile inserire una voce in grassetto (anche se già questo non è proprio piacevole), ma che è possibile inserire del codice in JavaScript, che può essere eseguito dal browser dell'ignaro utente che si connette al sito web e che, per qualche motivo, visualizza il nome del nostro nuovo aeroporto. Ad esempio,cosa succede se si inserisce come nome dell'aeroporto il valore prova<script> alert('XSS');</script> ?
Nel caso meno grave, una debolezza di questo tipo consente di rendere inservibile il nostro sito web (cosa che in gergo informatico prende il nome di attacco DOS, acronimo di Denial Of Service). Ma nei casi più gravi, il codice JavaScript può fare cose anche più complesse, ad esempio redirigere il browser dell'utente a un sito web che sembra uguale a quello originario, ma non lo è. A questo punto l'utente ignaro potrebbe prenotare un volo, pensando di essere nel sito web della compagnia, e magari inserire un numero di carta di credito, quando invece i dati immessi raggiungono tutt'altra destinazione.
Il problema è che, come per le stringhe che vengono mandate a MySQL, anche le stringhe che vengono mandate al browser dovrebbero essere quotate, quando non siamo sicuri del loro contenuto. A questo scopo, è possibile utilizzare la seguente funzione:
string htmlspecialchars (string stringa)
: restituisce una stringa ottenuta da quella in input, sostituendo & per &, " per le virgolette, < per il simbolo < e > per il simbolo >.
Se prima di mandare una stringa in output la si processa con htmlspecialchars
, siamo sicuri che eventuali tag HTML non verranno interpretati come tali. Nel nostro caso, si tratta di modificare le righe
Codice aeroporto: <?php echo $row['id']; ?><P>
Nome Aeroporto: <?php echo $row['nome']; ?>
con
Codice aeroporto: <?php echo htmlspecialchars($row['id']); ?><P>
Nome Aeroporto: <?php echo htmlspecialchars($row['nome']); ?>
ATTENZIONE. Ovviamente, non basta modificare solo il file inserimento-aeroporto.php, ma occorre modificare tutti gli script PHP che visualizzano dei dati provenienti originariamente da un utente esterno.
In realtà, esiste un'altra possibilità per proteggerci da questo tipo di attacco. Invece di quotare le stringhe che si mandano al browser, si potrebbero filtrare i dati immessi dall'utente, per esempio eliminando i tag HTML. Per far ciò esiste, ad esempio, una funzione apposita che si chiama strip_tags
:
string strip_tags (string str)
: restituisce la stringa str ma eliminando tutti i tag e i commenti HTML.
Ritengo che la soluzione di quotare i dati sia migliore in quanto:
- consente di trattare in maniera letterale le stringhe immesse dall'utente;
- è più sicura, perché anche nel caso il database venga compromesso in qualche modo, inserendo qualche tag HTML al suo interno, questi tag comparirebbero in maniera letterale nelle pagine web.
Esercizio 3
Modificare inserimento-aeroporto.php
in modo da eliminare i tag HTML sia dal codice aeroporto che dalla descrizione.
Quotare le URL
Purtroppo, i problemi non finiscono mai. Quello che affrontiamo adesso non è propriamente un problema di sicurezza, quanto una causa di possibili malfunzionamenti dell'applicazione. Cosa succede adesso se inseriamo un codice aeroporto che comprende un e commerciale, ad esempio &a ? Quando viene invocata la funzione redirect_browser
, il browser viene rediretto alla pagina
inserimento-aeroporto.php?id=&a
Il problema è che il PHP non capisce che la & in questa URL non serve a separare la variabile id da qualche altra variabile, ma costituisce il contenuto della variabile id.
Il fatto è che anche le URL, come il testo HTML, deve essere quotato, per trasformare la & in un %26 (dove 26 è il codice ASCII di & in esadecimale) che viene correttamente interpretato da PHP. A questo scopo usiamo la seguente funzione:
string urlencode (string url)
: quota la stringa passata come input, rimpiazzando tutti i caratteri non alfabetici con %xx dove xx è il codice ASCII del carattere. Inoltre rimpiazza gli spazi con il simbolo +.
Tutte le stringe passate come parametri in una URL devono essere protette da urlencode
. Ad esempio, nel nostro caso, la riga
redirect_browser("$_SERVER[PHP_SELF]?id=$_POST[id]");
diventa
redirect_browser("$_SERVER[PHP_SELF]?id=".urlencode($_POST['id']));
Combinando questa modifica con quella precedente, si ottiene lo script inserimento-aeroporto-htmlok.php.
URL all'interno di codice HTML
L'esempio di sopra funziona quando la URL è utilizzata con redirect_browser
e quindi, in definitiva, viene inviata nell'header del protocollo HTTP. Quando invece la URL viene utilizzata all'interno del codice HTML, sorge un altro problema. Supponiamo di avere uno script provalt.php
che utilizza i parametri x ed lt come input:
<?php
echo "Il parametro x vale: ",$_GET['x'],"<br>";
echo "Il parametro lt vale: ",$_GET['lt'];
?>
Per quanto abbiamo visto, se mettiamo nella barra degli indirizzi del browser la stringa provalt.php?x=3<=5, ci aspettiamo che venga visualizzato il seguente output:
Il parametro x vale: 3
Il parametro lt vale: 5
Questo è, effettivamente, ciò che accade. Il problema è che, se invece mettiamo questa URL dentro un tag <A>, le cose non funziona come dovrebbero. Si provi ad esempio, a cliccare la scritta "Link errato" visualizzata dalla seguente pagina minimale:
<A href="provalt.php?x=3<=4">Link errato</A>
Notare che non funziona perché, siccome la URL è dentro il codice HTML, la sequenza di caratteri < viene interpretata come il codice del simbolo minore, per cui la URL a cui effettivamente si salta è provalt.php?x=3<=4. La pagina con il link va allora corretta in
<A href="provalt.php?x=3&lt=4">Link errato</A>
In generale, è bene accertarsi che una di queste due condizioni sia vera:
- non si utilizzano parametri il cui nome possa essere confuso con una entità HTML;
- tutte le & siano sostituite in & nelle URL presenti nel codice HTML, eventualmente ricorrendo alla funzione
htmlspecialchars
.
Ci sono altre situazioni che richiedono ulteriori operazioni per il quotaggio delle stringhe, ma le funzioni che abbiamo esaminato in questa lezione sono sufficienti per la maggior parte dei casi.
Uno schema generale per applicazioni sicure
Sembra ormai chiaro che i dati che vengono presi in input dall'utente costituiscono una fonte di problemi senza limite. È bene quindi avere una strategia per gestirli nel migliore dei modi.
I punti fondamentali sono due:
- tutto l'input deve essere filtrato: cosa fare esattamente durante l'operazione di filtraggio dipende da cosa ci si aspetta in input. Esaminiamo alcuni casi, senza pretesa di completezza:
- stringhe: la norma generale è quella di troncarle ad una lunghezza accettabile, per evitare errori di buffer overflow. Inoltre, se
magic_quotes_gpc
è abilitato, è consigliabile eliminare la quotatura automatica, in modo da ottenere le stringhe nella loro forma "nativa";
- interi: si può trasformare l'input in un intero utilizzando la funzione
intval
di cui abbiamo già parlato, o lo si può trattare come se fosse di tipo stringa;
- altri: caso per caso, sono possibili operazioni di filtraggio più specifiche. Ad esempio, per il campo codice aeroporto, si può controllare che sia una stringa di esattamente 3 caratteri alfabetici maiuscoli. Da questo punto di vista, il problema della sicurezza si intreccia con quello della validazione dei dati.
Sempre per quanto riguarda l'operazione di filtraggio, c'è anche da decidere cosa fare quando i dati non sono esattamente del tipo che ci si aspetta: le due possibilità sono quella di interrompere l'esecuzione dello script, eventualmente segnalando un errore, oppure continuare lo stesso, ma trasformando il dato in input in qualcosa che comunque è accettabile. Ad esempio, se uno script accetta come input un identificatore di aereo che però, invece di essere un intero, è una stringa del tipo "44gatti", lo script deve continuare con il valore 44 come identificatore, o deve arrestarsi? Nelle nostre lezioni abbiamo sempre seguito l'approccio più "permissivo", ma anche l'altro è del tutto plausibile.
- tutto l'output deve essere quotato: quando un qualunque dato deve essere mandato in output al browser, al server SQL, o ad un altro agente esterno, occorre sempre ricordarsi di quotarlo rispettando i requisiti di chi deve riceverlo. Nei casi esaminati, questo vuol dire usare
mysql_real_escape_string
per MySQL e htmlspecialchars
per il browser. È necessario quotare sempre tutti i dati? Alcune volte lo si può evitare. Ad esempio, se siamo assolutamente sicuri che un certa variabile $x
contenga un codice aeroporto valido (3 caratteri alfabetici maiuscoli), la si può mandare in output con echo $x
piuttosto che con echo htmlspecialchars($x)
, visto che un dato di questo tipo non può contenere tag HTML. Tuttavia, siamo veramente sicuri che $x
abbia il formato che ci aspettiamo? Magari pensiamo che lo sia perché $x
proviene da un campo codice aeroporto del nostro database, e noi controlliamo a priori tutti i valori che inseriamo in questa tabella. Ma cosa succede se un cracker è riuscito, magari da un'altra applicazione che non è scritta bene come la nostra, a inserire un valore che non rispetta la specifica dei 3 caratteri alfabetici? Quando non siamo assolutamente sicuri, è meglio errare nella prudenza, e quotare comunque i dati.
Per semplificare lo sviluppo di applicazioni secondo questo modello, è possibile crearsi delle funzioni ausiliarie per il filtraggio dei dati in input o per quotare tipi particolari di dati in output. Vediamo l'esempio di una generica funzione di filtraggio, che elimina l'effetto di magic_quotes_gpc
(se abilitato) e tronca l'input ad una lunghezza massima specificata. Altre funzioni che controllano anche il tipo di dato in input (se si tratta di una data, di un intero, etc..) possono essere ottenute a partire da questa:
function filter_generic($str,$len) {
if (get_magic_quotes_gpc())
return substr(stripslashes($str),0,$len);
else
return substr($str,0,$len);
}
Utilizzando questi principi possiamo riscrivere tutte le pagine relative alla nostra applicazione. A titolo di esempio, ecco le nuove versioni delle pagine di inserimento volo e inserimento aeroporto, che prevedono anche il controllo sulla corretta autenticazione dell'utente: inserimento-volo-sicuro.php, inserimento-aeroporto-sicuro.php.
In queste pagine, tutti gli input sono trattati come tipi stringa, anche quelli come il codice aereo che sarebbero più propriamente interi. I codici aeroporti sono considerati stringhe di 3 caratteri qualunque, e non viene forzata la regola che siano 3 caratteri alfabetici. Inoltre, si è deciso di utilizzare un approccio "permissivo" al filtraggio degli input: se ad esempio un dato di input è più lungo di quanto dovrebbe essere, esso viene semplicemente troncato. Una soluzione alternativa sarebbe stata quella di interrompere l'esecuzione dello script.
Il codice di queste pagine è anche eccessivamente prolisso: ad esempio, in inserimento-volo-sicuro.php
, nella parte che si occupa effettivamente di eseguire le query SQL, abbiamo:
$numaereo=filter_generic($_POST['numaereo'],6);
$idsrc=filter_generic($_POST['idsrc'],3);
$iddst=filter_generic($_POST['iddst'],3);
$data=filter_generic($_POST['data'],20);
......
$mnumaereo=mysql_real_escape_string($numaereo);
$midsrc=mysql_real_escape_string($idsrc);
$middst=mysql_real_escape_string($iddst);
$mdata=mysql_real_escape_string($data);
Il primo gruppo di assegnamenti crea le variabili filtrate, che verranno poi successivamente quotate per l'inserimento nella query SQL. Visto che l'unico utilizzo di $numaereo, $idsrc, $iddst e $data è nelle query SQL, si sarebbe potuto evitare la duplicazione degli assegnamenti, e rimpiazzare il tutto con assegnamenti del tipo
$mnumaereo=mysql_real_escape_string($filter_generic($_POST['numaereo'],6);
Tuttavia, si è preferito avere un programma più lungo e prolisso per illustrare meglio il principio generale.
Notare inoltre che è stata utilizzata un ben precisa convenzione sui nomi delle variabili, che ci consente di individuare a colpo d'occhio se una variabile sia stata filtrata e/o quotata: riferendoci al dato idsrc
, abbiamo che $_POST['idsrc']
contiene il dato originale proveniente dall'esterno, $idsrc
il dato filtrato e $midsrc
quello quotato per MySQL.
Pulizia dell'applicazione
Visto che in questa lezione ci siamo occupati, oltre che di sicurezza, anche di come far funzionare in maniera più corretta la nostra applicazione, dedichiamoci ad altri aspetti di questo tipo.
Prima di tutto, la funzione filter_generic
può essere inserita nella pagina error.php. Anzi, siccome error.php contiene funzioni di utilità generale per la nostra applicazione, che non hanno strettamente a che fare con la gestione degli errori, cambiamogli nome in qualcosa di più sensato, ad esempio libreria.php.
Sembra anche sensato evitare di scrivere costanti come 3 o 100 all'interno del codice PHP. Infatti, se a un certo momento decidessimo di modificare la lunghezza del campo aeroporti.nome
, bisognerebbe andare alla ricerca di tutti i numeri 100 nei nostri script PHP e rimpiazzarli con la nuova lunghezza. Analogamente, non è buona norma inserire nelle mysql_connect direttamente il nome dell'host a cui collegarsi, il nome utente e la password da usare per la connessione: ogni variazione da apportare a questi dati, richiederebbe infatti la modifica di praticamente tutti gli script dell'applicazione.
Decidiamo allora di spostare tutti questi dati in un file a parte, che
chiamiamo config.php.
Questo verrà incluso all'inizio di ogni script, esattamente come
libreria.php, e
conterrà un insieme di dichiarazioni di costanti. Una costante in
PHP si definisce con la funzione define().
- bool define (string name, mixed value):
definisce una costante name
che ha il valore value.
Restituisce true se
l'operazone ha successo, false
altrimenti.
Ad esempio, se do il comando
define('HOSTNAME','localhost');
successivamente posso usare la costante HOSTNAME al posto della stringa
localhost, ad esempio in
mysql_connect(HOSTNAME,'studente');
Se a un certo punto sposto il server database su www.unich.it, basta modificare
il comando define di cui
sopra con
define('HOSTNAME','www.unich.it')
e tutto il resto dell'applicazione rimane invariato.
Applicando le opportune modifica alla pagina per l'inserimento degli aeroporti, otteniamo gli script inserimento-aeroporto-sicuro2.php
e i due script ausiliari libreria.php
e config.php.
Altri problemi di sicurezza
Quelli che abbiamo visto sono solo alcuni dei problemi di sicurezza che si possono presentare. Tuttavia, il problema generale della sicurezza dei sistemi informativi in rete è ben più vasto, e sicuramente andrebbe affrontato in un corso interamente dedicato.
Ad ogni modo, una breve guida sulla sicurezza delle applicazioni PHP è la PHP Security Guide. Qui vogliamo invece elencare altri aspetti tipici delle problematiche di sicurezza, non strettamente collegati al PHP:
- trasmissione in chiaro / cifrata.
I dati che viaggiano dal browser al server web (e viceversa) sono
normalmente in chiaro (non criptati). Questo vuol dire che è possibile
intercettarli, se ci si trova su una macchina che sta lungo il percorso
di trasmissione tra browser e server web.
Di questo ovviamente bisogna tener conto quando si trasmettono in un
senso o nell'altro dati importanti, ad esempio password o numeri di
carta di credito. Queste informazioni devono
essere trasmesse in modo cifrato. La maggior parte dei browser e dei
server web consentono di effettuare anche connessioni cifrate (il
protocollo in questo caso non è http ma https).
Lo stesso problema si presenta nella connessione tra server web e
server di database. La connessione è normalmente in chiaro.
Spesso questo è sufficiente, o perché il server web e di database
sono la stessa macchina (come nel nostro caso) o perché sono macchine diverse
della stessa azienda, nella quale, si suppone, non esiste nessun
malintenzionato. Se tuttavia si vuole una assoluta sicurezza, anche in
questo caso è necessario che la connessione tra server web e
server database venga criptata. MySQL supporta la possibilità di
connessioni criptate.
-
sicurezza del server di database.
Di solito il server di database non dovrebbe essere accessibile
dall'esterno della rete locale dell'azienda in cui è inserito.
L'unico accesso dall'esterno dovrebbe avvenire tramite l'interfaccia
web. In questo modo si evita che eventuali bug del server di database,
una sua errata configurazione o una scelta poco furba delle password
consenta a qualche cracker di entrare direttamente nella base di dati.
- sicurezza del server web.
Il server web è uno dei punti più delicati per la
sicurezza del sistema informativo. I cracker sono sempre alla ricerca
di bug che gli consentano di superare i sistemi di protezione o di
eseguire programmi non consentiti. Le problematiche relative sono molto
complesse. La configurazione corretta del web server non spetta
ovviamente al progettista dell'applicazione che usa il database
nè all'amministratore del database, ma ad una figura dedicata
che
si occupa della sicurezza globale della rete.
Per finire, alcuni consigli su come aumentare la sicurezza dei sistemi
di database:
- le applicazioni che usano i database non dovrebbero connettersi
come utente root. Per ogni applicazione dovrebbe essere creato un
utente
speciale con i minimi privilegi necessari a portare a termini i compiti
previsti dall'applicazione. Se l'applicazione si collega come utente
root e un cracker riesce a trovare il modo di forzarla e farle eseguire
una query SQL a piacere, egli ha il completo controllo del database. Se
invece la connessione avviene con un nome utente dotato di privilegi
limitati, anche le possibilità del cracker saranno limitate.
- fare in modo che il sistema non visualizzi troppi messaggi di
errore, in particolare quelli generati automaticamente da PHP. Questi
ultimi sono utili allo sviluppatore per capire cosa sta succedendo, ma
sono pericolosi quando appaiono in un sistema
allo stato di produzione, utilizzato quotidianamente. Gli utenti
comuni
infatti se ne fanno poco di messaggi che non capiscono e sui quali
comunque non possono agire, mentre i cracker possono trarre da questi
messaggi informazioni preziose per capire come è organizzato il sistema,
che tipo di interrogazione vengono impartite e quindi, alla fin fine, per capire
come portare avanti un attacco. Per questo, una volta sperimentato che il
sistema funziona correttamente, è bene disabilitare la
visualizzazione dei messaggi d'errori (cosa che si realizza facilmente
sostituendo la riga "php_value
display_errors 1" con "php_value
display_errors 0" nel file .htaccess)
- ancora, come detto prima: mai
fidarsi dei dati immessi dall'utente!! Prima di utilizzarli
pensare sempre due volte a come questo dato potrebbe essere utilizzato
per fare qualcosa di non previsto e di dannoso.
I file utili per questa lezione: airdb3.sql,
error.php,
e .htaccess