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 HTTPValor por defeitoProtege contra
Content-Security-Policydefault-src ‘self’; upgrade-insecure-requestsXSS, injeção de recursos externos
Strict-Transport-Securitymax-age=31536000; includeSubDomainsDowngrade de HTTPS para HTTP
X-Content-Type-OptionsnosniffMIME sniffing, execução de conteúdo indevido
X-DNS-Prefetch-ControloffFugas DNS não autorizadas
Origin-Agent-Cluster?1Side-channel attacks entre origens
Referrer-Policyno-referrerFuga de URL completo em cabeçalhos Referer
Cross-Origin-Opener-Policysame-originAcesso cross-origin via window.opener
Cross-Origin-Resource-Policysame-originCarregamento de recursos por origens externas
X-XSS-Protection0 (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:

FerramentaVersão mínimaNotas
Node.js22.x LTSLinha 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.
npm10.xIncluído com Node.js 22
Express5.xEstável desde 2025, com tratamento nativo de promessas assíncronas
Helmet.jsVersão mais recente via npmCompatível com Express 4.x e 5.x
curlQualquer versãoPara 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çalhoValor padrão HelmetEfeito prático
Cross-Origin-Opener-Policysame-originIsola o contexto de navegação, impede acesso via window.opener de outras origens
Cross-Origin-Resource-Policysame-originImpede que páginas de outras origens carreguem recursos desta app (imagens, scripts)
Cross-Origin-Embedder-PolicyDesativadoNã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:

ErroSintomaSolução
CSP bloqueia scripts inline“Refused to execute inline script” no console do browserUsar nonces por pedido ou mover scripts para ficheiros .js externos
HSTS ativo sem HTTPS configuradoBrowser recusa permanentemente ligações HTTP ao domínioSó ativar HSTS depois de TLS configurado e testado
Helmet registado depois do CORSCabeçalhos inconsistentes ou ausentes em algumas rotasSempre colocar app.use(helmet()) como primeiro middleware
CSP bloqueia CDN de terceirosRecursos CSS/JS não carregam, erros 404 em redeAdicionar explicitamente os domínios CDN ao CSP na diretiva correta
COEP ativo com iframes externosIframes de pagamento ou mapas não carregamDesativar crossOriginEmbedderPolicy ou usar política credentialless
upgrade-insecure-requests em desenvolvimentoSafari bloqueia recursos HTTP locaisRemover esta diretiva em ambientes sem HTTPS configurado
frame-ancestors bloqueia ferramentas de previewApp não carrega em ferramentas como Storybook ou Figma previewAdicionar 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:

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.