Helmet.js: headers de segurança em APIs Node.js

Helmet.js configura automaticamente dezenas de headers HTTP de segurança recomendados pelo OWASP. Em uma linha de código, sua API ativa proteções contra XSS, clickjacking, MIME sniffing e information disclosure. A configuração padrão é segura, mas entender cada header permite ajustar para seu caso de uso.

Configuração básica do Helmet

Adicionar Helmet com configuração de produção:

typescript
// npm install helmet

import helmet from 'helmet'
import { Express } from 'express'

export function configurarSeguranca(app: Express) {
  // Helmet deve ser o PRIMEIRO middleware:
  app.use(helmet({
    // Content-Security-Policy: controla de onde scripts/styles podem ser carregados
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", 'data:', 'https:'],
        connectSrc: ["'self'"],
        fontSrc: ["'self'"],
        objectSrc: ["'none'"],
        mediaSrc: ["'self'"],
        frameSrc: ["'none'"],
        // Para APIs puras (sem HTML): pode desabilitar CSP
        // contentSecurityPolicy: false,
      },
    },

    // HTTP Strict Transport Security: forçar HTTPS por 1 ano:
    hsts: {
      maxAge: 31_536_000,       // 1 ano em segundos
      includeSubDomains: true,
      preload: true,            // incluir na lista de preload do Chrome
    },

    // X-Frame-Options: DENY (previne clickjacking):
    frameguard: { action: 'deny' },

    // X-Content-Type-Options: nosniff (previne MIME sniffing):
    noSniff: true,

    // X-XSS-Protection: desabilitado (browsers modernos usam CSP)
    xssFilter: false,

    // Referrer-Policy: controlar o header Referer:
    referrerPolicy: { policy: 'strict-origin-when-cross-origin' },

    // X-Permitted-Cross-Domain-Policies:
    permittedCrossDomainPolicies: false,

    // Remove X-Powered-By: Express (não revelar tecnologia):
    hidePoweredBy: true,
  }))
}

Content Security Policy para APIs com frontend

CSP ajustado para APIs que servem HTML (docs, admin):

typescript
// Para APIs que servem APENAS JSON: CSP simples ou desabilitado
app.use(helmet({ contentSecurityPolicy: false }))

// Para APIs que servem algum HTML (Swagger UI, painel admin):
// Nonce-based CSP (mais seguro que 'unsafe-inline'):
import crypto from 'crypto'

app.use((req, res, next) => {
  // Gerar nonce único por requisição:
  res.locals.nonce = crypto.randomBytes(16).toString('base64')
  next()
})

app.use((req, res, next) => {
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: [
        "'self'",
        (req, res) => `'nonce-${(res as Response).locals.nonce}'`,
      ],
      styleSrc: [
        "'self'",
        (req, res) => `'nonce-${(res as Response).locals.nonce}'`,
      ],
      // Permitir CDN específica:
      scriptSrc: ["'self'", 'https://cdn.jsdelivr.net'],
      // Conectar para APIs externas:
      connectSrc: ["'self'", 'https://api.seudominio.com.br'],
      imgSrc: ["'self'", 'data:', 'https:'],
      objectSrc: ["'none'"],
      upgradeInsecureRequests: [],
    },
  })(req, res, next)
})

// No template HTML: usar o nonce
// <script nonce="<%= nonce %>">...</script>

// Relatório de violações CSP (monitorar erros em produção):
// contentSecurityPolicy: {
//   directives: {
//     reportUri: '/api/csp-report',
//     ...
//   }
// }

Headers de segurança customizados

Adicionar headers além do Helmet:

typescript
// Headers extras não cobertos pelo Helmet:
app.use((req, res, next) => {
  // Permissions Policy (antes Feature-Policy): controlar APIs do browser:
  res.setHeader('Permissions-Policy',
    'camera=(), microphone=(), geolocation=(), payment=()'
  )

  // Cross-Origin Headers (Isolation — necessário para SharedArrayBuffer):
  res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
  res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
  res.setHeader('Cross-Origin-Resource-Policy', 'same-origin')

  // Cache de segurança para dados sensíveis:
  if (req.path.startsWith('/api/auth') || req.path.startsWith('/api/admin')) {
    res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate')
    res.setHeader('Pragma', 'no-cache')
  }

  next()
})

// Remover headers que revelam stack:
app.use((req, res, next) => {
  res.removeHeader('X-Powered-By')
  res.removeHeader('Server')  // pode precisar configurar no Nginx também
  next()
})

// Verificar headers em produção:
// curl -I https://api.seudominio.com.br/api/health | grep -i "x-|strict|content-security|referrer"

// Ferramentas de teste:
// securityheaders.com — analisa headers da sua API
// observatory.mozilla.org — análise completa de segurança

HSTS e forçar HTTPS no Node.js + Nginx

Redirecionar HTTP para HTTPS e configurar HSTS:

bash
# HSTS (HTTP Strict Transport Security) no Nginx:
# /etc/nginx/sites-enabled/api.conf

# Redirecionar HTTP → HTTPS:
server {
    listen 80;
    server_name api.seudominio.com.br;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name api.seudominio.com.br;

    # Certificado SSL (Let's Encrypt):
    ssl_certificate     /etc/letsencrypt/live/api.seudominio.com.br/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.seudominio.com.br/privkey.pem;

    # TLS moderno:
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;

    # HSTS no Nginx (além do Helmet no Node.js):
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    proxy_pass http://127.0.0.1:3000;
}

# No Node.js: forçar HTTPS também (redundância):
app.use((req, res, next) => {
  if (process.env.NODE_ENV === 'production' && !req.secure) {
    return res.redirect(301, `https://${req.headers.host}${req.url}`)
  }
  next()
})

Auditoria de headers com teste automatizado

Testar headers de segurança nos testes de integração:

typescript
// tests/seguranca-headers.test.ts
import request from 'supertest'
import app from '../src/app'

describe('Headers de segurança', () => {
  let res: Response

  beforeAll(async () => {
    res = await request(app).get('/api/health')
  })

  test('X-Powered-By não deve ser exposto', () => {
    expect(res.headers['x-powered-by']).toBeUndefined()
  })

  test('X-Content-Type-Options: nosniff', () => {
    expect(res.headers['x-content-type-options']).toBe('nosniff')
  })

  test('X-Frame-Options: DENY', () => {
    expect(res.headers['x-frame-options']).toBe('DENY')
  })

  test('Strict-Transport-Security presente', () => {
    expect(res.headers['strict-transport-security']).toMatch(/max-age=\d+/)
  })

  test('Content-Security-Policy presente', () => {
    expect(res.headers['content-security-policy']).toBeDefined()
  })

  test('Referrer-Policy configurado', () => {
    expect(res.headers['referrer-policy']).toBeDefined()
  })

  // Testar que endpoints de auth têm Cache-Control no-store:
  test('Auth endpoints: Cache-Control: no-store', async () => {
    const authRes = await request(app).get('/api/auth/me')
    expect(authRes.headers['cache-control']).toMatch(/no-store/)
  })
})

Não quer configurar manualmente?

Implante o VPS para APIs em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

Recomendação: Helmet na aplicação + HSTS e TLS config no Nginx. Helmet cobre headers específicos da aplicação (CSP, X-Frame-Options, Referrer-Policy) e se mantém atualizado com as melhores práticas. Nginx cobre TLS (certificado SSL, ciphers modernos) e redirecionamento HTTP→HTTPS. Os dois são complementares, Nginx garante a camada de transporte segura, Helmet garante os headers de resposta seguros.

Para APIs puramente JSON sem HTML: CSP não é necessário (não há scripts ou estilos para controlar). Use helmet({ contentSecurityPolicy: false }) e mantenha os outros headers. CSP é importante para páginas HTML onde scripts podem ser injetados. Se a API tem qualquer endpoint que retorna HTML (Swagger UI, emails renderizados, landing pages): configure CSP para esses endpoints.

Clickjacking: atacante embute sua página em um iframe invisível no site dele, posicionado sobre um botão atraente, o usuário acha que clica no botão do atacante mas clica no botão da sua app. X-Frame-Options: DENY impede que sua página seja carregada em iframes. Para APIs: configure DENY. Para páginas que precisam ser embedadas em iframes do mesmo domínio: SAMEORIGIN. Para casos específicos: use CSP frame-ancestors em vez de X-Frame-Options.

Helmet mitiga XSS principalmente via Content Security Policy: a diretiva script-src limita de onde scripts podem ser carregados, então mesmo que um atacante injete <script src="https://evil.com/xss.js">, o browser bloqueia. O header X-XSS-Protection foi depreciado (browsers modernos ignoram), Helmet desabilita por padrão para evitar falsos positivos. A defesa real contra XSS é escapar outputs no código; CSP é a última linha de defesa.

HSTS preload: submeter seu domínio à lista de preload do Chrome (e outros browsers), todos os browsers que têm essa lista forçam HTTPS mesmo antes da primeira visita. Sem preload: na primeira visita HTTP, o browser recebe o redirect 301 e então HSTS. Com preload: sempre HTTPS, sem possibilidade de downgrade na primeira visita. Para adicionar: hsts.preload.org. Cuidado: uma vez na lista, é muito difícil remover, não use se não tiver HTTPS estável em todos os subdomínios.