Tema representativo de entrevista

Entrevista de Frontend: ¿Cómo depuras el preflight de CORS y las solicitudes con credenciales?

FrontendIntermedio
Equipo editorial de Offer.ccPublicado Actualizado

Pregunta

Una página en https://app.example.com usa fetch para enviar un POST JSON a https://api.example.com/profile/preferences con un encabezado X-CSRF-Token y una cookie de inicio de sesión. La llamada funciona en curl pero falla con un error de CORS en el navegador. Explica qué solicitudes envía el navegador, diagnostica la configuración, proporciona una solución segura y describe cómo la verificarías.

La pregunta y cuándo aplica

La página se ejecuta en https://app.example.com, mientras que la API está en https://api.example.com. El frontend envía una preferencia de usuario:

ts
await fetch("https://api.example.com/profile/preferences", {
  method: "POST",
  credentials: "include",
  headers: {
    "Content-Type": "application/json",
    "X-CSRF-Token": csrfToken,
  },
  body: JSON.stringify({ compactMode: true }),
})

Actualmente, el servidor devuelve esta respuesta a OPTIONS:

http
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST

Un POST directo a través de curl tiene éxito, pero la consola del navegador solo reporta un error de origen cruzado (cross-origin). Explica:

  1. por qué los dos subdominios siguen siendo de origen cruzado;
  2. por qué el navegador envía OPTIONS primero y cuándo procederá con el POST;
  3. qué está mal en los encabezados de respuesta actuales y cómo aplicar una solución de privilegio mínimo;
  4. las responsabilidades independientes de CORS, las cookies, la autenticación, la autorización y CSRF;
  5. cómo demostrar la solución en las herramientas de desarrollo y en pruebas automatizadas.

Esto es útil para entrevistas de frontend y full-stack. La prueba no consiste en si alguien puede recitar un conjunto de encabezados. Consiste en si puede razonar hacia atrás a partir de las fases de red del navegador y separar cuatro preguntas: ¿se envió la solicitud?, ¿se adjuntaron las credenciales?, ¿puede JavaScript leer la respuesta? y ¿la aplicación autorizó la operación?

Qué evalúa el entrevistador

En primer lugar, el candidato debe identificar un origen con precisión. Un origen consta de esquema (scheme), host y puerto. Las dos URL usan HTTPS y normalmente son del mismo sitio (same-site), pero sus hosts difieren, por lo que la solicitud es same-site y cross-origin. CORS se basa en el origen; el atributo de cookie SameSite se basa en el sitio. No son intercambiables.

En segundo lugar, el candidato debe comprender qué desencadena el preflight. application/json no es un valor de encabezado de solicitud en la lista segura de CORS (CORS-safelisted), y X-CSRF-Token no es un encabezado de solicitud en la lista segura. Por lo tanto, el navegador envía OPTIONS para preguntar si el método y los encabezados reales están permitidos. Si el preflight falla, el POST no se envía.

En tercer lugar, el candidato debe detectar dos errores directos de configuración. Una respuesta con credenciales no puede usar Access-Control-Allow-Origin: *, y la respuesta de preflight no permite Content-Type ni X-CSRF-Token. Incluir POST en los métodos permitidos no es suficiente.

Por último, el candidato debe preservar el límite de seguridad. CORS controla si el navegador comparte una respuesta de origen cruzado con el script. No reemplaza la autenticación, la autorización ni la protección contra CSRF. El servidor no debe reflejar orígenes arbitrarios solo para silenciar la consola ni permitir todos los métodos y encabezados.

Preguntas para aclarar primero

  • ¿En qué fase de red falla? Ninguna solicitud, solo OPTIONS, o un POST completado cuya respuesta no está

disponible para JavaScript apuntan a fallas distintas.

  • ¿La solicitud debe llevar una cookie? Un token de portador (bearer token) o un recurso público anónimo cambian la política de

credenciales y de origen permitido.

  • ¿Qué orígenes exactos de frontend están permitidos? Producción, staging y desarrollo local necesitan orígenes

explícitos, incluyendo esquema y puerto. "El dominio de la empresa" no es lo suficientemente preciso.

  • ¿Una gateway, middleware de autenticación o redirección intercepta OPTIONS? Un preflight normalmente no tiene

credenciales de usuario, por lo que no se le puede exigir que pase primero por la autenticación de inicio de sesión.

  • ¿Cuáles son los atributos Domain, Path, Secure y SameSite de la cookie? Pasar CORS no garantiza

que el navegador envíe una cookie.

  • ¿Las respuestas reales y de error llevan encabezados CORS? Si las respuestas 401, 403 o 500 pierden el encabezado

de origen permitido, el navegador puede ocultar el estado real detrás de un error genérico de CORS.

  • ¿Hay una CDN o caché compartida? Si una respuesta varía según el Origin, envía Vary: Origin para que la

respuesta de un origen no se reutilice para otro.

Estructura de respuesta en 30 segundos

"Dividiría esto en cuatro filtros: origen, permiso de preflight, transporte de credenciales y uso compartido de la respuesta. Los hosts difieren, por lo que la solicitud es de origen cruzado. El Content-Type JSON y X-CSRF-Token activan un preflight OPTIONS. El navegador envía el POST solo si la respuesta permite el origen exacto, POST y ambos encabezados. Debido a que fetch usa credentials: include, Allow-Origin no puede ser un comodín. Tras una coincidencia exacta en la lista de permitidos, el servidor debe devolver https://app.example.com, Allow-Credentials y Vary: Origin. OPTIONS en sí mismo no depende de la cookie de inicio de sesión; el POST aún necesita autenticación, autorización y validación de CSRF. Verificaría OPTIONS, POST, cookies, encabezados de respuesta y el estado de la aplicación en el panel Network, y luego ejecutaría una matriz de pruebas con orígenes positivos y hostiles."

Análisis detallado paso a paso

Paso uno: modelar la máquina de estados que ejecuta realmente el navegador.

El preflight es aproximadamente:

http
OPTIONS /profile/preferences HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,x-csrf-token

El navegador no pregunta si el frontend y el backend pertenecen a la misma empresa. Pregunta si la respuesta permite explícitamente este Origin, método y conjunto de encabezados. La siguiente evidencia aísla cada fase:

Evidencia de redSignificadoSiguiente comprobación
Ni OPTIONS ni POSTJavaScript no se ejecutó, o una regla previa como CSP bloqueó la URLInspeccionar primero la llamada y la consola
Solo un OPTIONS fallidoEl POST no se envióInspeccionar estado, redirecciones y encabezados allow
OPTIONS tiene éxito; POST no tiene cookieEl permiso CORS pasó; el transporte de credenciales noInspeccionar credentials y los atributos de la cookie
POST devuelve 401/403/500, pero el script solo ve CORSLa respuesta de error carece de encabezados CORS válidosPreservar el estado y agregar los encabezados de origen permitido
POST tiene éxito y el script puede leerloLa ruta de CORS pasóVerificar el estado de la aplicación y negativos de seguridad

Paso dos: derivar el conjunto mínimo de permisos a partir de la solicitud.

Para el único frontend de producción en la pregunta, devuelve:

http
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type, X-CSRF-Token
Vary: Origin

El servidor debe comparar Origin con una lista de permitidos exacta y escribirlo de vuelta solo tras una coincidencia. No copies el Origin proporcionado por el cliente incondicionalmente. Si los entornos de staging y locales también están permitidos, lista cada origen completo en su entorno correspondiente. Una coincidencia de sufijo puede aceptar un dominio atacante similar.

El comodín puede ser adecuado para un recurso público, anónimo y sin credenciales. Este endpoint de preferencias de usuario lleva una cookie y realiza una operación sensible, por lo que necesita un origen específico. El preflight OPTIONS no lleva cookie. Una gateway puede manejarlo antes de la autenticación mientras sigue validando estrictamente Origin, método y encabezados de solicitud.

Paso tres: inspeccionar la solicitud real y las credenciales por separado.

El navegador envía POST solo después de un preflight exitoso. credentials: "include" permite que una solicitud de origen cruzado entre en el procesamiento de credenciales, pero Domain, Path, Secure, SameSite y las políticas de privacidad del navegador todavía deciden si se adjunta una cookie.

Estos dos subdominios HTTPS son normalmente same-site y cross-origin. SameSite puede permitir la cookie mientras que CORS todavía aplica. "CORS pasó" no implica "la cookie fue enviada", y "la cookie fue enviada" no implica "el script puede leer la respuesta". Inspecciona Request Cookies, el estado de la respuesta y los encabezados de respuesta CORS como hechos independientes en el panel Network.

Paso cuatro: separar CORS de la seguridad de la aplicación.

CORS es un protocolo de uso compartido de respuestas del navegador, no un control de acceso del lado del servidor. Curl y los clientes HTTP del lado del servidor no aplican la política de mismo origen (same-origin policy) del navegador. Una llamada exitosa con curl demuestra que la API puede manejar la solicitud, no que la ruta del navegador sea correcta.

El POST aún debe:

  • autenticar al usuario;
  • autorizar a ese usuario a modificar el recurso de destino;
  • validar el token CSRF o usar una defensa de igual solidez;
  • validar la entrada y definir el comportamiento de idempotencia o envío duplicado;
  • devolver encabezados CORS correctos en respuestas 401, 403 y 500 de orígenes permitidos para que el frontend vea el error real.

"La página hostil no puede leer la respuesta" no significa "la solicitud no tuvo ningún efecto secundario". Algunas solicitudes de origen cruzado tipo formulario no requieren preflight y pueden llegar al servidor. Las escrituras sensibles no pueden depender únicamente de CORS para la protección contra CSRF.

Paso cinco: demostrar el límite con pruebas positivas y negativas.

Después de la corrección, verifica al menos:

CasoResultado esperado
El origen permitido envía la solicitud indicadaOPTIONS 204, luego POST; el script lee la respuesta real
El origen permitido omite el token CSRFLa configuración del protocolo determina el preflight; la validación de la aplicación rechaza la escritura
Un origen no permitido envía la misma solicitudNingún encabezado CORS permite ese origen; el preflight no pasa
La solicitud usa un método o encabezado no permitidoEl preflight falla, POST no se envía y el estado no cambia
La cookie de inicio de sesión ha expiradoPOST devuelve un 401 legible y el frontend inicia su flujo de inicio de sesión
El servidor devuelve 500El frontend ve 500 y un error estructurado, no un mensaje genérico de CORS
La solicitud repetida usa la caché de preflightEl comportamiento sigue siendo correcto; los cambios de configuración también se prueban con caché fría

Correlaciona esto con los registros del servidor. Un preflight fallido no debe tener un POST coincidente ni una actualización de preferencias. Los registros pueden registrar Origin, método, nombres de encabezados y motivo de rechazo, pero no valores de cookies ni tokens CSRF.

Ejemplo de una respuesta sólida

"Primero identificaría la fase de red. app.example.com y api.example.com tienen hosts diferentes, por lo que son de origen cruzado aunque sean del mismo sitio (same-site). El POST usa Content-Type JSON y X-CSRF-Token, por lo que el navegador envía primero una solicitud OPTIONS sin cookies. En ella declara el Origin, POST y los dos encabezados no simples. El navegador envía el POST solo si la respuesta de preflight permite los tres.

La configuración actual tiene dos defectos directos. Primero, el frontend establece credentials: include, por lo que la respuesta con credenciales no puede usar un comodín para Access-Control-Allow-Origin. Segundo, la respuesta no incluye Access-Control-Allow-Headers: Content-Type, X-CSRF-Token. Validaría el Origin contra una lista de permitidos exacta en el servidor. Para https://app.example.com, devolvería ese valor específico, Allow-Credentials, POST, ambos encabezados permitidos y Vary: Origin. OPTIONS se puede manejar antes del middleware de autenticación de usuario, pero un origen, método o encabezado no permitido se sigue rechazando.

Después de que el preflight pasa, inspeccionaría Request Cookies en el POST. credentials: include es necesario pero no suficiente; Domain, Path, Secure, SameSite y las políticas del navegador siguen aplicando. El POST debe continuar autenticando, autorizando y validando CSRF porque CORS controla el uso compartido de respuestas, no el control de acceso.

Para la verificación, confirmaría en el panel Network que el POST aparece solo después de un OPTIONS exitoso, y luego inspeccionaría cookies, estado real y encabezados de respuesta. Probaría un origen permitido, un origen no permitido, un método no permitido, un inicio de sesión expirado y una respuesta 500. Los registros del servidor deben demostrar que los preflights fallidos no tienen POST ni cambio de estado. Curl es una línea base de API, no un sustituto de la evidencia de red CORS del navegador."

Errores comunes

  • Tratar same-site como same-origin → Subdominios diferentes siguen invocando CORS → **Compara esquema, host

y puerto.**

  • Asumir que POST en Allow-Methods es suficiente → Los encabezados no simples no están permitidos → **Verifica Origin,

Method y Headers en conjunto.**

  • Usar Allow-Origin: * con credenciales → El navegador se niega a exponer la respuesta → **Devuelve el

origen exacto de la lista de permitidos.**

  • Reflejar cualquier Origin solicitado → Cualquier sitio puede obtener acceso de lectura a respuestas con credenciales → **Haz coincidir

primero con una lista de permitidos exacta.**

  • Autenticar OPTIONS como usuario conectado → El preflight normalmente no tiene credenciales → **Realiza una

comprobación de preflight restringida antes de la autenticación de usuario.**

  • Usar mode: "no-cors" como solución → El frontend recibe una respuesta opaca ilegible → **Corrige el

protocolo CORS del servidor.**

  • Tratar CORS como protección contra CSRF → Una solicitud puede llegar al servidor y crear efectos secundarios → **Mantén verificaciones

de CSRF y de autorización en escrituras sensibles.**

  • Agregar encabezados CORS solo a respuestas 2xx → Un 401 o 500 se disfraza como error de origen cruzado → **Usa

encabezados CORS consistentes en errores para orígenes permitidos.**

  • Probar solo con curl → Curl no aplica la política de mismo origen del navegador → **Verifica con evidencia de red

real del navegador.**

Preguntas de seguimiento

¿Por qué una solicitud puede omitir OPTIONS y aun así fallar por CORS?

Una solicitud que utiliza métodos, encabezados y Content-Type de la lista segura puede enviarse directamente. El navegador aún comprueba los encabezados CORS de la respuesta real. Si no se permite compartir la respuesta, JavaScript no puede leerla a pesar de que el servidor la haya procesado. La evidencia de red debe distinguir entre "enviada pero no compartida" y un preflight fallido.

¿Por qué Access-Control-Allow-Credentials no debería aparecer en la solicitud de preflight?

Es un encabezado de respuesta del servidor que indica que el navegador puede exponer una respuesta real realizada en modo de credenciales. El cliente expresa su intención a través del modo credentials de Fetch; el preflight en sí mismo no contiene credenciales de usuario. La respuesta real también debe cumplir las condiciones de origen específico y Allow-Credentials.

¿Qué problema resuelve Vary: Origin?

Cuando un servidor elige Access-Control-Allow-Origin en función del Origin de la solicitud, una caché compartida debe incluir Origin en su clave de caché. De lo contrario, una respuesta para un origen permitido puede reutilizarse para otro, causando una denegación o divulgación incorrecta.

¿Por qué la cookie sigue faltando después de arreglar CORS?

Verifica credentials: "include" y luego inspecciona Domain, Path, Secure, SameSite, expiración de la cookie y restricciones de privacidad del navegador. El permiso de CORS y el transporte de cookies son filtros adyacentes pero independientes.

¿Cómo se deben admitir múltiples dominios de frontend legítimos?

Mantén una lista de permitidos exacta, valida el Origin completo de la solicitud, escribe de vuelta el valor coincidente y envía Vary: Origin. No devuelvas múltiples valores de Allow-Origin ni uses una expresión regular permisiva, coincidencia de sufijos o reflejo incondicional.

¿Puede el frontend corregir una falla de CORS del servidor por sí mismo?

No. El frontend puede eliminar encabezados no simples innecesarios o seleccionar el modo de credenciales deseado, pero la API, el proxy inverso o la gateway deben devolver los encabezados de respuesta que permitan compartir entre orígenes. El código de producción no debe depender de extensiones del navegador ni de desactivar controles de seguridad.

¿Cuánto tiempo debe almacenarse en caché el preflight?

Elige una duración basada en la frecuencia de cambios de configuración, necesidades de revocación y límites del navegador. Al implementar un cambio de CORS, prueba tanto una caché fría como una ruta ya almacenada en caché. El almacenamiento en caché reduce el tráfico de OPTIONS pero puede prolongar un permiso incorrecto, por lo que no debe ocultar defectos de configuración.

Fuentes públicas

Preguntas relacionadas