Lezione Precedente | Elenco Lezioni | Lezione Successiva |
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.
http://<hostname>/<percorso>/script.php?param1=val1¶m2=val2quello 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.
<valore>
.
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.
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).
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
.
PDOStatement PDO::prepare( string $statement )
: prepara MySQL ad eseguire il comando $statement
. Quest'ultimo può contenere dei punti interrogativi al posto di valori che andranno specificati successivamente. Il risultato di questo metodo è un oggetto di tipo PDOStatement
, come il risultato di PDO::query
, che però non è ancora pronto ad essere utilizzato: deve essere prima eseguito.bool PDOStatement::execute( array $input_parameters )
: esegue un comando preparato con PDO::prepare
. I punti interrogativi eventualmente presenti nel comando preparato vengono rimpiazzati, in ordine da sinistra a destra, con i valori dell'array $input_parameters
. Restituisce TRUE
se l'esecuzione ha avuto successo, FALSE
altrimenti (a meno che non siano abilitate le eccezioni, come facciamo noi, nel qual caso se l'esecuzione non ha successo si genera una eccezione). Una volta che lo statement preparato è stato eseguito, si può utilizzare il metodo PDOStatement::fetch
per accedere al risultato, come nel caso di query non preparata.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.
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 |