Configuração incorreta de CORS (CWE-942): como o reflexo de Origin vaza dados e como corrigir
Uma API que responde a qualquer Origin com Access-Control-Allow-Origin igual a essa origem e Access-Control-Allow-Credentials: true permite que qualquer site leia suas respostas com os cookies do visitante. Como verificações de sufixo, a origem null e origens refletidas quebram a política de mesma origem, e a lista de permissões com correspondência exata que corrige isso, com um exemplo em Express e cors.
Explicação em Linguagem Simples (ELI5)
Um caixa de banco lê o seu saldo em voz alta para quem você mandar, desde que o mensageiro mostre uma carta dizendo para quem trabalha e a lista do caixa aprove esse nome. A regra de um caixa é 'aprovar qualquer nome que termine com a palavra Silva'. Um desconhecido manda um mensageiro com uma carta de 'Mendes da Silva', o caixa aprova, e seu saldo é lido para alguém que você nunca mandou. O reflexo de Origin é a versão preguiçosa em que o caixa aprova todos os nomes. A correção é uma lista curta e exata de nomes, comparados letra por letra.
Conceitos Centrais e Termos
- Política de mesma origem
- Os navegadores só deixam um script ler respostas da sua própria origem (esquema, host e porta). Os cabeçalhos de resposta CORS são a forma de o servidor relaxar essa regra de propósito para origens nomeadas.
- Access-Control-Allow-Origin com credenciais
- Se a resposta nomeia a origem de quem chama em
Access-Control-Allow-Origine enviaAccess-Control-Allow-Credentials: true, um script nessa origem pode ler respostas de requisições que levaram os cookies do usuário. Os navegadores recusam*junto com credenciais, e por isso servidores mal configurados ecoam a origem. - Reflexo de Origin e verificação de sufixo
- Copiar o cabeçalho
Originpara a resposta confia em todos os sites. Verificações comoendsWith('example.com'), buscas por substring ou expressões regulares sem âncoras e com pontos não escapados também aceitamhttps://attacker-example.com. - A origem null
- Iframes em sandbox, URLs
data:e alguns redirecionamentos enviamOrigin: null. Qualquer atacante consegue produzi-la, então permitirnullé o mesmo que permitir todo mundo. - Vary: Origin
- Quando
Access-Control-Allow-Origindepende da requisição, a resposta precisa levarVary: Originpara que caches compartilhados não entreguem os cabeçalhos CORS de uma origem a outra. O pacotecorso adiciona para origens dinâmicas.
Fluxo de Ataque Passo a Paso
A vítima está logada no site da API
O navegador dela guarda um cookie de sessão de api.example.com que é enviado em requisições entre sites.
Ela abre uma página no domínio do atacante
Um script chama fetch('https://api.example.com/api/account', { credentials: 'include' }). Uma página em https://attacker-example.com que só registra no console o tamanho da resposta ilustra o caso.
A API aprova a origem
A verificação de sufixo aceita attacker-example.com, então a resposta traz essa origem em Access-Control-Allow-Origin junto com Access-Control-Allow-Credentials: true.
O script lê dados privados
O navegador entrega o JSON da conta ao script do atacante, que pode enviá-lo para qualquer lugar. Em 2016 James Kettle encontrou esse padrão em várias corretoras de Bitcoin, onde ele expunha chaves de API e dados de carteiras.
Código-Fonte: Vulnerável vs. Seguro
// server.js: qualquer origem terminada em example.com, mais null, pode ler com cookies
import express from 'express';
import cors from 'cors';
import { accountFor, requireSession } from './account.js';
const app = express();
// true faz o cors ecoar a Origin de quem chama junto com credenciais
app.use(cors({
origin: (origin, callback) => callback(null, !origin || origin === 'null' || origin.endsWith('example.com')),
credentials: true,
}));
// dados privados da conta legíveis pelas origens aprovadas
app.get('/api/account', requireSession, (req, res) => {
res.json(accountFor(req.session.userId));
});
// server.js: duas origens exatas, nada mais
import express from 'express';
import cors from 'cors';
import { accountFor, requireSession } from './account.js';
const app = express();
// origens completas com esquema e host, comparadas exatamente
const ALLOWED_ORIGINS = new Set(['https://app.example.com', 'https://admin.example.com']);
// Origin desconhecida ou ausente não recebe cabeçalhos CORS; o cors adiciona Vary: Origin
app.use(cors({
origin: (origin, callback) => callback(null, origin !== undefined && ALLOWED_ORIGINS.has(origin)),
credentials: true,
methods: ['GET', 'POST'],
maxAge: 600,
}));
// dados pessoais nunca ficam em caches compartilhados
app.get('/api/account', requireSession, (req, res) => {
res.set('Cache-Control', 'no-store');
res.json(accountFor(req.session.userId));
});
Lista de Verificação de Segurança para Engenharia
- Mantenha uma lista de permissões com correspondência exata de origens completas (esquema, host e porta) e compare com uma consulta a um
Set; nunca reflitaOriginnem use verificações de sufixo, substring ou regex sem âncoras. - Nunca permita a origem
nulle nunca permita credenciais para uma origem que você não controla. - Envie
Vary: Originsempre queAccess-Control-Allow-Originfor escolhido por requisição, eCache-Control: no-storeem respostas com dados pessoais. - Defina os cookies de sessão como
SameSite=LaxouStrictpara que requisições entre sites não os levem. - Adicione um teste no CI que envie
Origin: https://attacker.exampleeOrigin: nullpara rotas autenticadas e falhe seAccess-Control-Allow-Originvoltar.