CORS: Entenda como funciona o Cross-Origin Resource Sharing

Cross-Origin Resource Sharing (CORS) é um mecanismo fundamental para permitir que aplicativos web acessem recursos de servidores localizados em origens diferentes daquelas em que estão sendo executados. Veja neste texto, como funciona e como resolver os problemas relacionados.

Computador conectado a vários servidores em um Data Center. À esquerda o texto CORS
CORS - Cross-Origin Resource Sharing

De cara vamos deixar algo bem claro: CORS é um mecanismo de segurança gerenciado pelo navegador, ou seja, client side. Isso já justifica um comentário comum: "funciona no Postman mas não no navegador". Outro ponto importante, as configurações CORS são feitas no servidor que hospeda o recurso que precisa ser acessado. O servidor define quais origens são permitidas acessar o recurso, enviando os cabeçalhos CORS corretos na resposta.

Dito isto, vamos explorar neste texto o que é o CORS e como podemos tratar seus erros.

Introdução ao CORS

Cross-Origin Resource Sharing, ou CORS, é um mecanismo do navegador disparado quando há uma tentativa de acessar recursos de uma origem diferente daquela da página que os está solicitando.

Exemplo

Considere dois endereços hipotéticos:

  1. http://pagina.com: retorna uma página HTML com um pequeno JavaScript que busca dados em http://api.com/dados
  2. http://api.com: API com um único endpoint, /dados, que retorna um JSON
Diagrama ilustrando um navegador. No navegador há um script que faz uma requisição para um outro endereço, ilustrando caso de CORS
Diagrama mostrando um caso de CORS

Quando o usuário abre http://pagina.com no navegador e o pequeno script é interpretado, o navegador faz uma requisição ao endereço http://api.com/dados para obter um JSON. Repare que cada recurso, a página HTML e o JSON, tem uma origem diferente. O fato de serem fornecidos por origens diferentes, faz com que o navegador faça as validações CORS.

CORS: funcionamento básico

O princípio básico é que quem está servindo os dados indique no cabeçalho da resposta quais são as regras, fazendo uma analogia, seria algo como "minha casa, minhas regras" ou "meu recurso, minhas regras".

Cabeçalhos HTTP enviados em resposta a solicitações CORS

Os cabeçalhos que indicam essas regras são:

  • Access-Control-Allow-Origin
    Especifica quais origens têm permissão para acessar o recurso.
  • Access-Control-Allow-Methods
    Especifica quais métodos HTTP são permitidos para acessar o recurso.
  • Access-Control-Allow-Headers
    Especifica quais cabeçalhos HTTP são permitidos na solicitação.
  • Access-Control-Allow-Credentials
    Indica se as credenciais (como cookies e tokens de autenticação) são permitidas na solicitação.

Caso o endereço de destino não informe nenhum dos cabeçalhos acima ou os valores informados impeçam a origem de consumi-los, o navegador pode bloquear a requisição lançando uma mensagem de erro parecida com a seguinte no console do DevTools:

Access to fetch at 'http://api.com/dados' from origin 'http://pagina.com' has been blocked by CORS policy

Diagrama de sequência ilustrando um cenário de CORS
Diagrama de sequência ilustrando um cenário de CORS

Na sequência há 2 exemplos que podem ajudar no entendimento e também progredir um pouco mais no assunto.

💡
Os exemplos utilizam Node.js e o framework Express.

Exemplo 1: simulando um problema de CORS com uma requisição GET

Preparando o ambiente:

# Cria o diretório
mkdir estudo-cors
cd estudo-cors

# Inicia o pacote NPM
npm init -y

# Instala o express
npm i express

Após executar o script acima, criei um arquivo chamado app.js:

const express = require('express');
const app = express();
const port = 80;

app.get('/', (req, res) => {
  res.set('Content-Type', 'text/html');
  res.send(`
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
      </head>
      <body>
        <script>
          async function consultarDados() {
            const url = 'http://api.com/dados';
            const response = await fetch(url);
            const json = await response.json();
    
            // Renderiza o elemento na tela:
            const div = document.createElement('div');
            div.innerText = json.dados;
            document.body.appendChild(div);
          }
    
          consultarDados();
        </script>
      </body>
    </html>
  `);
});

app.get('/dados', (_, res) => {
  res.set('Content-Type', 'application/json');
  res.json({ dados: 'ola' });
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

app.js

O código acima iniciará uma escuta na porta 80, que é padrão para o protocolo HTTP. Dado que apenas uma aplicação está sendo executada na porta 80, para reproduzir o exemplo mencionado, modifiquei o arquivo hosts do meu computador para incluir as seguintes entradas:

 127.0.0.1       pagina.com
 127.0.0.1       api.com

# Local do arquivo hosts:
# MacOS: /etc/hosts
# Windows: C:\Windows\System32\drivers\etc
💡
O arquivo hosts é um arquivo de texto usado pelos sistemas operacionais para mapear nomes de host para endereços IP. Basicamente, ele permite que um computador resolva nomes de host em endereços IP sem precisar consultar um servidor DNS.

Com isto, quando digitar pagina.com ou api.com no meu navegador, o IP correspondente será meu próprio computador, ou seja, http://pagina.com e http://api.com, apontarão para a aplicação Node.js que está em execução na porta 80.

Erro CORS

Para testar, primeiramente executei o código com o comando node app.js e depois abri o endereço http://pagina.com no meu navegador. Conforme esperado e já comentado, pegamos um erro relacionado a CORS:

DevTools de um navegador, mostrando um erro de CORS
Erro de CORS

Resolvendo o problema de CORS

Neste exemplo, o site http://pagina.com precisa acessar dados em http://api.com/dados. Então o dono do recurso, http://api.com, precisa incluir no cabeçalho de resposta quem pode consumir este recurso. Para fazer isto, basta incluir na resposta da requisição o cabeçalho Access-Control-Allow-Origin.

Este cabeçalho pode ser um endereço específico, como http://pagina.com, ou * que significa nenhuma restrição de origem.

Modificando o código:

app.get('/dados', (_, res) => {
  res.set(
    'Access-Control-Allow-Origin',
    'http://pagina.com'
  );

  res.json({ dados: 'ola' });
});

trecho do arquivo app.js

Agora http://pagina.com pode consumir recursos de http://api.com/dados:

DevTools do navegador, mostrando o cabeçalho Access-Control-Allow-Origin com o valor http://pagina.com
Exemplo de um apontamento CORS resolvido

Este foi um exemplo bem simples, porém real. Essa resolução é válida para os cenários que atendem as seguintes regras:

  • Requisições do tipo:
    • GET
    • HEAD
    • POST
  • Além dos cabeçalhos definidos automaticamente pelo navegador do usuário, os únicos cabeçalhos que podem ser definidos manualmente são aqueles listados na especificação Fetch:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (detalhes abaixo)
  • O valor do Content-Type deve ser:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

Caso a requisição atenda as regras acima, a resolução proposta neste exemplo funcionará corretamente. Por exemplo:

POST /dados HTTP/1.1
Content-Length: 40
Host: api.com
Origin: http://pagina.com
content-type: application/x-www-form-urlencoded

username=joaosilva&password=senhasecreta

Caso você busque mais informações em outros sites, este tipo de requisição é citada como "Simple Requests" em alguns textos.

CORS: preflight - solicitação preliminar

As requisições que não atendam às especificações acima são tratadas de forma diferenciada, como por exemplo uma requisição POST enviando um JSON.

Antes de o navegador enviar o JSON em um POST para um outro endereço que não seja o atual, ele faz uma solicitação preliminar para verificar se realmente pode enviar os dados para o destino. Essa requisição preliminar é conhecida como "preflight".

Primeiro o navegador envia uma requisição HTTP utilizando o método OPTIONS. O objetivo principal é realizar uma verificação antecipada para garantir que a requisição real seja segura através da resposta dessa requisição. Se a requisição for bem-sucedida, ou seja, o servidor responde à requisição OPTIONS com cabeçalhos CORS específicos, o navegador prossegue com o envio do JSON através de uma requisição POST.

O diagrama abaixo ilustra esse cenário:

Diagrama de sequência mostrando o funcionamento do CORS preflight
Diagrama de sequência mostrando o funcionamento do CORS preflight

Exemplo 2: problema de CORS envolvendo preflight

Pegando o código do primeiro exemplo, alterei o JavaScript para efetuar um POST de um JSON qualquer. Também adicionei um middleware para fazer o parse do conteúdo application/json:

const express = require('express');
const app = express();
const port = 80;

app.get('/', (req, res) => {
  res.set('Content-Type', 'text/html');
  res.send(`
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
      </head>
      <body>
        <script>
          async function consultarDados() {
            const url = 'http://api.com/dados';

            // Agora enviamos um JSON através
            // de uma requisição POST
            const payload = {
              mensagem: 'olá',
            };

            const response = await fetch(url, {
              method: 'post',
              body: JSON.stringify(payload),
              headers: {
                'content-type': 'application/json',
              },
            });
    
            const json = await response.json();
    
            // Renderiza o elemento na tela:
            const div = document.createElement('div');
            div.innerText = json.dados;
            document.body.appendChild(div);
          }
    
          consultarDados();
        </script>
      </body>
    </html>
  `);
});

// Middleware para tratar o conteúdo
// do tipo application/json
app.use(express.json());

app.post('/dados', (req, res) => {
  res.json({
    dados: 'Dados recebidos: ' + JSON.stringify(req.body),
  });
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

Executando o código, podemos observar que temos um problema de CORS:

DevTools do navegador mostrando um erro de CORS
DevTools do navegador mostrando um erro de CORS

Corrigindo o problema de CORS

Por se tratar de um POST de um JSON, precisamos que quem está fornecendo o endereço (endpoint) para receber os recursos, responda à requisições com o método OPTIONS e POST fornecendo os cabeçalhos com os valores adequados para a origem da requisição. Desta forma, não teremos problemas com CORS.

Para fazer este trabalho podemos recorrer a alguma biblioteca, mas para fins didáticos vamos escrever direto no código de uma forma bem simples para ajudar no entendimento:

function definirCabecalhosCORS(res) {
  res.set(
    'Access-Control-Allow-Origin',
    'http://pagina.com'
  );

  res.set('Access-Control-Allow-Headers', 'content-type');
  res.set('Access-Control-Allow-Methods', 'POST');
}

app.options('/dados', (_, res) => {
  definirCabecalhosCORS(res);
  res.status(204).end();
});

// Middleware para tratar o conteúdo
// do tipo application/json
app.use(express.json());

app.post('/dados', (req, res) => {
  definirCabecalhosCORS(res);
  res.json({
    dados: 'Dados recebidos: ' + JSON.stringify(req.body),
  });
});

trecho do arquivo app.js

Repare na imagem abaixo que a requisição OPTIONS foi respondida com o status code 204 (no content) e posteriormente o POST /dados foi efetuado com sucesso:

DevTools do navegador mostrando que as requisições POST e OPTIONS retornaram os cabeçalhos adequados para CORS
Resultado

Tratando Múltiplas Origens no Access-Control-Allow-Origin - CORS

Em alguns casos, o dono do recurso precisa autorizar mais de uma origem a consumir seus recursos. Isso pode ser necessário, por exemplo, para permitir que uma aplicação web e um aplicativo mobile acessem o mesmo conjunto de dados.

Separar as origens por vírgula no cabeçalho Access-Control-Allow-Origin não é a solução correta, pois isso pode gerar o erro "Multiple CORS header 'Access-Control-Allow-Origin' not allowed". A solução correta é verificar a origem da requisição e incluí-la no cabeçalho de resposta se ela estiver em uma lista de origens permitidas.

Exemplo de implementação em Node.js com Express:

app.get('/dados', (req, res) => {
  const allowedOrigins = [
    'http://pagina.com',
    'http://outro-endereco.com',
  ];

  const origin = req.get('origin');
  if (origin && allowedOrigins.includes(origin)) {
    res.set('Access-Control-Allow-Origin', origin);
  }

  res.set('Content-Type', 'application/json');
  res.json({ dados: 'ola' });
});

Considerações

Em ambos os exemplos, ilustramos como configurar corretamente os cabeçalhos CORS para estabelecer regras sobre quem pode acessar os recursos e como fazê-lo. Por ser algo comum, normalmente os frameworks disponibilizam uma forma de efetuarmos essa configuração. Sugiro buscar na documentação do seu framework mais detalhes sobre este tema.

// https://docs.nestjs.com/security/cors
const app = await NestFactory.create(AppModule);
app.enableCors();
await app.listen(3000);

Exemplo da documentação do NestJS

Em resumo, o CORS funciona através da troca de cabeçalhos HTTP entre o navegador e o servidor. Basicamente o navegador envia um cabeçalho Origin com a origem da solicitação, e o servidor responde com cabeçalhos Access-Control-Allow-Origin e Access-Control-Allow-Methods indicando quais origens e métodos de solicitação são permitidos.

Deste mecanismo podemos destacar alguns pontos:

  • O CORS não garante segurança completa. É crucial implementar outras medidas de segurança, como autenticação e criptografia, para proteger dados confidenciais.
  • O CORS é amplamente suportado por navegadores modernos, mas é importante verificar a compatibilidade com navegadores mais antigos.
  • A configuração do CORS pode ser complexa, exigindo conhecimento técnico e atenção aos detalhes.

Links interessantes: