● CWE-942 · OWASP A05:2021

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-Origin e envia Access-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 Origin para a resposta confia em todos os sites. Verificações como endsWith('example.com'), buscas por substring ou expressões regulares sem âncoras e com pontos não escapados também aceitam https://attacker-example.com.
A origem null
Iframes em sandbox, URLs data: e alguns redirecionamentos enviam Origin: null. Qualquer atacante consegue produzi-la, então permitir null é o mesmo que permitir todo mundo.
Vary: Origin
Quando Access-Control-Allow-Origin depende da requisição, a resposta precisa levar Vary: Origin para que caches compartilhados não entreguem os cabeçalhos CORS de uma origem a outra. O pacote cors o adiciona para origens dinâmicas.

Fluxo de Ataque Passo a Paso

Passo 1

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.

Passo 2

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.

Passo 3

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.

Passo 4

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

IMPLEMENTAÇÃO VULNERÁVEL
// 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));
});
PATCH SEGURO E ROBUSTO
// 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

Fontes

← Ver o diretório completo de segurança Todos os guias de vulnerabilidades →