O Express.js não inclui um único cabeçalho de segurança por defeito. Quando uma app Node.js responde a um pedido HTTP sem configuração adicional, o browser recebe zero proteções contra XSS, clickjacking ou MIME sniffing. O Helmet.js resolve isso com uma linha de código, instalando 15 middlewares de segurança que transformam completamente o perfil de risco da aplicação. Com mais de 2 milhões de transferências semanais no npm e 9.400 estrelas no GitHub, é o padrão de facto para segurança HTTP em Node.js.
Este guia mostra os 12 passos para configurar o Helmet.js numa app Express, desde a instalação até à auditoria completa dos cabeçalhos em produção. Em menos de 30 minutos, a aplicação passa de zero proteções a um perfil compatível com a OWASP A05:2021 Security Misconfiguration.
O que é o Helmet.js e Por Que Toda App Node.js Precisa Dele
O Helmet.js é uma biblioteca open source que funciona como middleware Express. Cada chamada a app.use(helmet()) regista 15 sub-middlewares independentes, cada um responsável por configurar um cabeçalho HTTP de segurança diferente. Não existe magia: o Helmet simplesmente define cabeçalhos HTTP nas respostas antes de estas chegarem ao browser.
O problema que justifica a sua existência é simples: o Express.js foi desenhado para ser minimalista. A framework não impõe opiniões sobre segurança, o que significa que uma app Express instalada de base não envia Content-Security-Policy, não ativa HSTS, e não instrui o browser a evitar MIME sniffing. Esta configuração padrão está na origem de ataques reais. Segundo a OWASP Top 10 2021, a categoria A05 (Security Misconfiguration) inclui explicitamente a ausência de cabeçalhos de segurança HTTP como vetor de ataque.
A popularidade do Helmet reflete a escala do problema. O npm regista mais de 2 milhões de transferências por semana, o que indica que a maioria dos projetos Express em produção já depende desta biblioteca. A LogRocket descreve o Helmet como o wrapper de 15 sub-middlewares que nenhuma app Express devia dispensar.
O impacto de não usar cabeçalhos de segurança é concreto. Sem Content-Security-Policy, um atacante que consiga injetar um script malicioso na página pode roubar sessões de utilizadores, exfiltrar dados de formulários ou redirecionar para sites de phishing. Sem X-Content-Type-Options: nosniff, o browser pode interpretar um ficheiro de texto como JavaScript executável. Sem X-Frame-Options ou frame-ancestors, a app fica vulnerável a ataques de clickjacking em que a página é carregada dentro de um iframe invisível.
Cabeçalhos que o Helmet.js Configura por Defeito
Antes de escrever uma linha de código, é importante entender exatamente o que o Helmet faz. A tabela seguinte lista os cabeçalhos ativados por defeito com uma chamada simples a helmet():
| Cabeçalho HTTP | Valor por defeito | Protege contra |
|---|---|---|
| Content-Security-Policy | default-src ‘self’; upgrade-insecure-requests | XSS, injeção de recursos externos |
| Strict-Transport-Security | max-age=31536000; includeSubDomains | Downgrade de HTTPS para HTTP |
| X-Content-Type-Options | nosniff | MIME sniffing, execução de conteúdo indevido |
| X-DNS-Prefetch-Control | off | Fugas DNS não autorizadas |
| Origin-Agent-Cluster | ?1 | Side-channel attacks entre origens |
| Referrer-Policy | no-referrer | Fuga de URL completo em cabeçalhos Referer |
| Cross-Origin-Opener-Policy | same-origin | Acesso cross-origin via window.opener |
| Cross-Origin-Resource-Policy | same-origin | Carregamento de recursos por origens externas |
| X-XSS-Protection | 0 (desativado) | N/A – desativado por ser contraproducente em browsers modernos |
O X-XSS-Protection merece atenção especial: o Helmet desativa-o deliberadamente porque versões antigas deste mecanismo nos browsers podiam ser exploradas para introduzir XSS. O CSP moderno substitui-o com muito mais eficácia e granularidade. A diretiva frame-ancestors dentro do CSP substitui também o antigo cabeçalho X-Frame-Options, com a vantagem de suportar múltiplas origens autorizadas.
Pré-requisitos e Versões Recomendadas
Para seguir este tutorial, é necessário ter instalado o seguinte:
| Ferramenta | Versão mínima | Notas |
|---|---|---|
| Node.js | 22.x LTS | Linha ativa com suporte até abril de 2027. O projeto lançou atualização HIGH em junho de 2026 para as linhas 22.x, 24.x e 26.x. |
| npm | 10.x | Incluído com Node.js 22 |
| Express | 5.x | Estável desde 2025, com tratamento nativo de promessas assíncronas |
| Helmet.js | Versão mais recente via npm | Compatível com Express 4.x e 5.x |
| curl | Qualquer versão | Para verificação de cabeçalhos via linha de comandos |
Conhecimento necessário: JavaScript assíncrono com async/await, conceito básico de middleware Express, e familiaridade com HTTP e cabeçalhos de resposta.
Passo 1: Criar o Projeto Node.js
Comece por criar uma diretoria de projeto limpa e inicializar o package.json com as configurações adequadas para um projeto moderno em Node.js:
mkdir api-segura-helmet
cd api-segura-helmet
npm init -y
# Verificar que o Node.js 22.x está ativo
node --version
# Saída esperada: v22.x.x
# Converter para ES Modules (recomendado em 2026)
npm pkg set type="module"
O ficheiro package.json resultante deve ter "type": "module" para usar a sintaxe import nos exemplos seguintes. Se preferir CommonJS, substitua import helmet from 'helmet' por const helmet = require('helmet') em todos os exemplos. Ambas as sintaxes são suportadas.
Passo 2: Instalar o Express e o Helmet.js
Instalar as dependências principais. O Helmet não tem dependências externas, o que simplifica a cadeia de fornecimento de software e reduz a superfície de ataque a zero dependências transitivas:
npm install express helmet
# Verificar as versões instaladas
npm list express helmet
# Instalar nodemon para desenvolvimento
npm install --save-dev nodemon
# Adicionar scripts ao package.json
npm pkg set scripts.dev="nodemon src/index.js"
npm pkg set scripts.start="node src/index.js"
# Criar estrutura de diretorias
mkdir -p src
touch src/index.js src/helmet-config.js src/routes.js
Confirme que o Helmet foi instalado corretamente verificando a sua presença no node_modules e no package.json:
# Verificar que o pacote foi instalado
ls node_modules/helmet
# Verificar a entrada no package.json
cat package.json | grep helmet
Passo 3: Integração Básica com Express
A integração mais simples consiste em registar o Helmet como primeiro middleware da aplicação. Desta forma, todos os pedidos recebem os cabeçalhos de segurança antes de qualquer outra lógica ser executada. A ordem dos middlewares em Express é sequencial e determinista, por isso a posição do Helmet importa:
// src/index.js
import express from 'express';
import helmet from 'helmet';
const app = express();
const PORT = process.env.PORT || 3000;
// Helmet deve ser o PRIMEIRO middleware registado
app.use(helmet());
app.use(express.json());
app.get('/', (req, res) => {
res.json({ status: 'ok', mensagem: 'API segura com Helmet.js' });
});
app.listen(PORT, () => {
console.log(`Servidor a correr na porta ${PORT}`);
});
Inicie o servidor e verifique os cabeçalhos com curl:
node src/index.js &
# Verificar todos os cabeçalhos de resposta
curl -I http://localhost:3000/
A resposta deve incluir os seguintes cabeçalhos, confirmando que o Helmet está ativo:
HTTP/1.1 200 OK
content-security-policy: default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests
cross-origin-opener-policy: same-origin
cross-origin-resource-policy: same-origin
origin-agent-cluster: ?1
referrer-policy: no-referrer
strict-transport-security: max-age=31536000; includeSubDomains
x-content-type-options: nosniff
x-dns-prefetch-control: off
Compare com o que Express envia sem Helmet: apenas X-Powered-By: Express, que além de não proteger nada, revela a tecnologia usada pela aplicação a potenciais atacantes.
Passo 4: Configurar o Content-Security-Policy (CSP)
O Content-Security-Policy é o cabeçalho mais complexo e mais importante que o Helmet configura. Diz ao browser quais origens são autorizadas a carregar scripts, estilos, imagens e outros recursos. Uma CSP bem configurada elimina praticamente todos os vetores de ataque XSS baseados em injeção de scripts externos.
O CSP padrão do Helmet usa default-src 'self', o que bloqueia qualquer recurso externo. Para uma API REST pura que responde apenas em JSON, esta configuração é mais do que suficiente. Para aplicações web com recursos externos (Google Fonts, CDNs, analytics), é necessário personalizar as diretivas:
// src/helmet-config.js
export const helmetConfig = {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
// Adicionar apenas CDNs que a app usa explicitamente
"https://cdn.jsdelivr.net"
],
styleSrc: [
"'self'",
"'unsafe-inline'", // Necessário para muitos frameworks CSS
"https://fonts.googleapis.com"
],
fontSrc: [
"'self'",
"https://fonts.gstatic.com"
],
imgSrc: [
"'self'",
"data:",
"https:" // Permite imagens HTTPS de qualquer origem
],
connectSrc: [
"'self'",
"https://api.exemplo.pt" // API externa autorizada
],
objectSrc: ["'none'"],
frameAncestors: ["'none'"], // Equivalente a X-Frame-Options: DENY
baseUri: ["'self'"],
formAction: ["'self'"],
upgradeInsecureRequests: [],
},
},
};
Aplique a configuração personalizada no ficheiro principal:
// src/index.js (atualizado)
import express from 'express';
import helmet from 'helmet';
import { helmetConfig } from './helmet-config.js';
const app = express();
app.use(helmet(helmetConfig));
// resto da app...
Uma armadilha comum com CSP é o bloqueio de recursos legítimos em produção. Para identificar violações sem bloquear, use o modo de reporte antes de enforçar:
// Modo Report-Only: não bloqueia, apenas reporta violações CSP
app.use(
helmet({
contentSecurityPolicy: {
useDefaults: true,
reportOnly: true, // Muda header para Content-Security-Policy-Report-Only
directives: {
defaultSrc: ["'self'"],
reportTo: "/csp-report",
},
},
})
);
// Endpoint para receber relatórios CSP
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.log('Violação CSP:', JSON.stringify(req.body, null, 2));
res.status(204).end();
});
Passo 5: Configurar o HSTS (Strict-Transport-Security)
O HSTS instrui o browser a usar exclusivamente HTTPS durante um período definido, mesmo que o utilizador tente aceder via HTTP. O Helmet define por defeito max-age=31536000; includeSubDomains, correspondendo a 365 dias. Qualquer tentativa de acesso HTTP durante este período é automaticamente redirecionada para HTTPS pelo browser, sem contactar o servidor.
Para produção com domínio registado na HSTS preload list dos principais browsers (Chrome, Firefox, Edge), adicione a diretiva preload:
app.use(
helmet({
hsts: {
maxAge: 31536000, // 1 ano em segundos
includeSubDomains: true, // Aplica a todos os subdomínios
preload: true, // Para inclusão na HSTS preload list dos browsers
},
})
);
O HSTS só deve ser ativado quando o domínio já serve HTTPS em todas as rotas. Uma app que não tenha TLS configurado não deve ativar HSTS, pois os browsers passarão a rejeitar ligações HTTP durante o período de max-age. Para desativar o HSTS em desenvolvimento local:
app.use(
helmet({
hsts: process.env.NODE_ENV === 'production'
? { maxAge: 31536000, includeSubDomains: true, preload: true }
: false, // Desativado em desenvolvimento sem HTTPS
})
);
Passo 6: Cabeçalhos Cross-Origin e Referrer-Policy
Os cabeçalhos cross-origin controlam como a aplicação interage com recursos de outras origens e como outras origens podem interagir com os recursos da app. O Helmet configura três deles por defeito, cada um com um objetivo específico:
| Cabeçalho | Valor padrão Helmet | Efeito prático |
|---|---|---|
| Cross-Origin-Opener-Policy | same-origin | Isola o contexto de navegação, impede acesso via window.opener de outras origens |
| Cross-Origin-Resource-Policy | same-origin | Impede que páginas de outras origens carreguem recursos desta app (imagens, scripts) |
| Cross-Origin-Embedder-Policy | Desativado | Não ativo por defeito para não quebrar integrações de terceiros |
O Referrer-Policy: no-referrer por defeito impede que o URL completo da página atual seja enviado como referrer em pedidos para outras origens. Para apps que dependem de analytics via referrer, use uma política menos restritiva mas ainda segura:
app.use(
helmet({
referrerPolicy: {
// Envia referrer apenas para a mesma origem
// Para HTTPS externos, envia apenas a origem sem path
policy: 'strict-origin-when-cross-origin',
},
crossOriginResourcePolicy: {
// Permite que recursos sejam carregados por qualquer origem (CDN, etc.)
policy: 'cross-origin',
},
crossOriginOpenerPolicy: {
// Mais permissivo para pop-ups OAuth e autenticação de terceiros
policy: 'same-origin-allow-popups',
},
})
);
Para apps que usam SharedArrayBuffer ou precisam de isolamento de origem completo (necessário para mitigações Spectre), ative o COEP:
app.use(
helmet({
crossOriginEmbedderPolicy: {
policy: 'require-corp', // Requer CORP em todos os recursos carregados
},
})
);
Passo 7: Remover o Cabeçalho X-Powered-By
O Express envia por defeito X-Powered-By: Express em todas as respostas. Este cabeçalho não tem qualquer valor funcional e revela informação sobre a stack tecnológica da aplicação. Um atacante que identifica que o servidor usa Express pode focar os ataques em vulnerabilidades específicas de Express ou das versões Node.js associadas. O Helmet remove-o automaticamente.
Para confirmar que foi removido após instalar o Helmet:
# Sem Helmet - expõe a tecnologia:
# x-powered-by: Express
# Com Helmet - cabeçalho ausente das respostas
curl -I http://localhost:3000/ | grep -i powered
# Saída esperada: (nenhuma linha - cabeçalho removido com sucesso)
# Verificar todos os cabeçalhos de segurança de uma vez
curl -I http://localhost:3000/ | grep -iE "(content-security|strict-transport|x-content|referrer|cross-origin|origin-agent)"
Se o projeto usa o módulo HTTP nativo do Node.js sem Express, defina os cabeçalhos manualmente usando res.setHeader(). Para projetos Express que ainda não usam Helmet, a remoção do X-Powered-By pode ser feita isoladamente:
// Apenas remoção do X-Powered-By, sem o Helmet completo
app.disable('x-powered-by');
Passo 8: Configuração para Produção vs Desenvolvimento
A configuração de segurança em desenvolvimento deve ser funcional mas mais permissiva do que em produção. O padrão recomendado é criar um ficheiro de configuração único que lê a variável de ambiente NODE_ENV e adapta as políticas em conformidade:
// src/helmet-config.js - versão completa produção vs desenvolvimento
const isProd = process.env.NODE_ENV === 'production';
export const helmetConfig = {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: isProd
? ["'self'"]
: ["'self'", "'unsafe-eval'", "'unsafe-inline'"], // Dev: permite eval para hot-reload
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: isProd
? ["'self'", process.env.API_URL].filter(Boolean)
: ["'self'", "ws://localhost:*", "http://localhost:*"], // Dev: WebSocket para HMR
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
...(isProd && { upgradeInsecureRequests: [] }),
},
},
hsts: isProd
? { maxAge: 31536000, includeSubDomains: true, preload: true }
: false,
dnsPrefetchControl: { allow: false },
referrerPolicy: {
policy: isProd ? 'no-referrer' : 'no-referrer-when-downgrade',
},
};
No ficheiro .env de produção, defina sempre as variáveis necessárias antes de iniciar a aplicação:
# .env.production
NODE_ENV=production
API_URL=https://api.exemplo.pt
PORT=3000
# .env.development
NODE_ENV=development
PORT=3000
Passo 9: Helmet com CORS para APIs REST
O Helmet e o CORS (Cross-Origin Resource Sharing) resolvem problemas diferentes mas complementares. O Helmet protege a app contra ataques baseados em cabeçalhos de resposta. O CORS controla que origens externas podem fazer pedidos à app. Os dois devem ser usados em conjunto, e a ordem de registo dos middlewares é determinante:
npm install cors
// src/index.js - Helmet + CORS para APIs REST
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import { helmetConfig } from './helmet-config.js';
const app = express();
// 1. Helmet primeiro - define cabeçalhos de segurança em todas as respostas
app.use(helmet(helmetConfig));
// 2. CORS a seguir - define origens permitidas para pedidos cross-origin
const corsOptions = {
origin: process.env.NODE_ENV === 'production'
? (process.env.ALLOWED_ORIGINS || '').split(',').filter(Boolean)
: ['http://localhost:3000', 'http://localhost:5173'], // Vite dev server
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // Permite cookies cross-origin
maxAge: 600, // Cache do preflight OPTIONS por 10 minutos
};
app.options('*', cors(corsOptions)); // Responder a pedidos preflight
app.use(cors(corsOptions));
// 3. Parser do corpo com limite de tamanho
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: false, limit: '10kb' }));
app.get('/api/v1/status', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`API a correr na porta ${PORT}`));
A ordem dos middlewares é fundamental: o Helmet deve vir antes do CORS. Se invertir a ordem, os cabeçalhos CORS podem sobrescrever configurações do Helmet, criando inconsistências na política de segurança.
Passo 10: Helmet com Fastify e NestJS
O Helmet não é exclusivo do Express. Para projetos que usam Fastify ou NestJS, existem integrações específicas que respeitam a arquitetura de plugins de cada framework.
Fastify com @fastify/helmet
No Fastify, o Helmet funciona como plugin e não como middleware Express. O pacote correto é @fastify/helmet, que deve ser registado com await app.register() e não com app.use():
npm install @fastify/helmet
// app-fastify.js
import Fastify from 'fastify';
import helmet from '@fastify/helmet';
const fastify = Fastify({ logger: true });
// Registar como plugin Fastify - NÃO usar app.use() no Fastify
await fastify.register(helmet, {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
objectSrc: ["'none'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
},
});
fastify.get('/', async (request, reply) => {
return { status: 'ok' };
});
await fastify.listen({ port: 3000 });
console.log('Servidor Fastify a correr na porta 3000');
NestJS com Helmet
No NestJS com o adaptador Express por defeito, o Helmet é aplicado como middleware global no ficheiro main.ts, antes de qualquer outro middleware ou módulo:
// main.ts (NestJS com Express)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import helmet from 'helmet';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Aplicar Helmet antes de qualquer outra configuração
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
},
referrerPolicy: { policy: 'no-referrer' },
}));
await app.listen(3000);
console.log('NestJS API com Helmet a correr na porta 3000');
}
bootstrap();
Passo 11: Testar os Cabeçalhos de Segurança
Após configurar o Helmet, é essencial verificar que os cabeçalhos estão a ser enviados corretamente antes de publicar em produção. Existem várias abordagens, desde testes manuais com curl até suites de testes automáticos integrados no pipeline de CI/CD:
# Listar todos os cabeçalhos de resposta ordenados
curl -I -s http://localhost:3000/ | sort
# Verificar CSP especificamente
curl -I -s http://localhost:3000/ | grep -i content-security-policy
# Verificar HSTS
curl -I -s http://localhost:3000/ | grep -i strict-transport
# Verificar que X-Powered-By foi removido (nenhuma saída = correto)
curl -I -s http://localhost:3000/ | grep -i powered
# Verificar todos os cabeçalhos cross-origin
curl -I -s http://localhost:3000/ | grep -i "cross-origin\|origin-agent"
Para testes de integração automáticos com Jest e Supertest:
// tests/security-headers.test.js
import request from 'supertest';
import app from '../src/index.js';
describe('Cabeçalhos de Segurança HTTP', () => {
test('deve incluir Content-Security-Policy com default-src self', async () => {
const res = await request(app).get('/');
expect(res.headers['content-security-policy']).toBeDefined();
expect(res.headers['content-security-policy']).toContain("default-src 'self'");
});
test('deve incluir HSTS em produção', async () => {
const res = await request(app).get('/');
if (process.env.NODE_ENV === 'production') {
expect(res.headers['strict-transport-security']).toContain('max-age=31536000');
expect(res.headers['strict-transport-security']).toContain('includeSubDomains');
}
});
test('deve ter X-Content-Type-Options: nosniff', async () => {
const res = await request(app).get('/');
expect(res.headers['x-content-type-options']).toBe('nosniff');
});
test('não deve expor X-Powered-By', async () => {
const res = await request(app).get('/');
expect(res.headers['x-powered-by']).toBeUndefined();
});
test('deve ter Referrer-Policy configurado', async () => {
const res = await request(app).get('/');
expect(res.headers['referrer-policy']).toBeDefined();
});
test('deve ter Cross-Origin-Opener-Policy', async () => {
const res = await request(app).get('/');
expect(res.headers['cross-origin-opener-policy']).toBe('same-origin');
});
});
Para apps em produção acessíveis via HTTPS, use ferramentas externas de auditoria: o OWASP Secure Headers Project disponibiliza uma lista de referência de cabeçalhos recomendados, e a documentação MDN detalha o comportamento específico de cada cabeçalho como o Content-Security-Policy e o Strict-Transport-Security.
Passo 12: Projeto Completo com API Express Segura
O código seguinte representa uma API Express completa com todas as boas práticas de segurança integradas: Helmet configurado para produção e desenvolvimento, CORS restritivo, limite de tamanho de pedidos, validação de input, e tratamento de erros sem exposição de informação sensível:
// src/index.js - Projeto completo e funcional
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
const app = express();
const isProd = process.env.NODE_ENV === 'production';
// === SEGURANÇA: Helmet com configuração completa ===
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: isProd ? ["'self'"] : ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'none'"],
frameSrc: ["'none'"],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
...(isProd && { upgradeInsecureRequests: [] }),
},
},
hsts: isProd
? { maxAge: 31536000, includeSubDomains: true, preload: true }
: false,
dnsPrefetchControl: { allow: false },
referrerPolicy: { policy: 'no-referrer' },
crossOriginOpenerPolicy: { policy: 'same-origin' },
crossOriginResourcePolicy: { policy: 'same-origin' },
originAgentCluster: true,
}));
// === CORS: Origens autorizadas explicitamente ===
const allowedOrigins = isProd
? (process.env.ALLOWED_ORIGINS || '').split(',').filter(Boolean)
: ['http://localhost:3000', 'http://localhost:5173'];
app.options('*', cors({ origin: allowedOrigins, credentials: true }));
app.use(cors({
origin: allowedOrigins,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 600,
}));
// === CORPO: Limitar tamanho e tipo ===
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: false, limit: '10kb' }));
// === ROTAS ===
app.get('/api/v1/status', (req, res) => {
res.json({
status: 'ok',
ambiente: process.env.NODE_ENV || 'desenvolvimento',
timestamp: new Date().toISOString(),
});
});
app.post('/api/v1/dados', (req, res) => {
const { nome, email } = req.body;
// Validação de input no servidor (nunca confiar no cliente)
if (!nome || typeof nome !== 'string' || nome.length > 100) {
return res.status(400).json({ erro: 'Nome inválido' });
}
if (!email || typeof email !== 'string' || !email.includes('@') || email.length > 254) {
return res.status(400).json({ erro: 'Email inválido' });
}
res.json({ mensagem: 'Dados recebidos com segurança', id: Date.now() });
});
// === TRATAMENTO DE ERROS: Sem exposição de informação interna ===
app.use((req, res) => {
res.status(404).json({ erro: 'Recurso não encontrado' });
});
app.use((err, req, res, next) => {
console.error('Erro interno:', err.message); // Log interno apenas
res.status(500).json({ erro: 'Erro interno do servidor' }); // Nunca expor stack trace
});
const PORT = parseInt(process.env.PORT || '3000', 10);
app.listen(PORT, '0.0.0.0', () => {
console.log(`API segura com Helmet.js a correr na porta ${PORT} [${process.env.NODE_ENV || 'desenvolvimento'}]`);
});
export default app;
Erros Comuns ao Usar o Helmet.js e Como Evitá-los
A maioria dos problemas com Helmet em produção resulta de configuração demasiado restritiva ou de conflitos com outros middlewares. Estes são os erros mais frequentes reportados na comunidade Node.js:
| Erro | Sintoma | Solução |
|---|---|---|
| CSP bloqueia scripts inline | “Refused to execute inline script” no console do browser | Usar nonces por pedido ou mover scripts para ficheiros .js externos |
| HSTS ativo sem HTTPS configurado | Browser recusa permanentemente ligações HTTP ao domínio | Só ativar HSTS depois de TLS configurado e testado |
| Helmet registado depois do CORS | Cabeçalhos inconsistentes ou ausentes em algumas rotas | Sempre colocar app.use(helmet()) como primeiro middleware |
| CSP bloqueia CDN de terceiros | Recursos CSS/JS não carregam, erros 404 em rede | Adicionar explicitamente os domínios CDN ao CSP na diretiva correta |
| COEP ativo com iframes externos | Iframes de pagamento ou mapas não carregam | Desativar crossOriginEmbedderPolicy ou usar política credentialless |
| upgrade-insecure-requests em desenvolvimento | Safari bloqueia recursos HTTP locais | Remover esta diretiva em ambientes sem HTTPS configurado |
| frame-ancestors bloqueia ferramentas de preview | App não carrega em ferramentas como Storybook ou Figma preview | Adicionar origens de ferramentas de desenvolvimento em NODE_ENV=development |
Como Usar Nonces com CSP para Scripts Inline Seguros
Quando scripts inline são indispensáveis, como configuração inicial de uma SPA ou variáveis de ambiente passadas para o cliente, use nonces em vez de 'unsafe-inline'. Um nonce é um valor criptograficamente aleatório gerado por pedido que o browser valida antes de executar o script:
import crypto from 'node:crypto';
// Middleware para gerar nonce único por pedido
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
next();
});
// Helmet configurado com o nonce gerado por pedido
app.use((req, res, next) => {
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", `'nonce-${res.locals.cspNonce}'`],
// 'unsafe-inline' NÃO é necessário com nonces
},
},
})(req, res, next);
});
// No template HTML, usar o nonce em todos os scripts inline:
// <script nonce="<%= locals.cspNonce %>">
// window.__CONFIG__ = { apiUrl: 'https://api.exemplo.pt' };
// </script>
Resolução de Problemas: 8 Situações Frequentes
Os problemas seguintes são os mais reportados em projetos Node.js que integram o Helmet.js em ambiente de produção:
1. Helmet não está a ser aplicado em todas as rotas
Sintoma: algumas rotas retornam os cabeçalhos, outras não. Causa: o Helmet foi registado depois de algumas rotas. Solução: registar app.use(helmet()) antes de qualquer app.get() ou app.use('/rota', router). Em Express, os middlewares são executados pela ordem de registo.
2. Erros ao iniciar com configuração personalizada
Causa: diretivas CSP inválidas ou valores undefined provenientes de variáveis de ambiente não definidas. Solução: validar todas as variáveis de ambiente antes de as passar ao Helmet:
// Validação defensiva de variáveis de ambiente
const API_URL = process.env.API_URL;
if (process.env.NODE_ENV === 'production' && !API_URL) {
throw new Error('API_URL é obrigatória em produção. Verifique o ficheiro .env');
}
3. Imagens de terceiros bloqueadas pelo CSP
Sintoma: imagens de CDNs não carregam. Solução: adicionar o domínio à diretiva imgSrc:
imgSrc: ["'self'", "data:", "https://cdn.exemplo.com", "https://storage.googleapis.com"]
4. Fontes do Google Fonts bloqueadas
Solução: adicionar as origens do Google às diretivas styleSrc e fontSrc separadamente, pois os CSS e as fontes vêm de domínios diferentes:
styleSrc: ["'self'", "https://fonts.googleapis.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"]
5. CORS funciona em desenvolvimento mas não em produção
Causa: a lista de origens CORS em produção lê process.env.ALLOWED_ORIGINS que não foi definido no servidor. Verificação: console.log('Origens:', process.env.ALLOWED_ORIGINS) no arranque da app.
6. Pedidos de preflight OPTIONS a falhar com 404
Sintoma: pedidos PUT ou DELETE falham com erro CORS. Causa: o Express não tem rota para OPTIONS. Solução: adicionar app.options('*', cors(corsOptions)) antes das outras rotas.
7. Cabeçalhos duplicados com proxies reversos
Sintoma: Strict-Transport-Security aparece duas vezes nos cabeçalhos de resposta. Causa: tanto o Nginx como a app Express estão a definir HSTS. Solução: desativar HSTS no Helmet quando o proxy reverso já o configura:
// Quando o Nginx ou Cloudflare já definem HSTS
app.use(helmet({
hsts: false, // Desativar no Node.js para evitar duplicação
}));
8. fetch() a falhar em pedidos com credenciais cross-origin
Sintoma: pedidos com credentials: 'include' falham com erro CORS mesmo com o cabeçalho Access-Control-Allow-Origin presente. Causa: Access-Control-Allow-Origin: * (wildcard) não é compatível com credenciais. Solução: especificar a origem exata no CORS e definir credentials: true tanto no servidor como no cliente.
Dicas Avançadas para Hardening em Produção
Com o Helmet básico configurado, estas são as otimizações que separam uma app segura de uma app verdadeiramente protegida para produção:
Auditoria automática de cabeçalhos no pipeline CI/CD. Adicione um passo no pipeline que verifica os cabeçalhos em staging antes de promover para produção:
#!/bin/bash
# Script de auditoria de cabeçalhos para CI/CD
URL="https://staging.exemplo.pt"
HEADERS=$(curl -Is "$URL")
PASS=0
FAIL=0
check_header() {
local header="$1"
if echo "$HEADERS" | grep -qi "$header"; then
echo "PASS: $header presente"
PASS=$((PASS + 1))
else
echo "FAIL: $header em falta"
FAIL=$((FAIL + 1))
fi
}
check_header "content-security-policy"
check_header "strict-transport-security"
check_header "x-content-type-options"
check_header "referrer-policy"
check_header "cross-origin-opener-policy"
# X-Powered-By não deve estar presente
if echo "$HEADERS" | grep -qi "x-powered-by"; then
echo "FAIL: X-Powered-By está a expor a tecnologia"
FAIL=$((FAIL + 1))
else
echo "PASS: X-Powered-By removido"
PASS=$((PASS + 1))
fi
echo ""
echo "Resultado: $PASS passaram, $FAIL falharam"
[ "$FAIL" -eq 0 ] || exit 1
Helmet com rate limiting por IP para endpoints de autenticação. A proteção de cabeçalhos não dispensa rate limiting em endpoints sensíveis como login ou reset de password. Para uma configuração completa de autenticação segura, consulte o tutorial de Autenticação de Dois Fatores em Node.js e o guia de OAuth 2.0 em Node.js com PKCE.
Monitorização de violações CSP em produção. Configure um endpoint de reporte CSP dedicado que envia alertas quando o browser deteta tentativas de injeção:
// Endpoint para relatórios CSP com alertas
app.post('/api/v1/csp-report',
express.json({ type: ['application/json', 'application/csp-report'] }),
(req, res) => {
const violacao = req.body['csp-report'] || req.body;
// Registar a violação com contexto suficiente para investigação
console.warn('Violação CSP detetada:', {
documento: violacao['document-uri'],
diretiva: violacao['violated-directive'],
recurso: violacao['blocked-uri'],
ip: req.ip,
userAgent: req.get('User-Agent'),
timestamp: new Date().toISOString(),
});
// Integrar com sistema de alertas em produção
// ex: Slack, PagerDuty, Sentry, etc.
res.status(204).end();
}
);
Para complementar a segurança ao nível do transporte, consulte o guia de mTLS em Node.js: TLS 1.3 em 12 Passos que cobre a configuração de TLS mútuo e certificados de cliente. Para criptografia ao nível dos dados, os tutoriais de Assinaturas Digitais em Node.js: ECDSA e Ed25519 mostram como garantir integridade e autenticidade das mensagens. Para referência completa sobre os cabeçalhos, o repositório oficial do Helmet.js no GitHub documenta cada opção de configuração disponível.
Cobertura Relacionada
Para completar a estratégia de segurança da sua app Node.js, consulte:
- OAuth 2.0 em Node.js com PKCE: 12 Passos [2026] – autenticação de terceiros com PKCE e proteção contra CSRF
- Autenticação de Dois Fatores em Node.js: 12 Passos [2026] – TOTP, QR codes e WebAuthn em Express
- mTLS em Node.js: TLS 1.3 em 12 Passos [2026] – certificados de cliente e TLS mútuo para APIs internas
- Assinaturas Digitais em Node.js: ECDSA e Ed25519 em 12 Passos [2026] – verificação de integridade com criptografia assimétrica
- AES-256 vs ChaCha20: Qual Cifra Escolher [2026] – comparação de algoritmos de encriptação simétrica com benchmarks reais
- Segurança Online: Guia Prático – fundamentos de segurança digital para developers
Perguntas Frequentes sobre Helmet.js em Node.js
O Helmet.js substitui um WAF (Web Application Firewall)?
Não. O Helmet define cabeçalhos HTTP que instruem o browser a adotar comportamentos seguros, como não executar scripts não autorizados. Um WAF filtra tráfego malicioso ao nível da rede antes de chegar à aplicação. São complementares: o Helmet protege do lado do browser, o WAF protege ao nível da infraestrutura. Use os dois em produção para defesa em profundidade.
O CSP do Helmet quebra todas as aplicações React, Vue ou Angular?
A configuração padrão com default-src 'self' pode bloquear scripts inline gerados pelo bundler de SPAs. A abordagem recomendada é usar o modo Content-Security-Policy-Report-Only durante uma semana em staging para identificar todas as violações, depois ajustar as diretivas CSP com base nos relatórios recebidos, antes de enforçar em produção.
Preciso de atualizar o Helmet regularmente?
Sim. As boas práticas de cabeçalhos HTTP evoluem à medida que browsers adicionam novas diretivas CSP ou novas políticas de isolamento. Configure o Dependabot ou npm audit no CI/CD para receber alertas automáticos de atualizações. O Node.js lançou em junho de 2026 atualizações de severidade HIGH para as linhas 22.x, 24.x e 26.x, ilustrando a importância de manter dependências atualizadas.
O Helmet tem impacto na performance da app?
O impacto é negligenciável. O Helmet define cabeçalhos HTTP de texto simples, uma operação de microssegundos. Em comparação com o overhead de parsear JSON, validar autenticação ou aceder a bases de dados, o Helmet não é mensurável em benchmarks realistas. A LogRocket confirma que o overhead do Helmet é praticamente zero em aplicações Express em produção.
O Helmet protege contra SQL Injection ou XSS no lado do servidor?
Não diretamente. O Helmet age no nível dos cabeçalhos HTTP de resposta, instruindo o browser a não executar scripts injetados via CSP. A proteção contra SQL Injection e XSS no servidor requer validação e sanitização de input com bibliotecas como zod ou joi, mais o uso de queries parametrizadas para bases de dados. O Helmet é a última linha de defesa no browser, não a primeira linha no servidor.
Posso desativar cabeçalhos específicos sem desativar o Helmet completo?
Sim. Qualquer sub-middleware pode ser desativado passando false na opção correspondente, enquanto os restantes continuam ativos:
app.use(helmet({
dnsPrefetchControl: false, // Desativa X-DNS-Prefetch-Control
originAgentCluster: false, // Desativa Origin-Agent-Cluster
// Todos os outros 13 cabeçalhos continuam ativos
}));
Devo usar Helmet numa API GraphQL ou apenas em APIs REST?
O Helmet é relevante para qualquer servidor HTTP que responda a browsers, independentemente do protocolo da API. Para APIs GraphQL consumidas por browsers, o CSP e o CORS são especialmente importantes porque o GraphQL Playground e outros clientes web estão sujeitos aos mesmos vetores de ataque que qualquer SPA. Para APIs consumidas exclusivamente por servidores em comunicações server-to-server, o HSTS e o CORS têm menos relevância, mas os outros cabeçalhos continuam a valer a pena para defesa em profundidade.
O Helmet.js é compatível com TypeScript?
Sim. O Helmet inclui definições TypeScript nativas no pacote principal, sem necessidade de instalar @types/helmet separadamente. A importação import helmet from 'helmet' funciona diretamente em projetos TypeScript com resolução de módulos moduleResolution: node16 ou bundler.




