Configuração básica do Helmet
Adicionar Helmet com configuração de produção:
// 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):
// 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:
// 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çaHSTS e forçar HTTPS no Node.js + Nginx
Redirecionar HTTP para HTTPS e configurar HSTS:
# 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:
// 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/)
})
})