Lezione Precedente
Lezione Successiva

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''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:
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:
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:

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: 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: 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:

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: Ritengo che la soluzione di quotare i dati sia migliore in quanto:
  1. consente di trattare in maniera letterale le stringhe immesse dall'utente;
  2. è 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: 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&lt=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&lt=4">Link errato</A>
Notare che non funziona perché, siccome la URL è dentro il codice HTML, la sequenza di caratteri &lt 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&amp;lt=4">Link errato</A>
In generale, è bene accertarsi che una di queste due condizioni sia vera: 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:

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().
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:

Per finire, alcuni consigli su come aumentare la sicurezza dei sistemi di database:
Lezione Precedente
Lezione Successiva

I file utili per questa lezione: airdb3.sql, error.php, e .htaccess