Sicurezza web e API
Leggere il JavaScript con l'IA per trovare endpoint
Il JavaScript del front end elenca l'API con cui parla un'app. Usa l'IA per leggere i bundle e far emergere endpoint e parametri, che poi verifichi.
Il cross-site request forgery usa contro di te i cookie del tuo browser. Le difese in ordine di efficacia e un laboratorio Flask per provare SameSite.
Il cross-site request forgery (CSRF) permette a una pagina di un sito di far inviare al tuo browser una richiesta che cambia lo stato su un altro sito dove hai fatto l'accesso. Il browser allega da solo il cookie di sessione, quindi l'altro sito considera la richiesta come tua. Le difese sono un segreto per sessione che l'attaccante non può leggere, l'attributo SameSite dei cookie, un controllo su Origin e la regola che le richieste GET non cambiano mai lo stato. Questo articolo le spiega una per una, poi passa a un piccolo laboratorio Flask per farti vedere come si muove il cookie.
Una sessione web vive di solito in un cookie. Dopo che accedi a bank.example, il browser salva un cookie per quell'host e lo invia con ogni richiesta verso quell'host. Il server legge il cookie, trova la tua sessione ed esegue l'azione.
Il browser non controlla da dove arriva una richiesta. Se una pagina su evil.example contiene un form che fa POST verso https://bank.example/transfer, il browser invia quel POST con il cookie di bank.example allegato. La banca vede una sessione valida e sposta il denaro. L'attaccante non vede mai il tuo cookie e non legge mai la risposta. All'attacco basta che il server esegua l'azione.
CWE-352 descrive così la debolezza: l'applicazione esegue un'azione che cambia lo stato per conto di un utente senza controllare che l'utente abbia scelto di fare quella richiesta. La correzione consiste nel permettere al server di distinguere una richiesta fatta dall'utente da una forgiata da una pagina di terzi.
Qualsiasi app che tiene le sessioni nei cookie e modifica dati quando riceve una richiesta è esposta. Le azioni più delicate sono quelle che cambiano credenziali o impostazioni: un cambio di password, un cambio di indirizzo email, una nuova chiave API, un nuovo metodo di pagamento, una regola di inoltro. Un cambio di email forgiato è spesso il primo passo per impossessarsi di un account, perché a quel punto l'attaccante può reimpostare la password tramite il nuovo indirizzo.
Le sessioni con cookie sono il comportamento predefinito in molti framework, e il browser invia il cookie che lo sviluppatore ci abbia pensato o no. Per questo il CSRF compare ancora negli elenchi di OWASP e CWE.
bank.example. Il browser salva il cookie di sessione per quell'host.https://bank.example/transfer con valori scelti dall'attaccante. Può partire da solo al caricamento della pagina, quindi la vittima non deve cliccare nulla.bank.example.Nota cosa non è servito all'attaccante. Non ha rubato il cookie, non ha indovinato una password e non ha letto un solo byte della risposta della banca. Il lavoro l'ha fatto il browser.
Una richiesta GET si può innescare senza alcun form. Un tag <img src="https://bank.example/delete?id=7"> fa inviare al browser una richiesta GET appena la pagina viene renderizzata. Anche il prefetch, le anteprime dei link e i crawler possono inviare richieste GET. Se una route GET modifica dati, al CSRF non serve nemmeno un form.
La regola è semplice: GET e HEAD leggono i dati e non li modificano mai. Le modifiche passano da POST, PUT, PATCH o DELETE. Da sola la regola non è una difesa, perché un POST si può comunque forgiare, ma ogni altra difesa funziona meglio quando la regola è rispettata.
Il server crea un token casuale per ogni sessione, lo mette in ogni form come campo nascosto e lo controlla su ogni richiesta che cambia lo stato. Una richiesta senza il token giusto viene rifiutata.
Il token funziona perché la pagina dell'attaccante non può leggerlo. La same-origin policy impedisce a evil.example di leggere l'HTML restituito da bank.example, quindi l'attaccante non può copiare il campo nascosto in un form forgiato. Una richiesta forgiata arriva con il cookie e senza il token.
Il token deve essere imprevedibile (usa un generatore casuale crittografico), legato alla sessione dell'utente e controllato su ogni metodo non sicuro, compresi gli endpoint chiamati dal tuo JavaScript. La maggior parte dei framework include questa protezione. Per questo il cheat sheet di OWASP mette per prime le difese basate su token.
I double-submit cookie sono una variante più debole. Il token va sia in un cookie sia in un campo della richiesta, e il server li confronta. Un attaccante che può impostare cookie sul tuo dominio, per esempio da un sottodominio fratello, può aggirarli. Preferisci il synchronizer token quando puoi tenere lo stato sul server.
SameSite dice al browser quando allegare un cookie a una richiesta che parte da un altro sito. L'attributo ha tre valori.
Secure, quindi il cookie viaggia solo su HTTPS. Usa None solo quando un flusso cross-site reale ha bisogno del cookie, come un widget di terze parti incorporato.MDN documenta l'attributo e i suoi valori nella pagina di riferimento di Set-Cookie. La bozza RFC 6265bis definisce "sito" a partire dal dominio registrabile (il suffisso pubblico più l'etichetta alla sua sinistra, quindi site.example per www.site.example). I numeri di porta non contano. I sottodomini contano come stesso sito. Questo torna utile più avanti nel laboratorio, perché localhost e 127.0.0.1 sono host diversi e quindi siti diversi.
SameSite è una difesa forte, ma incompleta. Un sottodominio fratello come forum.bank.example è same-site rispetto a bank.example, quindi Lax non ferma un attacco lanciato da lì. I browser più vecchi che ignorano l'attributo inviano il cookie ovunque. E Lax invia ancora il cookie nelle navigazioni GET di primo livello, ed è per questo che la regola su GET resta valida. Considera SameSite uno degli strati.
I browser inviano un header Origin nelle richieste POST e nella maggior parte delle richieste cross-site. L'header contiene schema, host e porta della pagina che ha avviato la richiesta, così il server può confrontarlo con la propria origine. Sec-Fetch-Site dà un segnale simile, con valori come same-origin, same-site e cross-site.
Il controllo è semplice. Rifiuta le richieste che cambiano lo stato quando Origin è presente e non corrisponde alla tua origine. Se Origin manca, ricorri a Referer. Se in una richiesta che cambia lo stato mancano entrambi, rifiutala. Interpreta l'header come URL e confronta l'origine completa. Un controllo per sottostringa fallisce: Origin: https://bank.example.evil.test contiene il tuo dominio ma appartiene a qualcun altro.
I controlli su Origin intercettano le richieste chiaramente cross-site, ma dipendono da header che alcuni client omettono e che le impostazioni di privacy possono togliere. Usali come filtro e tieni un token per i casi che gli header non coprono.
Questa è una regola, e non un controllo che puoi verificare con una singola richiesta. Rispettala perché estende la copertura di ogni altra difesa. Con una route che cambia lo stato solo con POST, l'attaccante deve forgiare un form: non gli basta piazzare un tag immagine.
Ti servono Python 3.12 e tre finestre di terminale. Per questo articolo ho usato Python 3.12.10 su Windows 11 con Git Bash. Il laboratorio usa un'app Flask su 127.0.0.1:5000 e una semplice pagina statica su localhost:8001. È la seconda origine a rendere la richiesta cross-site.
Crea un ambiente virtuale e installa Flask. Su Linux e macOS lo attivi con . .venv/bin/activate. Su Windows con Git Bash, lo script di attivazione si trova in .venv/Scripts/activate.
python3 -m venv .venv && . .venv/bin/activate && pip install flaskIn questa esecuzione le versioni installate erano Flask 3.1.3 e Werkzeug 3.1.9.
Salva questo file come app.py. La route /login imposta un cookie di sessione con SameSite=Lax e HttpOnly. La route /transfer non modifica nulla. Stampa solo se il cookie è arrivato, insieme a due header della richiesta che aiutano a leggere il risultato.
import os
from flask import Flask, make_response, request
app = Flask(__name__)
# Change this to "None" to compare. Browsers reject SameSite=None without Secure.
SAMESITE = os.environ.get("LAB_SAMESITE", "Lax")
SECURE = SAMESITE == "None"
@app.get("/login")
def login():
resp = make_response("logged in\n")
resp.set_cookie("session", "lab", samesite=SAMESITE, secure=SECURE, httponly=True)
return resp
@app.post("/transfer")
def transfer():
arrived = request.cookies.get("session") == "lab"
print(
f"POST /transfer cookie_arrived={arrived} "
f"origin={request.headers.get('Origin')} "
f"sec_fetch_site={request.headers.get('Sec-Fetch-Site')}",
flush=True,
)
return f"cookie_arrived={arrived}\n"L'app per ora non convalida nulla, ed è voluto. Vuoi vedere cosa invia il browser prima di aggiungere una difesa.
Nel primo terminale:
flask --app app run --host 127.0.0.1 --port 5000In un secondo terminale, richiedi la route di login e leggi l'header:
curl -i http://127.0.0.1:5000/loginOutput di questa esecuzione, senza la riga Date:
HTTP/1.1 200 OK
Server: Werkzeug/3.1.9 Python/3.12.10
Content-Type: text/html; charset=utf-8
Content-Length: 10
Set-Cookie: session=lab; HttpOnly; Path=/; SameSite=Lax
Connection: close
logged inLa parte che conta è la riga Set-Cookie. HttpOnly tiene il cookie lontano dal JavaScript della pagina. Non impedisce al browser di inviarlo, quindi HttpOnly non è una difesa contro il CSRF. L'attributo che cambia ciò che il browser invia tra siti diversi è SameSite=Lax.
Prima di coinvolgere un browser, invia tu le richieste, così sai cosa registra il server:
curl -s -X POST -d "amount=100" http://127.0.0.1:5000/transfer
curl -s -X POST -H "Origin: http://localhost:8001" -H "Sec-Fetch-Site: cross-site" -b "session=lab" -d "amount=100" http://127.0.0.1:5000/transfer
curl -s -X POST -H "Origin: http://127.0.0.1:5000" -H "Sec-Fetch-Site: same-origin" -b "session=lab" -d "amount=100" http://127.0.0.1:5000/transferRisposte di questa esecuzione, una riga per comando:
cookie_arrived=False
cookie_arrived=True
cookie_arrived=TrueLe righe stampate dal terminale di Flask per quelle tre richieste:
POST /transfer cookie_arrived=False origin=None sec_fetch_site=None
POST /transfer cookie_arrived=True origin=http://localhost:8001 sec_fetch_site=cross-site
POST /transfer cookie_arrived=True origin=http://127.0.0.1:5000 sec_fetch_site=same-origincurl non applica SameSite. Invia quello che gli passi con -b. Quindi queste righe dimostrano che la route legge il cookie e gli header, ma non dicono cosa invia un browser. Quello è il passo successivo.
Crea una cartella chiamata attacker e salva questo file come attacker/index.html:
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Second origin (lab)</title></head>
<body>
<h1>Form on a second origin</h1>
<form method="POST" action="http://127.0.0.1:5000/transfer">
<input type="hidden" name="amount" value="100">
<button type="submit">Send transfer</button>
</form>
</body>
</html>In un terzo terminale, avvia un server statico dalla cartella che contiene attacker/:
python3 -m http.server 8001 --bind 127.0.0.1In questa esecuzione, una richiesta al server statico ha restituito HTTP 200. La pagina poi si carica su http://localhost:8001/attacker/. Fai attenzione al nome host. localhost e 127.0.0.1 sono host diversi, quindi il form servito da localhost:8001 fa POST verso un sito diverso dall'host del cookie. È la configurazione cross-site che ti serve.
Apri prima http://127.0.0.1:5000/login nel browser, così il cookie viene salvato per 127.0.0.1 in quel profilo. Poi apri http://localhost:8001/attacker/ nello stesso browser e clicca Send transfer. Leggi il terminale di Flask.
Con SameSite=Lax, un POST da un form cross-site non dovrebbe portare il cookie. La riga di log attesa è cookie_arrived=False. Il browser omette il cookie perché Lax non lo invia nelle richieste POST cross-site. Il passo nel browser non è stato eseguito per questo articolo, quindi confronta tu la riga di log con questa aspettativa.
Ora riavvia l'app con None:
LAB_SAMESITE=None flask --app app run --host 127.0.0.1 --port 5000Leggi l'header sul nuovo server:
curl -i http://127.0.0.1:5000/loginOutput di questa esecuzione:
Set-Cookie: session=lab; Secure; HttpOnly; Path=/; SameSite=NoneSameSite=None da solo non basta. I browser rifiutano un cookie None senza Secure, e Secure richiede HTTPS. Il laboratorio serve HTTP semplice su 127.0.0.1, e il fatto che un browser salvi o no un cookie Secure da quell'origine dipende dal browser. Apri il cookie negli strumenti per sviluppatori del browser e guarda se esiste. Se non è stato salvato, il log del server mostra cookie_arrived=False, ma il motivo è la regola di Secure e non SameSite. Se è stato salvato, invia di nuovo il form. Una riga cookie_arrived=True con None significa che il browser ha inviato un cookie in un POST cross-site. Questo comportamento rende None rischioso per i cookie di sessione, ed è il motivo per cui conviene usare Lax o Strict, a meno che un flusso cross-site reale non richieda None.
Ferma ogni server con Ctrl+C quando hai finito.
Il laboratorio mostra come i cookie e SameSite cambiano ciò che riceve il server. Non aggiunge una difesa. La route /transfer accetta ancora qualsiasi POST che porti il cookie, quindi un cookie SameSite=None lascerebbe passare un form forgiato. Per chiudere il buco, aggiungi un synchronizer token al form e controllalo nella route. Il laboratorio si ferma qui per farti vedere prima l'attributo da solo.
evil-bank.example e bank.example.evil.test contengono entrambi bank.example. Confronta le origini complete.Leggi il cheat sheet di OWASP dall'inizio alla fine. Copre i pattern dei token, i controlli sugli header e i casi che ognuno lascia scoperti. Leggi la bozza RFC 6265bis per le regole esatte di SameSite, compreso il valore predefinito quando l'attributo manca e il trattamento delle navigazioni di primo livello. La pagina MDN su Set-Cookie è un riferimento più rapido per i valori dell'attributo.
Nella tua app usa la protezione CSRF del framework, e testala. Scrivi un test che, senza token, fa POST su una route che cambia lo stato e si aspetta un rifiuto. Scrivine un secondo che fa POST con un token di un'altra sessione e si aspetta di nuovo un rifiuto. Se usi Flask, guarda l'estensione CSRFProtect di Flask-WTF. Controlla gli avvisi sui cookie negli strumenti per sviluppatori del browser, perché Chrome segnala i problemi di SameSite nel pannello Issues.
Per il livello di trasporto sotto questi cookie, leggi cosa fanno HTTPS e TLS. L'attributo Secure ha senso solo quando la connessione è cifrata. Il prossimo articolo di questa serie, come scrivere il tuo primo report di sicurezza, mostra come documentare un finding come questo per il team responsabile del codice.
Sicurezza web e API
Il JavaScript del front end elenca l'API con cui parla un'app. Usa l'IA per leggere i bundle e far emergere endpoint e parametri, che poi verifichi.
Sicurezza web e API
Metodo per IDOR, XSS, SSRF e auth rotta con un modello di IA come lettore e secondo parere, più come verificare un finding ed evitare i falsi positivi.
Sicurezza web e API
Le API si descrivono tramite le spec OpenAPI e l'introspection GraphQL. Usa l'IA per leggere lo schema e pianificare i test di autorizzazione.