✍ 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 3Lezione 3SELECTSicurezzaGriglia~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
📖 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.
Salviamo come seleziona.php, avviamo Apache e MySQL da XAMPP, apriamo localhost/test/seleziona.php
FLUSSO SELECT
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!
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
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.
$_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
🚀 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
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.