Security Headers: Protegendo sua Aplicação Web
Quando começamos a trabalhar com segurança web, rapidamente percebemos que o navegador é tanto um aliado quanto um campo de batalha. Os Security Headers são um conjunto de diretivas HTTP que você envia junto com a resposta do servidor, instruindo o navegador sobre como executar o código e interagir com recursos externos. Pense neles como um contrato entre seu servidor e o cliente: você estabelece as regras, e o navegador as segue.
A maioria das vulnerabilidades web exploradas em produção poderiam ser mitigadas ou até eliminadas com a implementação correta desses headers. Eles formam uma barreira defensiva que, mesmo que seu código tenha uma falha (e todo código tem), reduz drasticamente o dano que um atacante pode causar. Vamos explorar os quatro headers mais críticos e entender como implementá-los corretamente.
Content-Security-Policy (CSP): O Guardião do Conteúdo
O Conceito Fundamental
Content-Security-Policy é o header mais poderoso e complexo que você terá em seu arsenal de segurança. Ele funciona através de uma whitelist: você define explicitamente quais domínios e tipos de conteúdo são permitidos ser carregados em sua página. Qualquer recurso que não esteja na whitelist é bloqueado automaticamente pelo navegador.
O grande valor do CSP está na proteção contra Cross-Site Scripting (XSS). Mesmo que um atacante conseguisse injetar um <script> malicioso em sua página, o CSP bloquearia sua execução se o script não viesse de uma fonte autorizada. Isso é especialmente poderoso porque XSS é um dos vetores de ataque mais comuns.
Implementação Prática
Vou começar com uma configuração básica e graduada, que é o caminho correto para implementar CSP em uma aplicação existente:
// Node.js/Express - Fase 1: Monitoramento
const express = require('express');
const app = express();
app.use((req, res, next) => {
// Usar report-only inicialmente para não quebrar nada
res.setHeader(
'Content-Security-Policy-Report-Only',
"default-src 'self'; " +
"script-src 'self' https://cdn.example.com; " +
"style-src 'self' https://fonts.googleapis.com; " +
"img-src 'self' data: https:; " +
"font-src 'self' https://fonts.gstatic.com; " +
"connect-src 'self' https://api.example.com; " +
"report-uri /csp-report"
);
next();
});
// Endpoint para receber relatórios de violações
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.log('CSP Violation:', req.body);
res.status(204).send();
});
app.listen(3000);
Essa é a abordagem correta: começar com Content-Security-Policy-Report-Only. O navegador não bloqueia nada, apenas reporta violações. Você coleta dados, ajusta a política, e depois ativa o header real. Depois de algumas semanas sem violações legítimas, você pode fazer a transição:
// Fase 2: Ativação completa
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' https://cdn.example.com; " +
"style-src 'self' https://fonts.googleapis.com; " +
"img-src 'self' data: https:; " +
"font-src 'self' https://fonts.gstatic.com; " +
"connect-src 'self' https://api.example.com; " +
"frame-ancestors 'none'; " +
"base-uri 'self'; " +
"form-action 'self'"
);
next();
});
Directives Críticas Explicadas
Algumas directives merecem destaque especial porque seu uso incorreto é comum:
-
default-src 'self': Define a política padrão para tudo. Qualquer tipo de conteúdo que não tenha uma diretiva específica usa essa.'self'significa apenas do mesmo domínio. -
script-src 'self' 'unsafe-inline' 'unsafe-eval': Nunca use'unsafe-inline'ou'unsafe-eval'em produção. Esses anulam o CSP para scripts e derrotam seu propósito. -
img-src 'self' data: https:: Permite imagens do seu domínio, URLs data (emojis, pequenas imagens inline), e qualquer HTTPS. -
connect-src 'self' https://api.example.com: Controla para onde XMLHttpRequest, fetch, WebSocket e beacons podem se conectar.
HTTP Strict-Transport-Security (HSTS): Forçando HTTPS
Por Que HSTS é Essencial
Muitos desenvolvedores pensum que redirecionar HTTP para HTTPS é suficiente. Está errado. Se um atacante estiver na mesma rede (mesmo café WiFi), ele pode interceptar a primeira requisição HTTP e fazer o downgrade para HTTP, ou injetar um certificado falso. O HSTS resolve isso dizendo ao navegador: "Este domínio deve sempre usar HTTPS, sem exceções".
Quando um navegador recebe o header HSTS, ele memoriza essa informação por um período determinado. Em requisições futuras para esse domínio, o navegador automaticamente usa HTTPS, mesmo que o usuário digite http:// na barra de endereços. Isso acontece no lado do cliente, antes de enviar qualquer pacote de rede.
Implementação Segura
// Node.js/Express
const express = require('express');
const app = express();
// Implementação básica
app.use((req, res, next) => {
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
next();
});
// IMPORTANTE: Adicionar redirecionamento HTTP → HTTPS
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
res.redirect(301, `https://${req.header('host')}${req.url}`);
} else {
next();
}
});
A configuração do HSTS tem três partes importantes:
max-age=31536000: Tempo em segundos (aqui, 1 ano). O navegador lembrará dessa política por 1 ano.includeSubDomains: Aplica a política também aos subdomínios (ex: api.example.com também será HTTPS).preload: Permite que seu domínio seja incluído na lista de pré-carregamento de HSTS do navegador. Isso significa que navegadores já saberão que seu site usa HTTPS mesmo na primeira visita.
Se você optar pelo preload, você precisa registrar seu domínio em https://hstspreload.org. Mas cuidado: isso é um compromisso sério. Uma vez lá, é difícil remover.
# Python/Flask
from flask import Flask, redirect, request
app = Flask(__name__)
@app.after_request
def set_hsts(response):
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains; preload'
return response
@app.before_request
def enforce_https():
if not request.is_secure and os.environ.get('ENV') == 'production':
return redirect(request.url.replace('http://', 'https://', 1), code=301)
X-Frame-Options: Prevenindo Clickjacking
Entendendo o Ataque
Clickjacking é um ataque onde um site malicioso coloca seu site dentro de um <iframe> transparente ou oculto, e faz o usuário clicar em algo que ele pensa estar clicando em outra coisa. O clássico é um site malicioso com um botão "Clique para ganhar prêmio!", mas na verdade há um iframe invisível de sua aplicação bancária por cima, e o clique transfere dinheiro.
O X-Frame-Options é um header simples mas poderoso que diz: "Você pode (ou não) colocar este site dentro de um iframe". É um complemento imperfeito para o CSP moderno (que tem frame-ancestors), mas continua sendo importante porque navegadores mais antigos não entendem CSP.
Implementação Correta
// Node.js/Express - Prevenção total de clickjacking
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY');
next();
});
// Se você mesmo usa iframes no seu site
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
next();
});
// Exemplo: Um painel admin que pode estar em iframe apenas do seu próprio domínio
app.get('/admin', (req, res) => {
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
res.send('<h1>Painel Admin</h1>');
});
# Python/Flask
from flask import Flask, make_response
app = Flask(__name__)
@app.after_request
def set_x_frame_options(response):
response.headers['X-Frame-Options'] = 'DENY'
return response
@app.route('/admin')
def admin():
response = make_response('<h1>Admin</h1>')
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
return response
As opções disponíveis são simples:
DENY: Seu site nunca pode estar dentro de um iframe, nem mesmo dentro do seu próprio domínio.SAMEORIGIN: Seu site pode estar dentro de um iframe, mas apenas se o iframe está na mesma origem (protocolo + domínio + porta).ALLOW-FROM https://trusted.com: Permite apenas este domínio específico (deprecado, não use).
A recomendação é DENY por padrão, a menos que você realmente precise usar iframes em seu próprio site.
Permissions-Policy: Granularidade Total
O Que É e Por Que Importa
Permissions-Policy (anteriormente chamada Feature-Policy) controla quais APIs de navegador sua página e seus iframes podem acessar. Isso inclui câmera, microfone, localização, pagamentos, acelerômetro, e dezenas de outras. É uma segunda linha de defesa: mesmo que um iframe malicioso ou código injetado consiga rodar, ele não consegue acessar a câmera do usuário sem permissão explícita.
Este header é menos crítico que CSP ou HSTS, mas é extremamente valioso se sua aplicação carrega conteúdo de terceiros, como widgets de publicidade ou comentários de um serviço externo.
Implementação Prática
// Node.js/Express - Configuração completa
app.use((req, res, next) => {
res.setHeader(
'Permissions-Policy',
'accelerometer=(), ' +
'ambient-light-sensor=(), ' +
'autoplay=(), ' +
'battery=(), ' +
'camera=(), ' +
'display-capture=(), ' +
'document-domain=(), ' +
'encrypted-media=(), ' +
'execution-while-not-rendered=(), ' +
'execution-while-out-of-viewport=(), ' +
'fullscreen=(), ' +
'geolocation=(), ' +
'gyroscope=(), ' +
'magnetometer=(), ' +
'microphone=(), ' +
'midi=(), ' +
'navigation-override=(), ' +
'payment=(), ' +
'picture-in-picture=(), ' +
'publickey-credentials-get=(), ' +
'speaker-selection=(), ' +
'sync-xhr=(), ' +
'usb=(), ' +
'vr=(self), ' +
'wake-lock=(), ' +
'xr-spatial-tracking=()'
);
next();
});
Essa é a abordagem "deny-all" — você nega acesso a tudo por padrão. Depois, você seleciona apenas aquilo que sua aplicação realmente precisa:
// Versão realista: Aplicação que precisa de geolocalização
app.use((req, res, next) => {
res.setHeader(
'Permissions-Policy',
'accelerometer=(), ' +
'camera=(), ' +
'geolocation=(self "https://trusted-maps-provider.com"), ' +
'microphone=(), ' +
'payment=(), ' +
'usb=()'
);
next();
});
A sintaxe é: api-name=(allowlist) onde allowlist pode ser:
(): Bloqueado para todos.(self): Permitido apenas para sua origem.(self "https://domain.com"): Permitido para sua origem e um domínio específico.(*): Permitido para todos (não recomendado).
# Python/Flask
from flask import Flask
app = Flask(__name__)
@app.after_request
def set_permissions_policy(response):
response.headers['Permissions-Policy'] = (
'accelerometer=(), '
'camera=(), '
'geolocation=(self), '
'microphone=(), '
'payment=(), '
'usb=()'
)
return response
Integração Completa e Boas Práticas
Implementação em Produção
Aqui está como todos esses headers trabalham juntos em uma aplicação real:
// Node.js/Express - Middleware centralizado para security headers
const securityHeaders = (req, res, next) => {
// HSTS - Força HTTPS
res.setHeader(
'Strict-Transport-Security',
'max-age=63072000; includeSubDomains; preload'
);
// CSP - Controla recursos
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' https://cdn.jsdelivr.net; " +
"style-src 'self' https://fonts.googleapis.com; " +
"font-src 'self' https://fonts.gstatic.com; " +
"img-src 'self' data: https:; " +
"connect-src 'self' https://api.example.com; " +
"frame-ancestors 'none'; " +
"base-uri 'self'; " +
"form-action 'self'; " +
"upgrade-insecure-requests"
);
// X-Frame-Options - Previne clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// Permissions-Policy - Controla APIs do navegador
res.setHeader(
'Permissions-Policy',
'accelerometer=(), camera=(), microphone=(), payment=()'
);
// Headers adicionais de segurança
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
};
const express = require('express');
const app = express();
// Aplicar middleware globalmente
app.use(securityHeaders);
// Seu código de aplicação aqui
app.get('/', (req, res) => {
res.send('<h1>Seguro!</h1>');
});
app.listen(3000);
Testando Sua Implementação
Você pode verificar se seus headers estão sendo enviados corretamente:
# Via curl
curl -I https://seu-dominio.com
# Você deverá ver algo como:
# Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
# Content-Security-Policy: default-src 'self'; ...
# X-Frame-Options: DENY
# Permissions-Policy: accelerometer=(), camera=(), ...
Use ferramentas online como:
- https://securityheaders.com - Analisa seus headers e dá uma nota
- https://csp-evaluator.withgoogle.com - Verifica sua política CSP especificamente
Erros Comuns e Como Evitá-los
O maior erro que vejo é implementar CSP muito restritivo sem monitoramento. Isso quebra funcionalidades legítimas e cria pressão para remover o header. A abordagem correta é: Content-Security-Policy-Report-Only primeiro, depois ativar após validação.
Outro erro frequente: usar max-age muito pequeno no HSTS. Se você escolher 3600 (1 hora) e depois decidir parar de usar HTTPS, você está preso por no mínimo uma hora. Use pelo menos 31536000 (1 ano), mas entenda que é um compromisso sério.
Com X-Frame-Options, não use ALLOW-FROM — é deprecated e não funciona em navegadores modernos. Use CSP's frame-ancestors em vez disso.
Conclusão
Você aprendeu que Security Headers formam uma barreira defensiva em profundidade: CSP bloqueia scripts não autorizados e conteúdo injustado; HSTS força HTTPS desde a primeira visita; X-Frame-Options previne clickjacking; e Permissions-Policy nega acesso a APIs sensíveis por padrão.
A implementação correta segue um padrão: comece monitorando com *-Report-Only, colete dados reais de violações, ajuste conforme necessário, depois ative em modo enforcement. Não é um "copiar e colar" — cada aplicação tem necessidades diferentes, e um CSP muito restritivo pode quebrar funcionalidades legítimas.
O retorno sobre investimento é enorme. Esses headers resolvem categorias inteiras de vulnerabilidades (XSS, clickjacking, data exfiltration) sem custo de performance, e demonstram que você toma segurança seriamente. Isso importa tanto do ponto de vista técnico quanto de conformidade regulatória.