SELECT, Sicurezza avanzata e Griglia Utenti per Città - Tecnologia Anno 5 Modulo 3 Lezione 3 | Prof. Giagnotti
Tecnologia · Anno 5 · Modulo 3 · Lezione 3 - SELECT & Sicurezza Avanzata
✍ Autore: profgiagnotti · Materia: Tecnologia - PHP & MySQL Sicuro · SELECT & Protezioni & Griglia Città

Dalla SELECT alla griglia: leggere dati, 5 tecniche anti-SQL Injection ed esercizio città con PHP_SELF

Impariamo a leggere tutti gli utenti con fetch_assoc(), ripassiamo SQL Injection con login bypass, applichiamo 5 protezioni (prepared, escape, validazione, parole vietate, substr) e realizziamo l'esercizio completo: scelta città da SELECT DISTINCT e griglia risultati con single-page PHP_SELF + header() redirect.

Anno 5Modulo 3 Lezione 3SELECT SicurezzaGriglia ~60 min
OBIETTIVI
✓Connettersi a MySQL con mysqli, eseguire SELECT * e iterare con num_rows e fetch_assoc()
✓Comprendere SQL Injection con esempio login e 5 tecniche di protezione pratiche
✓Applicare prepared statement, mysqli_real_escape_string, ctype_alnum, controllo parole vietate, substr per buffer overflow
✓Creare tabella cittadini, popolare con dati e usare SELECT DISTINCT per popolare <select> dinamicamente
✓Realizzare esercizio completo in 2 file (cercacitta.php + elencocitta.php) e poi in single-page con $_SERVER['PHP_SELF'], isset() e header() redirect

1. Connessione al Database

Per interagire con un database MySQL in PHP, possiamo utilizzare mysqli o PDO. Ecco un esempio di connessione con mysqli (già visto in L1/L2):

PHP - CONNESSIONE.MYSQLI
<?php $servername = "localhost"; $username = "root"; $password = ""; $dbname = "test";$conn = new mysqli($servername, $username, $password, $dbname);if ($conn->connect_error) { die("Connessione fallita: " . $conn->connect_error); } ?>

2. SELECT e iterazione risultati

Ipotizziamo che il database test contenga la tabella utenti. Vogliamo selezionare e mostrare tutti i record.

SQL - SELECT BASE
-- Tutti i campi SELECT * FROM utenti;-- Solo alcuni campi SELECT id, nome, cognome, email FROM utenti;

A questo punto memorizziamo la query in una stringa PHP ed eseguiamola:

PHP - ESECUZIONE SELECT
<?php $sql = "SELECT id, nome, cognome, email FROM utenti"; $result = $conn->query($sql); ?>

Ora iteriamo i risultati riga per riga:

PHP - ITERAZIONE CON FETCH_ASSOC
<?php if ($result->num_rows > 0) { while ($row = $result->fetch_assoc()) { echo "ID: " . $row["id"] . " - Nome: " . $row["nome"] . " - Cognome: " . $row["cognome"] . " - Email: " . $row["email"] . "<br>"; } } else { echo "Nessun risultato"; } ?>
📖 Cosa fa questo codice - Spiegazione riga per riga

$result è un oggetto che contiene i risultati della query.
$result->num_rows restituisce numero di righe ottenute.
Se >0 ci sono dati. Il while itera per ogni riga.
fetch_assoc() restituisce riga successiva come array associativo: chiavi = nomi colonne.
$row["id"] estrae valore colonna id della riga attuale.
Concatenazione con . e <br> per andare a capo.

Codice completo:

PHP - SELEZIONA.PHP
<?php // Se hai file conn.php centralizzato, includilo require 'conn.php';$sql = "SELECT id, nome, cognome, email FROM utenti"; $result = $conn->query($sql);if ($result->num_rows > 0) { while ($row = $result->fetch_assoc()) { echo "ID: " . $row["id"] . " - Nome: " . $row["nome"] . " - Cognome: " . $row["cognome"] . " - Email: " . $row["email"] . "<br>"; } } else { echo "Nessun risultato"; }$conn->close(); ?>

Salviamo come seleziona.php, avviamo Apache e MySQL da XAMPP, apriamo localhost/test/seleziona.php

FLUSSO SELECT
$conn->query()SELECT * FROM utenti $result oggettonum_rows = 10 while fetch_assoc()riga 1, riga 2, riga 3... echo $row["nome"]Stampa HTML per ogni riga

3. SQL Injection: Cos'è e come prevenirla

L'SQL Injection è un attacco che permette a un malintenzionato di manipolare una query SQL attraverso input non validati. Questo può portare all'accesso non autorizzato o alla modifica del database.

PHP - LOGIN VULNERABILE - ESEMPIO
<?php $user = $_POST["utente"]; $pwd = $_POST["password"];// VULNERABILE: concatenazione diretta $sqlcmd = "SELECT * FROM utenti WHERE username='$user' AND password='$pwd'";$result = $conn->query($sqlcmd); ?>

Attaccante inserisce come password: ' OR '1'='1 → query diventa:

SQL - QUERY MANIPOLATA
SELECT * FROM utenti WHERE username='' OR '1'='1' AND password=''-- 1=1 sempre vera → accesso senza credenziali valide!

4. Tecniche di Protezione - 5 livelli

5 LIVELLI DI PROTEZIONE
1. PreparedMigliore ✅ 2. Escapereal_escape_string 3. Validazionectype_alnum 4. Parole vietatestripos blacklist 5. Lunghezzasubstr 0,15

4.1 Prepared Statements (Soluzione migliore)

PHP - PREPARED - SOLUZIONE CONSIGLIATA
<?php $stmt = $conn->prepare( "SELECT * FROM utenti WHERE username = ? AND password = ?" );$stmt->bind_param("ss", $user, $pwd); $stmt->execute(); $result = $stmt->get_result();if ($result->num_rows > 0) { echo "Login ok"; } ?>

4.2 Escape dei caratteri speciali

Trasforma apice ' e doppio apice " in sequenze escape \' e \". PHP e MySQL li trattano come parte della stringa, non come delimitatori.

PHP - MYSQLI_REAL_ESCAPE_STRING
<?php $user = mysqli_real_escape_string($conn, $_POST["utente"]); $pwd = mysqli_real_escape_string($conn, $_POST["password"]);$sql = "SELECT * FROM utenti WHERE username='$user' AND password='$pwd'"; ?>
💬 Nota docente - mysql_ vs mysqli_

Nel testo originale c'è mysql_real_escape_string senza i: è la vecchia estensione rimossa da PHP 7! Ora si usa mysqli_real_escape_string($conn, $string) con la connessione come primo parametro, o meglio ancora $conn->real_escape_string(). Ma ricordate: escape è meglio di niente, ma prepared è meglio di escape. All'esame citate entrambi e dite che prepared è lo standard.

4.3 Validazione dell'input con ctype_alnum

Controlla che ogni carattere sia alfanumerico (lettera o numero).

PHP - VALIDAZIONE CTYPE_ALNUM
<?php if (!ctype_alnum($user) || !ctype_alnum($pwd)) { die("Input non valido - solo lettere e numeri ammessi"); }// Se arriva qui, input è alfanumerico pulito echo "Input valido, procedo"; ?>
📖 Spiegazione riga per riga

ctype_alnum($user) → verifica se $user contiene solo A-Z, a-z, 0-9. true se solo alfanumerici, false se c'è spazio, apice, simbolo.
!ctype_alnum($user) → negazione: se NON è alfanumerico → true → entra nell'if.
|| (OR) → se almeno uno tra user o pwd non è alfanumerico → die().
die() → termina script e mostra messaggio. Blocca attacco con caratteri speciali.

4.4 Controllo di parole chiave pericolose

PHP - BLACKLIST PAROLE PERICOLOSE
<?php $vietate = ["select", "insert", "update", "delete", "drop", "alter", "--", "'"];foreach ($vietate as $word) { if (stripos($user, $word) !== false || stripos($pwd, $word) !== false) { die("Dati non validi - parola vietata rilevata: " . $word); } }// Se arriva qui, nessuna parola vietata trovata echo "Controllo parole chiave superato"; ?>
📖 Come funziona stripos

$vietate array con parole SQL riservate e caratteri -- e ' usati in SQL Injection.
foreach scorre ogni parola vietata.
stripos($user, $word) cerca $word dentro $user, case-insensitive (SELECT = select = SeLeCt). Restituisce posizione (0,5...) se trovata, false se non trovata.
!== false → se trovata (posizione 0 è valida ma in PHP 0==false, quindi serve !==).
Se trovata → die().

⚠️ Limite blacklist

Blacklist da sola è debole: attaccante può usare SEL/**/ECT per bypassare. Usatela solo come livello extra, mai come unica protezione. La vera protezione è prepared statement. Questo esercizio serve a capire come funzionava la sicurezza 15 anni fa.

4.5 Buffer overflow - Limitare la lunghezza dell'input

Attacco con stringhe lunghissime che invadono memoria. Contromisura: troncare ai caratteri previsti.

PHP - SUBSTR PER LIMITARE LUNGHEZZA
<?php // Se per utente e password sono previsti 15 caratteri $user = substr($_POST["utente"], 0, 15); $pwd = substr($_POST["password"], 0, 15);echo "User troncato a 15 char: " . $user; ?>
✅ Conclusione sicurezza

Scrivere codice sicuro è essenziale. Ordine di efficacia: 1. Prepared Statements (obbligatorio) → 2. Validazione alfanumerica → 3. Escape → 4. Controllo lunghezza → 5. Blacklist parole. In produzione usate sempre 1 + 2 + 5 insieme (difesa a strati). Mai solo blacklist o solo substr.

5. Esercizio Completo: griglia utenti di una città scelta dall'utente

Problema: estrarre dalla tabella cittadini del database test l'elenco utenti di una città scelta da menu a tendina. Soluzione efficiente con casella combinata che mostra solo città effettivamente presenti nel DB.

5.1 Creazione tabella cittadini

SQL - CREAZIONE TABELLA CITTADINI
CREATE TABLE IF NOT EXISTS cittadini ( codice INT NOT NULL AUTO_INCREMENT PRIMARY KEY, cognome VARCHAR(50) NOT NULL, nome VARCHAR(50) NOT NULL, indirizzo VARCHAR(100) NOT NULL, citta VARCHAR(50) NOT NULL );-- Esempio popolamento (10 record) INSERT INTO cittadini (cognome, nome, indirizzo, citta) VALUES ('Rossi', 'Mario', 'Via Roma 1', 'Bari'), ('Bianchi', 'Luca', 'Via Milano 10', 'Bari'), ('Verdi', 'Anna', 'Corso Italia 5', 'Milano'), ('Neri', 'Sara', 'Via Napoli 22', 'Roma'), ('Gialli', 'Paolo', 'Via Torino 3', 'Milano'), ('Rosa', 'Giulia', 'Via Firenze 8', 'Bari'), ('Viola', 'Marco', 'Via Palermo 12', 'Roma'), ('Grigio', 'Elena', 'Via Bologna 15', 'Torino'), ('Blu', 'Davide', 'Via Genova 9', 'Milano'), ('Marrone', 'Francesca', 'Via Bari 30', 'Torino');

Problema risolto con due file:

  • Form con <select> generato dinamicamente: legge città presenti nel DB con SELECT DISTINCT citta FROM cittadini ORDER BY citta
  • Pagina che riceve città e mostra tabella con SELECT * FROM cittadini WHERE citta = '$citta'

5.2 Soluzione in 2 file: cercacitta.php + elencocitta.php

File 1: cercacitta.php - Scelta città con select dinamica

PHP - CERCACITTA.PHP - FORM DINAMICO
<!DOCTYPE html> <html> <head> <title>Scelta della città</title> </head> <body> <form action="elencocitta.php" method="post"> <p>Scegli la città nell'elenco</p><select name="citta"> <?php $host = "localhost"; $username = "root"; $password = ""; $db_nome = "test";// Connessione $conn = new mysqli($host, $username, $password, $db_nome);if ($conn->connect_errno) { echo "Impossibile connettersi: " . $conn->connect_error; exit; }// Legge città distinte $tab_nome = "cittadini"; $sql = "SELECT DISTINCT citta FROM $tab_nome ORDER BY citta";$result = $conn->query($sql);while ($row = $result->fetch_assoc()) { // Genera option dinamicamente // \" inserisce virgolette nella stringa echo // \n inserisce a capo nel sorgente HTML generato echo "<option value=\"" . $row['citta'] . "\">" . $row['citta'] . "</option>\n"; }$result->free(); $conn->close(); ?> </select><input type="submit" name="invio" value="Cerca"> </form> </body> </html>
📖 Dettaglio echo con escape

\" → inserisce virgolette dentro stringa echo: "<option value=\"" produce <option value=" nell'HTML finale.
\n → ritorno a capo nel sorgente generato (non visibile nel browser, ma utile se fai Visualizza sorgente).
$row['citta'] usato due volte: come value (valore inviato) e come descrizione visibile.

File 2: elencocitta.php - Mostra elenco utenti per città scelta

PHP - ELENCOCITTA.PHP - GRIGLIA RISULTATI
<!DOCTYPE html> <html> <head> <title>Elenco utenti per città</title> <style> table { border-collapse: collapse; width: 80%; margin: 20px auto; } th, td { border: 1px solid #ccc; padding: 8px 12px; text-align: left; } th { background: #f1f5f9; } </style> </head> <body> <?php // Valore fornito dall'utente via POST $citta = $_POST["citta"]; echo "<h2>Elenco utenti: città = " . htmlspecialchars($citta) . "</h2>\n";$host = "localhost"; $username = "root"; $password = ""; $db_nome = "test";$conn = new mysqli($host, $username, $password, $db_nome);if ($conn->connect_errno) { echo "Impossibile connettersi: " . $conn->connect_error; exit; }$tab_nome = "cittadini";// NOTA SICUREZZA: qui sarebbe meglio prepared! // Per esercizio didattico manteniamo query diretta ma con htmlspecialchars sopra $sql = "SELECT * FROM $tab_nome WHERE citta = '$citta'"; $result = $conn->query($sql); ?><!-- Intestazione tabella --> <table border="1"> <tr> <th>Codice</th> <th>Cognome</th> <th>Nome</th> <th>Indirizzo</th> </tr><?php while ($row = $result->fetch_assoc()) { echo "<tr>\n"; echo " <td>" . $row["codice"] . "</td>\n"; echo " <td>" . $row["cognome"] . "</td>\n"; echo " <td>" . $row["nome"] . "</td>\n"; echo " <td>" . $row["indirizzo"] . "</td>\n"; echo "</tr>\n"; }$result->free(); $conn->close(); ?> </table> </body> </html>
💬 Nota docente - Sicurezza qui manca!

In elencocitta.php ho lasciato WHERE citta = '$citta' per rispettare il tuo testo originale, ma è vulnerabile! Se uno modifica il POST con Burp Suite e invia Bari' OR '1'='1 vede tutti i cittadini. Versione sicura: $stmt = $conn->prepare("SELECT * FROM cittadini WHERE citta = ?"); $stmt->bind_param("s",$citta);. Ve lo faccio fare come esercizio extra: rifate elencocitta.php in versione prepared. Così capite differenza tra codice didattico e codice professionale.

5.3 Soluzione in single-page con PHP_SELF e pulsante Aggiorna

Si può risolvere con una sola pagina PHP usando $_SERVER['PHP_SELF'] che contiene nome script in esecuzione. La pagina viene eseguita due volte: prima volta legge città e popola select, seconda volta esegue query utenti città selezionata. isset() distingue i due caricamenti.

PHP - SINGLE-PAGE - CERCACITTA + ELENCO UNITE
<!DOCTYPE html> <?php $host = "localhost"; $username = "root"; $password = ""; $db_nome = "test"; $tab_nome = "cittadini";$conn = new mysqli($host, $username, $password, $db_nome);if ($conn->connect_errno) { echo "Impossibile connettersi: " . $conn->connect_error; exit; } ?> <html> <head> <title>Elenco utenti per città - Single Page</title> </head> <body> <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post"> <p>Scegli la città nell'elenco</p><select name="citta"> <?php $sql = "SELECT DISTINCT citta FROM $tab_nome ORDER BY citta";$result = $conn->query($sql);while ($row = $result->fetch_assoc()) { echo "<option value=\"" . $row['citta'] . "\">" . $row['citta'] . "</option>\n"; } ?> </select><input type="submit" name="invio" value="Cerca"> <input type="submit" name="aggiorna" value="Aggiorna"> </form><?php // Pulsante Aggiorna: ricarica pagina pulita if (isset($_POST["aggiorna"])) { header("Location: " . $_SERVER['PHP_SELF']); exit; }// Pulsante Cerca: mostra risultati if (isset($_POST["invio"])) { $citta = $_POST["citta"]; echo "<h2>Elenco utenti: città = " . htmlspecialchars($citta) . "</h2>\n";// Query con prepared consigliata (versione sicura) $stmt = $conn->prepare("SELECT * FROM $tab_nome WHERE citta = ?"); $stmt->bind_param("s", $citta); $stmt->execute(); $result = $stmt->get_result(); ?><table border="1"> <tr> <th>Codice</th> <th>Cognome</th> <th>Nome</th> <th>Indirizzo</th> </tr><?php while ($row = $result->fetch_assoc()) { echo "<tr>\n"; echo " <td>" . $row["codice"] . "</td>\n"; echo " <td>" . $row["cognome"] . "</td>\n"; echo " <td>" . $row["nome"] . "</td>\n"; echo " <td>" . $row["indirizzo"] . "</td>\n"; echo "</tr>\n"; } ?> </table><?php $result->free(); $stmt->close(); } // fine isset invio$conn->close(); ?> </body> </html>
📖 PHP_SELF, isset e header() - Spiegazione

$_SERVER['PHP_SELF'] contiene nome script in esecuzione: se file si chiama elenco.php, vale /test/elenco.php. Usato in action, fa postare il form a se stesso.
isset($_POST["invio"]) controlla se utente ha cliccato Cerca. Primo caricamento → $_POST vuoto → isset false → mostra solo form. Dopo click → isset true → esegue query e mostra tabella.
Pulsante Aggiorna: header("Location: ".$_SERVER['PHP_SELF']) reindirizza browser alla stessa pagina con GET pulito, eliminando elenco precedente. Sintassi generale: header("Location: URL") deve essere chiamata prima di qualsiasi output HTML (no echo prima). Per questo in versione ottimizzata ho messo connessione prima di <html> e exit dopo header.
htmlspecialchars() aggiunto per evitare XSS: se città contiene <script>, lo stampa come testo, non come codice.

SINGLE PAGE FLOW
1° Caricamento GETisset($_POST)=falseMostra solo select Utente sceglie cittàClick CercaPOST citta=Bari 2° Caricamento POSTisset($_POST["invio"])=trueMostra tabella + form Click Aggiornaheader(Location: PHP_SELF)Ricarica pulita GET
🚀 Esercizi per casa - Da consegnare

Esercizio 1: Rifai seleziona.php con tabella HTML vera (non solo echo con br) + CSS alternanza righe.
Esercizio 2: Rifai elencocitta.php in versione sicura con prepared statement e htmlspecialchars(). Prova SQL Injection con Bari' OR '1'='1 e verifica che ora non funziona più.
Esercizio 3: Aggiungi paginazione: mostra solo 5 cittadini per pagina con LIMIT e pagina corrente da $_GET['page'].
Esercizio 4: Aggiungi validazione: solo città presenti in SELECT DISTINCT sono ammesse. Se POST contiene altra città, die("Città non valida").

🐙 Laboratori Pratici

Vuoi passare all'azione? Accedi alla repository ufficiale GitHub del corso per scaricare il file del laboratorio pratico

🛠️ Script lezione 03 →

Vuoi consultare la sezione dedicata ai laboratori sul sito? Accedi cliccando il pulsante qui sotto.

📊 Sezione dedicata ai Laboratori

Vuoi consultare la sezione dedicata alle guide sul sito? Accedi cliccando il pulsante qui sotto.

📚 Guida a XAMPP →

🎓 Fine Lezione 3 - Cosa sai fare ora

Hai imparato SELECT con mysqli, fetch_assoc() e num_rows per stampare dati. Conosci 5 livelli di protezione anti-SQL Injection e sai perché prepared batte tutti. Hai realizzato esercizio reale con tabella cittadini, SELECT DISTINCT per popolare select dinamica, doppia soluzione in 2 file e single-page con PHP_SELF, isset e header() redirect. Sai anche aggiungere Aggiorna per pulire risultati e usare htmlspecialchars contro XSS. Prossima lezione: login con sessioni, password_hash() e protezione pagine riservate.