CORS e Cookies HTTPOnly: Proteja seus Tokens de Acesso!

Foto do autor

By luis

últimas postagens

Neste artigo, exploraremos como ativar o CORS (Compartilhamento de Recursos de Origem Cruzada) juntamente com cookies HTTPOnly, visando proteger seus tokens de acesso de forma eficaz.

Atualmente, é comum que servidores de back-end e clientes de front-end residam em domínios distintos. Para permitir a comunicação entre eles em navegadores, torna-se essencial que o servidor habilite o CORS.

Além disso, para otimizar a escalabilidade, muitos servidores adotam a autenticação sem estado. Nesse modelo, os tokens são armazenados e gerenciados no lado do cliente, e não no servidor, como em sessões tradicionais. Para garantir a segurança, a prática recomendada é armazenar esses tokens em cookies HTTPOnly.

Por que as Solicitações de Origem Cruzada São Bloqueadas?

Consideremos um cenário em que nosso aplicativo de front-end está hospedado em https://app.etechpt.com. Um script carregado nesse domínio só pode fazer requisições para recursos dentro da mesma origem.

Qualquer tentativa de enviar uma requisição de origem cruzada para um domínio diferente, como https://api.etechpt.com, ou para uma porta diferente, como https://app.etechpt.com:3000, ou mesmo para um protocolo diferente, como http://app.etechpt.com, resultará no bloqueio da requisição pelo navegador.

Curiosamente, essa mesma requisição, bloqueada pelo navegador, pode ser enviada sem problemas por um servidor backend utilizando comandos curl ou ferramentas como o Postman. Essa diferença existe para proteger os usuários contra ataques como o CSRF (Cross-Site Request Forgery).

Imagine que um usuário esteja logado em sua conta do PayPal no navegador. Sem a proteção do CORS, um script malicioso carregado em um domínio externo (ex: malicious.com) poderia enviar uma requisição para o PayPal como se fosse uma requisição da mesma origem.

A partir daí, invasores poderiam criar páginas maliciosas, como https://malicious.com/transferir-dinheiro-para-conta-do-atacante-da-conta-paypal-do-usuario, e usar encurtadores de URL para ocultar o endereço real. Um usuário desavisado que clicasse nesse link teria o script malicioso enviando uma solicitação para transferir dinheiro da sua conta PayPal para a do atacante, tudo sem o seu conhecimento ou consentimento.

É por esse motivo de segurança que os navegadores bloqueiam as requisições de origem cruzada.

O Que É CORS (Cross-Origin Resource Sharing)?

CORS é um mecanismo de segurança baseado em cabeçalhos que o servidor utiliza para comunicar ao navegador quais domínios são confiáveis para efetuar requisições de origem cruzada. Ao habilitar o CORS, o servidor consegue evitar que as solicitações sejam bloqueadas pelos navegadores.

Como o CORS Funciona?

O servidor define seus domínios confiáveis na configuração do CORS. Quando uma requisição é enviada, a resposta do servidor informa ao navegador se o domínio que fez a requisição é confiável ou não, através dos cabeçalhos.

Existem dois tipos principais de requisições CORS:

  • Requisições Simples
  • Requisições Preflight

Requisição Simples:

  • O navegador envia a requisição para um domínio de origem cruzada (ex: https://app.etechpt.com).
  • O servidor envia a resposta com os métodos permitidos e a origem permitida.
  • O navegador compara o valor do cabeçalho `Origin` enviado (ex: https://app.etechpt.com) com o valor do cabeçalho `Access-Control-Allow-Origin` recebido (ex: https://app.etechpt.com) ou com o curinga (`*`). Se não houver correspondência, um erro CORS é lançado.

Requisição Preflight:

  • Dependendo de parâmetros como métodos HTTP (PUT, DELETE), cabeçalhos personalizados ou tipos de conteúdo diferentes, o navegador pode enviar uma requisição OPTIONS, conhecida como preflight, para verificar se a requisição real pode ser enviada.

Se o servidor responder com um status 204 (No Content) e os parâmetros estiverem permitidos, o navegador enviará a requisição de origem cruzada real.

Se `access-control-allow-origin` for definido como `*`, a resposta é permitida para todas as origens. Mas isso é arriscado, a menos que seja estritamente necessário.

Como Habilitar o CORS?

Para habilitar o CORS para qualquer domínio, configure os cabeçalhos CORS para permitir a origem, métodos, cabeçalhos personalizados e credenciais:

  • O navegador lê os cabeçalhos CORS do servidor e permite as requisições do cliente somente após verificar os parâmetros.
  • `Access-Control-Allow-Origin`: especifica os domínios exatos permitidos (ex: https://app.geekflate.com, https://lab.etechpt.com) ou usa o curinga `*`.
  • `Access-Control-Allow-Methods`: define quais métodos HTTP (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) são permitidos.
  • `Access-Control-Allow-Headers`: permite o envio de cabeçalhos personalizados (ex: Authorization, csrf-token).
  • `Access-Control-Allow-Credentials`: indica se as credenciais (cookies, cabeçalhos de autorização) podem ser enviadas em requisições de origem cruzada.

`Access-Control-Max-Age`: informa ao navegador por quanto tempo ele deve armazenar em cache a resposta preflight.

`Access-Control-Expose-Headers`: Especifica quais cabeçalhos podem ser acessados pelo script do lado do cliente.

Para configurar o CORS nos servidores Apache e Nginx, siga um tutorial específico.

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

app.get('/users', function (req, res, next) {
  res.json({msg: 'user get'})
});

app.post('/users', function (req, res, next) {
    res.json({msg: 'user create'})
});

app.put('/users', function (req, res, next) {
    res.json({msg: 'User update'})
});

app.listen(80, function () {
  console.log('CORS-enabled web server listening on port 80')
})

Habilitando CORS no ExpressJS

Vejamos um exemplo de aplicação ExpressJS sem CORS:

npm install cors

Neste exemplo, habilitamos a API `/users` para os métodos POST, PUT e GET, mas não para o método DELETE.

Para facilitar a ativação do CORS em aplicações ExpressJS, você pode usar o pacote `cors`:

app.use(cors({
    origin: '*'
}));

Acesso-Controle-Permitir-Origem

app.use(cors({
    origin: 'https://app.etechpt.com'
}));

Habilitando CORS para todos os domínios

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ]
}));

Habilitando CORS para um único domínio

Para permitir o CORS para origens como https://app.etechpt.com e https://lab.etechpt.com:

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ],
    methods: ['GET', 'PUT', 'POST']
}));

Métodos de Controle de Acesso-Permissão

Para habilitar o CORS para todos os métodos, basta omitir essa opção no módulo CORS no ExpressJS. Para habilitar métodos específicos, como (GET, POST, PUT):

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ],
    methods: ['GET', 'PUT', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token']
}));

Cabeçalhos de Controle de Acesso-Permitidos

Utilizado para permitir que cabeçalhos não padronizados sejam enviados nas requisições.

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ],
    methods: ['GET', 'PUT', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
    credentials: true
}));

Acesso-Controle-Permitir-Credenciais

Omita essa configuração caso não queira que o navegador envie credenciais em solicitações de origem cruzada, mesmo que `withCredentials` esteja definido como `true`.

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ],
    methods: ['GET', 'PUT', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
    credentials: true,
    maxAge: 600
}));

Acesso-Controle-Max-Idade

Define por quanto tempo as informações de resposta preflight ficarão em cache no navegador. Se você não deseja armazenar a resposta em cache, omita essa configuração.

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ],
    methods: ['GET', 'PUT', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
    credentials: true,
    maxAge: 600,
    exposedHeaders: ['Content-Range', 'X-Content-Range']
}));

A resposta preflight em cache ficará disponível por 10 minutos no navegador.

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ],
    methods: ['GET', 'PUT', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
    credentials: true,
    maxAge: 600,
    exposedHeaders: ['*', 'Authorization', ]
}));

Cabeçalhos de Acesso-Controle-Expostos

Se usarmos o curinga (`*`)

em `exposedHeaders`, o cabeçalho de `Authorization` não será exposto. Portanto, ele precisa ser explicitamente adicionado, como abaixo:

O código acima irá expor todos os cabeçalhos, incluindo o `Authorization`.

  • O que é um Cookie HTTP?
  • Um cookie é um pequeno fragmento de dados que o servidor envia para o navegador do cliente. Em requisições futuras, o navegador enviará todos os cookies relacionados àquele domínio em cada requisição.
  • Os cookies possuem atributos que podem ser definidos para alterar seu comportamento:
  • `Nome`: o nome do cookie.
  • `Valor`: os dados do cookie associados ao seu nome.
  • `Domínio`: o domínio para o qual o cookie será enviado.
  • `Caminho`: cookies só serão enviados para URLs que iniciem com o prefixo de caminho definido (ex: `path=’admin/’`). Cookies não serão enviados para `https://etechpt.com/expire/`, mas serão para `https://etechpt.com/admin/`).
  • `Max-Age/Expires` (número em segundos): quando o cookie irá expirar. A vida útil do cookie torna ele inválido após um tempo específico.
  • `HTTPOnly`(Booleano): Quando verdadeiro, este cookie só pode ser acessado pelo servidor e não pelo script do lado do cliente.
  • Secure(Booleano):Cookies são enviados somente por conexões SSL/TLS (HTTPS) quando verdadeiro.
  • `SameSite` (string: define se os cookies podem ser enviados em solicitações entre sites. Para saber mais detalhes, consulte o MDN. As opções são: `Strict`, `Lax` e `None`. Para o `sameSite` configurado como `None`, o atributo `Secure` do cookie precisa ser `true`.

Por que Usar Cookies HTTPOnly para Tokens?

Armazenar um token de acesso recebido do servidor em locais de armazenamento do lado do cliente (como localStorage, IndexedDB ou cookies sem o atributo HTTPOnly definido como verdadeiro) é mais vulnerável a ataques XSS. Um invasor, caso encontre uma vulnerabilidade XSS em alguma página do seu site, pode fazer uso indevido dos tokens de usuário armazenados no navegador.

Cookies HTTPOnly são definidos/obtidos apenas pelo servidor/backend, não sendo acessíveis no lado do cliente.

  • Scripts do lado do cliente são impedidos de acessar cookies HTTPOnly, tornando-os mais seguros contra ataques XSS. Como somente o servidor pode acessá-los, há uma maior proteção.
  • Habilitando Cookies HTTPOnly em Back-ends com CORS:
  • A configuração abaixo é necessária para habilitar o uso de cookies com o CORS no seu servidor:
  • O cabeçalho `Access-Control-Allow-Credentials` deve ser definido como `true`.

`Access-Control-Allow-Origin` e `Access-Control-Allow-Headers` não devem ser definidos com o curinga `*`.

const express = require('express');
const app = express();
const cors = require('cors');

app.use(cors({
  origin: [
    'https://app.geekflare.com',
    'https://lab.geekflare.com'
  ],
  methods: ['GET', 'PUT', 'POST'],
  allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
  credentials: true,
  maxAge: 600,
  exposedHeaders: ['*', 'Authorization' ]
}));

app.post('/login', function (req, res, next) {
  res.cookie('access_token', access_token, {
    expires: new Date(Date.now() + (3600 * 1000 * 24 * 180 * 1)), // segundos * minutos * horas * dias * ano
    secure: true, // defina como true se estiver usando https ou samesite for none
    httpOnly: true, // apenas back-end
    sameSite: 'none' // defina como none para requisições entre domínios
  });

  res.json({ msg: 'Login realizado com sucesso', access_token });
});

app.listen(80, function () {
  console.log('Servidor web com CORS ativado na porta 80')
});

O atributo `SameSite` do cookie deve ser definido como `None`.

Para usar `sameSite=none`, defina `secure=true`, garantindo que o back-end esteja utilizando um certificado SSL/TLS e rodando em um nome de domínio com HTTPS.

Vejamos um exemplo de código que define um token de acesso em um cookie HTTPOnly após verificar as credenciais de login.

Você pode configurar os cookies CORS e HTTPOnly seguindo as quatro etapas acima em seu back-end e servidor web.

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://api.etechpt.com/user', true);
xhr.withCredentials = true;
xhr.send(null);

Siga um tutorial específico para habilitar o CORS no Apache e Nginx, seguindo as etapas descritas acima.

fetch('http://api.etechpt.com/user', {
  credentials: 'include'
});

`withCredentials` para requisições de origem cruzada

$.ajax({
   url: 'http://api.etechpt.com/user',
   xhrFields: {
      withCredentials: true
   }
});

Credenciais (cookies, autorização) são enviadas com solicitações da mesma origem por padrão. Para solicitações de origem cruzada, é necessário especificar `withCredentials` como `true`.

axios.defaults.withCredentials = true

API XMLHttpRequest

API Fetch

JQuery AjaxAxiosConclusão Espero que este artigo tenha ajudado você a entender o funcionamento do CORS e como habilitá-lo para requisições de origem cruzada no seu servidor. Também discutimos porque armazenar cookies em HTTPOnly é mais seguro, e como `withCredentials` é usado em clientes para requisições de origem cruzada.