Volver al Blog
Plataforma

Limites CORS del navegador: cabeceras, preflight, credenciales y cache

Guia practica para diagnosticar fetch entre origenes sin confundir CORS con red, autorizacion, CSP o Permissions Policy.

BotBrowser Team

Documentación

Quieres la documentación estructurada de Plataforma?

Este artículo forma parte de la biblioteca editorial. Para pasos de configuración, material de referencia y actualizaciones continuas, entra en la sección de docs.

El navegador separa respuestas accesibles, comparticion CORS, credenciales y manejo de la aplicacion

Cross-Origin Resource Sharing (CORS) es un protocolo para compartir respuestas con codigo JavaScript del navegador. Responde a una pregunta limitada: despues de que una pagina de un origen solicita un recurso de otro origen, puede el script leer la respuesta? CORS no hace accesible una ruta, no autoriza al usuario en el servidor y no convierte una API externa en confiable. Un estado HTTP 200 y un error CORS pueden ocurrir a la vez: la red completo el intercambio, pero Fetch oculto la respuesta al script. Registra por separado URL, origen de la pagina, estado HTTP, cabeceras, mensaje de consola y estado visible de la aplicacion. Consulta el [protocolo CORS de Fetch](https://fetch.spec.whatwg.org/#http-cors-protocol) y la [guia CORS de MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) para los detalles normativos. ## Origenes y peticiones simples Un origen es esquema, host y puerto. El fixture local asigna puertos localhost distintos a la pagina y la API durante la ejecucion. Para una respuesta legible, el servidor suele devolver `Access-Control-Allow-Origin: `. El navegador compara ese valor con el origen que envio; el script no puede falsificar libremente la cabecera `Origin`. Mantén una lista de origenes permitidos y no reflejes cualquier origen entrante. Algunas peticiones `GET`, `HEAD` o `POST` con cabeceras y tipos safelisted no necesitan preflight. Aun asi, la respuesta debe tener una politica CORS compatible. `mode: 'no-cors'` produce una respuesta opaca, no una forma de evitar CORS: el cuerpo y la mayoria de cabeceras no son legibles. `mode: 'same-origin'` rechaza el destino cross-origin. ## Preflight y cabeceras de respuesta Una peticion con `Authorization`, `Content-Type: application/json`, un metodo no simple u otras cabeceras no safelisted suele comenzar con `OPTIONS`: ```http Origin: Access-Control-Request-Method: POST Access-Control-Request-Headers: authorization, content-type ``` El servidor debe responder con un metodo, cabeceras y origen compatibles: ```http Access-Control-Allow-Origin: Access-Control-Allow-Methods: POST Access-Control-Allow-Headers: authorization, content-type Access-Control-Max-Age: 300 Vary: Origin ``` El navegador valida el preflight antes de enviar el request real. Un `OPTIONS` redirigido, bloqueado por autenticacion o devuelto con 4xx impide interpretar el endpoint como CORS correcto. `Access-Control-Allow-Methods` no concede permisos de cuenta: la autorizacion del usuario sigue siendo responsabilidad del endpoint. ## Credenciales y cache Para cookies cross-origin, el cliente necesita `credentials: 'include'` y el servidor necesita un origen explicito junto con `Access-Control-Allow-Credentials: true`. El comodin `*` no es valido para una respuesta legible con credenciales. `SameSite`, `Secure`, el dominio de la cookie y la autorizacion del servidor siguen siendo controles independientes; CORS tampoco sustituye la defensa CSRF. Si el servidor elige dinamicamente `Access-Control-Allow-Origin`, incluye `Vary: Origin` para que un CDN no reutilice la respuesta de un origen para otro. Considera cache del navegador, cache de preflight, service workers, proxies y CDN. Para una prueba determinista usa un BrowserContext nuevo y captura las cabeceras reales; no cambies la cache de produccion solo para silenciar una prueba. ## CORS, CSP y Permissions Policy no son lo mismo | Control | Pregunta | Evidencia | No demuestra | | --- | --- | --- | --- | | Red y DNS | Se alcanza la URL? | DNS, TLS, conexion, error de red | Que la respuesta sea legible | | CORS | Puede leerla el JavaScript? | `Access-Control-Allow-*`, resultado Fetch | Autorizacion o negocio completado | | CSP | Que destinos puede usar el documento? | `connect-src`, informes y bloqueos | Permiso CORS o autorizacion | | Permissions Policy | Puede el documento o iframe usar una funcion? | Politica de respuesta y `allow` | Permiso del usuario, dispositivo o CORS | CSP puede bloquear `connect-src` antes de que exista una respuesta CORS; revisa la [documentacion CSP de MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP). Permissions Policy regula funciones del navegador en documentos e iframes, no comparte cuerpos de API; consulta [MDN Permissions Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy). No debilites una politica para ocultar un diagnostico CORS. ## Fixture propio control/candidate Sirve una ruta propia `https://api.example.test/cors-fixture` y dos paginas propias, `https://app.example.test/control` y `https://app.example.test/candidate`. Ambas usan la misma ruta alcanzable y cuerpo sintetico. Solo la respuesta de control contiene `Access-Control-Allow-Origin: `; candidate omite esa cabecera. El boton debe iniciar explicitamente la accion y mostrar un estado visible. ```js import { chromium } from 'playwright'; import http from 'node:http'; import assert from 'node:assert/strict';

let pageOrigin = ''; let apiOrigin = ''; const observations = []; const listen = server => new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); const close = server => new Promise(resolve => server.close(resolve)); const api = http.createServer((request, response) => { const name = request.url.slice(1); if (name === 'control') response.setHeader('Access-Control-Allow-Origin', pageOrigin); observations.push({ name, status: 200, allowOrigin: response.getHeader('Access-Control-Allow-Origin') || null }); response.writeHead(200, { 'Content-Type': 'application/json' }); response.end('{"fixture":true}'); }); const pageServer = http.createServer((request, response) => { const name = request.url.slice(1); response.end(<button>Run CORS check</button><output data-testid="cors-status"></output><script> document.querySelector('button').onclick = async () => { try { await fetch('${apiOrigin}/${name}'); document.querySelector('output').textContent = 'CORS_ALLOWED'; } catch { document.querySelector('output').textContent = 'FETCH_UNCLASSIFIED'; } };</script>); }); await listen(api); await listen(pageServer); const apiPort = api.address().port; const pagePort = pageServer.address().port; pageOrigin = http://127.0.0.1:${pagePort}; apiOrigin = http://127.0.0.1:${apiPort}; const browser = await chromium.launch(); const context = await browser.newContext(); context.setDefaultNavigationTimeout(8_000); async function runCase(name) { const page = await context.newPage(); try { await page.goto(${pageOrigin}/${name}); await page.getByRole('button', { name: 'Run CORS check' }).click(); await page.locator('[data-testid="cors-status"]').waitFor({ state: 'visible', timeout: 5_000 }); return await page.locator('[data-testid="cors-status"]').textContent(); } catch { return 'UNKNOWN'; } finally { await page.close(); } } try { const control = await runCase('control'); const candidate = await runCase('candidate'); assert.equal(control, 'CORS_ALLOWED'); assert.equal(candidate, 'FETCH_UNCLASSIFIED'); assert.deepEqual(observations.map(({ name, allowOrigin }) => ({ name, allowOrigin })), [ { name: 'control', allowOrigin: pageOrigin }, { name: 'candidate', allowOrigin: null } ]); } finally { await context.close(); await browser.close(); await close(pageServer); await close(api); }

Control debe mostrar `CORS_ALLOWED` tras leer el cuerpo sintetico. Candidate captura el rechazo de Fetch y muestra `FETCH_UNCLASSIFIED`: ese estado por si solo no identifica CORS. Solo confirma `CORS_BLOCKED` cuando el registro de la API propia demuestra que candidate llego al endpoint, ambas rutas devolvieron el mismo cuerpo y 200, candidate omitio deliberadamente `Access-Control-Allow-Origin`, y control fue legible con el origen exacto de la pagina. Guarda el registro, las cabeceras candidate y el registro DevTools.

Si navegacion, estado o servidor fallan, el resultado es `UNKNOWN`; no lo atribuyas a red ni CORS. La navegacion tiene limite de ocho segundos y cada page se cierra en cleanup.

## Tabla de decision

| Observacion                       | Responsable inicial | Comprobacion                                     | Conclusion segura                |
| --------------------------------- | ------------------- | ------------------------------------------------ | -------------------------------- |
| DNS, TLS o conexion falla         | Red/plataforma      | Ruta, proxy y salud del servidor                 | No hubo respuesta CORS           |
| Preflight devuelve 4xx/5xx        | API/plataforma      | Ruta `OPTIONS`, auth, metodo y cabeceras         | Fallo el intercambio previo      |
| Hay 200 pero el script no lee     | API/navegador       | Allow-Origin, credenciales y headers expuestos   | Fallo compartir CORS o fue opaca |
| Cookie ausente                    | Auth/navegador      | modo de credenciales, SameSite, Secure y alcance | CORS no habilita cookies         |
| Funciona para un origen y no otro | API/cache           | Allow-list y `Vary: Origin`                      | La politica o cache difiere      |
| Violacion CSP                     | Seguridad web       | `connect-src` y reportes                         | El bloqueo es CSP, no CORS       |

Encaje de BotBrowser
Los BrowserContexts controlados de BotBrowser pueden ejecutar fixtures propios autorizados de control y candidate, aislar cookies y almacenamiento sinteticos y comparar resultados Fetch visibles en un build declarado. La [documentacion de aislamiento multi-cuenta](https://botbrowser.io/docs/identity/multi-account-isolation/) describe ese limite de contexto.

Para validar CORS, BotBrowser puede ejecutar la comparacion control/candidate, pero no puede configurar la respuesta API ni eludir la regla de comparticion del navegador.
BotBrowser no escribe ni despliega cabeceras CORS, no concede autorizacion del servidor, no repara una API de terceros, no cambia el origen y no elude la aplicacion CORS del navegador. Tampoco demuestra que una operacion remota se haya confirmado. La configuracion del servidor, CDN, credenciales, CSP, Permissions Policy y estado de negocio pertenecen a sus respectivos propietarios.
Checklist operativo

1. Declara origen de pagina y API, metodo, cabeceras, credenciales y resultado esperado.
2. Comprueba DNS, TLS, proxy y ruta antes de interpretar un error de politica.
3. Captura Origin, preflight, estado, cabeceras CORS, `Vary` y cache.
4. Separa CORS de CSP, Permissions Policy, permiso de usuario y autorizacion.
5. Usa el fixture propio con navegacion de ocho segundos, estado de cinco y limpieza determinista.
6. Conserva tipos distintos para red, navegacion, timeout, preflight y bloqueo CORS.
   **Autor:** BotBrowser Team
   Consulta la [guia de aislamiento cross-origin](/es/blog/cross-origin-isolation-and-shared-memory-requirements/) y la [guia de Permissions Policy](/es/blog/permissions-policy-for-embedded-browser-features/).

## Sources

- Fixture URL (example only): https://app.example.test`
- Fixture URL (example only): https://app.example.test/control`

- [Protocolo CORS de Fetch](https://fetch.spec.whatwg.org/#http-cors-protocol)
- [CORS en MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
- [CSP en MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
- [Permissions Policy en MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy)
#CORS#Peticiones Cross-Origin#Fetch#Seguridad Web

Lleva BotBrowser de la investigación a producción

Usa estas guías para entender el modelo y después avanzar hacia validación multiplataforma, contextos aislados y despliegue de navegador preparado para escalar.