Lezione Precedente Elenco Lezioni Lezione Successiva

Laboratorio di Sistemi Informativi

Parametri, SQL Injection e Prepared Statements

Adesso che abbiamo capito come visualizzare l'elenco degli oggetti presenti in mancolista, vogliamo procedere con lo sviluppo dell'applicazione. In particolare, vogliamo far sì che, quando l'utente clicchi su un oggetto, compaia la relativa pagina di dettaglio, con la descrizione completa dell'oggetto e i messaggi del blog associato. Abbiamo dunque necessità di interagire in qualche modo con l'utente del sito. In questa interazione è possibile riconoscere due fasi:
  1. l'utente clicca su link (o, più in generale, specifica i parametri di una ricerca)
  2. all'utente vengono presentati i dati che corrispondono al link cliccato (o che soddisfano i parametri della ricerca).
Prima di lanciarci alla realizzazione della nuova pagina PHP bisogna affrontare un problema: le fasi 1 e 2 sono realizzate da pagine web distinte, eppure la fase 2 utilizza informazioni provenienti dalla fase 1. Come fare a passare queste informazioni da una pagina web ad un'altra?

ATTENZIONE! Prima di proseguire, si noti che da ora in poi, per eseguire i programmi PHP di esempio, avremo bisogno del file config.php (vedi la lezione corrispondente) contenente i parametri opportuni per la connessione al server MySQL. Ecco uno schema (che ognuno di voi deve modificare opportunamente) del file config.php.


Passaggio di parametri

Per quanto visto finora sembra non ci sia alcun modo di passare dei parametri ad una pagina PHP. Se fosse veramente così, le capacità di costruire una applicazione interattiva sarebbero fortemente limitate. Di sicuro non possiamo pensare di realizzare una pagina PHP diversa per ogni oggetto, non solo perché sarebbero tantissime, ma soprattutto perché l'aggiunta di nuovi oggetti ci costringerebbe a creare anche nuova pagine. Fortunatamente, le cose non stanno in questo modo!

Il metodo più diretto di passare dei parametri a PHP è tramite l'indirizzo (URL) della pagina. Se si utilizza un URL del tipo:
http://<hostname>/<percorso>/script.php?param1=val1&param2=val2
quello che succede è che il file script.php viene eseguito come un qualunque file PHP, ma in più sarà possibile accedere ai parametri param1 e param2 tramite l'array associativo predefinito $_GET.

La sintassi generale è per il passaggio dei parametri prevede, dopo il nome della pagina, un punto interrogativo e una lista di coppie <parametro>=<valore>. Ogni elemento della lista è separato dal successivo con una e commerciale (&). Quando lo script PHP viene eseguito, per ogni coppia <parametro>=<valore> si ha che la variabile $_GET["<parametro>"] viene inizializzata con <valore>.

È possibile fare degli esperimenti con questa pagina PHP di esempio:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Prova PHP con parametri</title>
</head>
<body>
<?php
echo "Il parametro param1 vale : ",$_GET["param1"],"<br>";
echo "Il parametro param2 vale : ",$_GET["param2"],"<br>";
?>
</body>
</html>

Salvarla col nome param.php dentro public_html e provare ad accedere all'URL http://<nomeserver>/<percorso>/param.php?param1=<v1>&param2=<v2> per differenti valori di v1 e v2. Notare che le URL vanno codificate in maniera particolare. Ad esempio il simbolo "+"  rappresenta uno spazio, mentre con la notazione "%xx" si indica il carattere il cui codice ASCII è xx (in esadecimale). Non andremo per ora in dettaglio in questo problema della codifica dell'URL, anche perché il browser si preoccupa di codificare in maniera opportuna i parametri che eventualmente non lo fossero già. Per curisità, osservare quello che succede se non si fornisce param1 o param2.

La pagina dei dettagli degli oggetti

Possiamo applicare questa idea per realizzare una pagina, che chiameremo dettagli.php, in grado di visualizzare le informazioni dettagliate di qualunque oggetto presente nella mancolista, sulla base del parametro id con cui viene caricata:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Dettaglio oggetti Mancolista di Paolo</title>
</head>
<body>
<?php
require 'config.php';
$conn=new PDO(DSN,USERNAME,PASSWORD);
$conn->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
$result=$conn->query("SELECT * FROM oggetti WHERE id={$_GET['id']}");
$riga=$result->fetch();
echo "<strong>",$riga["nome"],"</strong><br>";
echo $riga["descrizione_lunga"];
?>
</body>
</html>

In questa pagina non c'è nulla di nuovo rispetto a quanto sappiamo, tranne il fatto che il comando SQL dentro il metodo query viene costruito incollando un pezzo fisso SELECT * FROM oggetti WHERE id= e un pezzo che dipende dai parametri in input {$_GET['id']} (ricordate che, all'interno delle stringhe, le variabili vengono automaticamente rimpiazzate dal loro contenuto e che vi sono delle regole speciali per gli array che spiegano l'uso delle parentesi graffe). Cambiando il parametro in input che specifichiamo nella barra degli indirizzi, cambia il risultato della pagina.

Notare che, per semplicità, visualizziamo solo il nome dell'oggetto e la descrizione lunga, e non consideriamo per ora altre informazioni che andrebbero sicuramente in questa pagina come i commenti del blog.

Ovviamente, non possiamo pretendere che l'utente del nostro sito scriva a mano l'indirizzo della pagina dettagli, completo di parametri. Dobbiamo far sì che, quando l'utente clicca su di un oggetto nella pagina realizzata la lezione scorsa, automaticamente il browser carichi la pagina dettagli.php con i parametri opportuni. Ecco come:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Elenco oggetti Mancolista di Paolo</title>
</head>
<body>
<?php
require 'config.php';
$conn=new PDO(DSN,USERNAME,PASSWORD);
$conn->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
$result=$conn->query("SELECT * FROM oggetti");
while ($riga=$result->fetch())
  echo "<a href='dettagli.php?id={$riga['id']}'>",$riga["nome"],"</a> : ",$riga["descrizione"],"<br>";
?>
</body>
</html>

La parte in rosso mostra quello che abbiamo modificato rispetto alla lezione precedente. Fondamentalmente, ci siamo limitati ad aggiungere, all'interno dell'output del programma, un tag <a> per generare un link. Ogni oggetto ha un link diverso, e l'indirizzo a cui si salta cliccando sul link è sempre diverso: la pagina è sempre dettagli.php, ma a questa pagina si passa un parametro id il cui valore è proprio $riga['id'], ovvero l'identificatore dell'oggetto corrente.

SQL Injection

Per realizzare la pagina dettagli.php abbiamo costruito una query SQL "al volo", combinando un comando fisso ed un argomento variabile (nel nostro caso contenuto in $_GET['id']). In realtà questo modo di procedere è sconsigliato dagli sviluppatori della libreria PDO, in quanto porta facilmente ad avere siti web vulnerabili ad un tipo di attacco nome come SQL Injection.

Vediamo un esempio di un attacco di questo tipo che si può portare avanti sulla applicazione che stiamo sviluppando. Una cosa che sicuramente lo sviluppatore di un sito web non vuole è che sia possibile conoscere l'elenco degli utenti del sito o, peggio ancora, le loro password. Purtroppo, la pagina dettagli.php che abbiamo scritto, per quanto innocua, ha un terribile bug che consente a chiunque di sapere gli username e le password di tutti gli utenti del sito (le password per fortuna sono criptate, quindi non immediatamente utilizzabili).

Sebbene la cosa sembri impossibile, provate a fornire il seguente indirizzo:

http://<server>/<percorso>/dettagli.php?id=0 union select 1,username,1,password,1,1,1,1 from utenti limit 0,1

Il risultato sarà veder comparire, invece di nome e descrizione lunga di un oggetto, la username e password criptata del primo elemento della tabella utenti. Come è possibile? Consideriamo quello che succede quando lo script dettagli.php viene eseguito con questa URL. Viene costruita la query SELECT * FROM oggetti WHERE id= seguita dal valore del parametro $_GET['id'], che è 0 union select 1,username,1,password,1,1,1,1 from utenti limit 0,1. Se mettiamo tutto insieme (e formattiamo bene per maggior chiarezza), il comando SQL che otteniamo è:

SELECT * FROM oggetti WHERE id=0 
  UNION
SELECT 1,username,1,password,1,1,1,1 FROM utenti LIMIT 0,1

Il primo comando SELECT non restituisce alcun risultato, perché non ci sono oggetti con id pari a 0, ma la clausola UNION causa l'esecuzione anche della seconda SELECT, i cui risultati vengono aggiunti a quelli della prima. La seconda SELECT è strana per un motivo preciso: perché la UNION funzioni, le due SELECT devono produrre due tabelle con lo stesso numero di colonne: poiché la tabella oggetti ha 8 campi, il risultato della seconda SELECT deve avere pure 8 colonne. I sei numeri uno servono appunto a questo. Inoltre, poiché lo script visualizza solo i campi nome e descrizione_lunga, gli uno sono messi in modo tale che i campi username e password vadano a finire proprio in posizione 2 e 4, in corrispondenza dei campi nome e descrizione_lunga nella tabella oggetti. Infine, LIMIT 0,1 è una estensione di MySQL che dice di estrarre solo un sottoinsieme del risultato della query: il primo numero, 0, è il numero della riga da cui iniziare ad estrarre il risultato (0 è la prima riga), e il secondo (1) è il numero di righe da estrarre. Modificando lo 0 con 1, 2, etc... è possibile generare come risultato le varie righe della tabella utente. Ecco il risultato della query quando eseguita dal monitor SQL:

mysql> select * from oggetti where id=0 union select 1,username,1,password,1,1,1,1 from utenti limit 0,1;
+----+------+-------------+------------------------------------------+--------------+-----------+---------+------------+
| id | nome | descrizione | descrizione_lunga                        | categoria_id | utente_id | dataora | confermato |
+----+------+-------------+------------------------------------------+--------------+-----------+---------+------------+
|  1 | ut1  | 1           | cdf1e43d1dad39898a0656c2ec84eaf3e81b2a90 |            1 |         1 | 1       |          1 | 
+----+------+-------------+------------------------------------------+--------------+-----------+---------+------------+
1 row in set (0.00 sec)

I campi nome e descrizione_lunga verranno regolarmente visualizzati dal resto dello script PHP, che ignora il fatto che non provengono dalla tabella oggetti ma da quella utenti.

Volendo riassumere il problema, quello che succede è che il parametro $_GET['id'], che dovrebbe contenere solo un valore con cui confrontare il campo id della tabella utenti, contiene invece un vero e proprio pezzo di comando SQL, che modifica completamente la semantica della query presente in dettagli.php. Ci sono vari modi per impedire che tutto questo succeda, e su questi torneremo in una prossima lezione, ma un primo metodo molto efficace è l'uso dei comandi preparati (prepared statements).

I prepared statement

Quando una query SQL dipende da parametri presenti in delle variabili, come è nel caso di dettagli.php, invece di usare il metodo PDO::query e costruire manualmente la query risultante incollando, è consigliabile utilizzare i metodi PDO::prepare e PDOStatement::execute.

Per esempio, ecco come modificare dettagli.php usando i prepared statements:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Dettaglio oggetti Mancolista di Paolo</title>
</head>
<body>
<?php
require 'config.php';
$conn=new PDO(DSN,USERNAME,PASSWORD);
$conn->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
$stmt=$conn->prepare("SELECT * FROM oggetti WHERE id=?");
$stmt->execute(array($_GET['id']));
$riga=$stmt->fetch();
echo "<strong>",$riga["nome"],"</strong><br>";
echo $riga["descrizione_lunga"];
?>
</body>
</html>

Con la nuova versione siamo al sicuro da errori di tipo SQL Injection. Per quanto proviamo a fornire dei parametri id bizzarri, questi non vengono mai interpretati come comandi SQL, ma solo come valori con cui confrontare il campo id della tabella oggetti. Tuttalpiù la pagina dettagli.php non darà alcun risultato, ma non potremo mai ottenere informazioni provenienti da altre tabelle. Inoltre, sebbene il programma con i prepared statements abbia una riga in più di quello senza, la separazione tra comandi e parametri nella query SQL rende il tutto più chiaro.

Esercizi e Soluzioni

Esercizio 1

Inserire un paio righe nella tabella messaggi, e modificare dettagli.php in modo che visualizzi i messaggi relativi all'oggetto selezionato.

Lezione Precedente Elenco Lezioni Lezione Successiva

Valid HTML 4.01 Transitional Valid CSS!