Security headers automatizados: middleware e proxies

1. Por que Automatizar Security Headers?

Manter security headers manualmente em cada rota ou endpoint é uma receita para falhas de segurança. Um estudo do Google em 2023 mostrou que 72% dos sites que implementaram Content Security Policy (CSP) o fizeram de forma incompleta, deixando brechas para XSS. A automação resolve três problemas críticos:

  • Esquecimento humano: desenvolvedores podem pular headers em rotas novas
  • Inconsistência: diferentes equipes aplicam políticas diferentes
  • Manutenção: atualizar um header exige alterar dezenas de arquivos

Os headers essenciais que devem ser automatizados incluem:

Content-Security-Policy: default-src 'self'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=()

2. Implementação via Middleware em Frameworks Web

Express.js com Helmet

O pacote helmet é o padrão ouro para Node.js. Ele configura 15 headers de segurança automaticamente:

npm install helmet
const express = require('express');
const helmet = require('helmet');

const app = express();

// Configuração padrão segura
app.use(helmet());

// Personalização avançada
app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", "https://analytics.example.com"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:", "https://images.example.com"],
      },
    },
    crossOriginEmbedderPolicy: false, // Desabilitar se usar CDN
  })
);

app.get('/', (req, res) => {
  res.send('Headers seguros aplicados');
});

Django com middleware customizado

Django permite criar middlewares que interceptam todas as respostas:

# middleware.py
from django.utils.deprecation import MiddlewareMixin

class SecurityHeadersMiddleware(MiddlewareMixin):
    def process_response(self, request, response):
        response['X-Content-Type-Options'] = 'nosniff'
        response['X-Frame-Options'] = 'DENY'
        response['Referrer-Policy'] = 'strict-origin-when-cross-origin'

        if request.is_secure():
            response['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'

        return response
# settings.py
MIDDLEWARE = [
    'app.middleware.SecurityHeadersMiddleware',
    # ... outros middlewares
]

# Para CSP, use django-csp
INSTALLED_APPS = [
    'csp',
    # ...
]

CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "https://cdn.example.com")
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")

FastAPI/Starlette com SecureHeaders

pip install secure
from fastapi import FastAPI
from secure import SecureHeaders

app = FastAPI()
secure_headers = SecureHeaders()

@app.middleware("http")
async def add_security_headers(request, call_next):
    response = await call_next(request)
    secure_headers.framework.fastapi(response)
    return response

@app.get("/")
async def root():
    return {"message": "Headers seguros via middleware"}

3. Proxies Reversos como Camada Centralizada

Nginx

O Nginx permite aplicar headers globalmente ou por localização:

server {
    listen 443 ssl;
    server_name exemplo.com;

    # Headers globais
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # HSTS condicional
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # CSP dinâmico com map
    map $uri $csp_policy {
        default "default-src 'self'";
        ~^/api/ "default-src 'self'; script-src 'none'";
    }

    add_header Content-Security-Policy $csp_policy always;

    location / {
        proxy_pass http://localhost:3000;
    }
}

Apache com mod_headers

<VirtualHost *:443>
    ServerName exemplo.com

    Header always set X-Frame-Options "DENY"
    Header always set X-Content-Type-Options "nosniff"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"

    <LocationMatch "^/api/">
        Header unset X-Frame-Options
        Header set Content-Security-Policy "default-src 'self'; script-src 'none'"
    </LocationMatch>
</VirtualHost>

Traefik em Docker Compose

version: '3.8'

services:
  traefik:
    image: traefik:v3.0
    command:
      - "--providers.docker=true"
      - "--entrypoints.websecure.address=:443"
    labels:
      - "traefik.http.middlewares.security-headers.headers.customframeoptionsvalue=DENY"
      - "traefik.http.middlewares.security-headers.headers.contenttypenosniff=true"
      - "traefik.http.middlewares.security-headers.headers.stsseconds=31536000"
      - "traefik.http.middlewares.security-headers.headers.stsincludesubdomains=true"
      - "traefik.http.middlewares.security-headers.headers.customresponseheaders.Content-Security-Policy=default-src 'self'"

  app:
    image: myapp:latest
    labels:
      - "traefik.http.routers.app.middlewares=security-headers@docker"

4. Headers Dinâmicos com Base na Rota ou Ambiente

Diferenciando produção vs. desenvolvimento

# Node.js com dotenv
const helmet = require('helmet');
require('dotenv').config();

const cspDirectives = {
  defaultSrc: ["'self'"],
  scriptSrc: ["'self'"],
  styleSrc: ["'self'", "'unsafe-inline'"],
};

if (process.env.NODE_ENV === 'development') {
  cspDirectives.scriptSrc.push("'unsafe-eval'"); // Para hot reload
  cspDirectives.styleSrc.push("'unsafe-inline'");
}

app.use(helmet({
  contentSecurityPolicy: { directives: cspDirectives }
}));

Headers específicos para APIs

// Express middleware condicional
function apiSecurityHeaders(req, res, next) {
  if (req.path.startsWith('/api/')) {
    res.setHeader('X-Frame-Options', 'SAMEORIGIN'); // APIs podem ser embutidas
    res.setHeader('Content-Security-Policy', "default-src 'none'"); // APIs não renderizam HTML
  } else {
    res.setHeader('X-Frame-Options', 'DENY');
  }
  next();
}

app.use(apiSecurityHeaders);

5. Testando e Validando Headers Automaticamente

Testes com curl

# Verificar headers de uma URL
curl -I https://exemplo.com

# Script de validação
#!/bin/bash
URL="https://exemplo.com"
HEADERS=$(curl -sI $URL)

echo "$HEADERS" | grep -q "Strict-Transport-Security" && echo "HSTS presente" || echo "HSTS ausente!"
echo "$HEADERS" | grep -q "X-Frame-Options: DENY" && echo "X-Frame-Options OK" || echo "X-Frame-Options ausente!"

Testes unitários com supertest (Node.js)

const request = require('supertest');
const app = require('./app');

describe('Security Headers', () => {
  it('deve incluir X-Frame-Options', async () => {
    const res = await request(app).get('/');
    expect(res.headers['x-frame-options']).toBe('DENY');
  });

  it('deve incluir HSTS em HTTPS', async () => {
    const res = await request(app)
      .get('/')
      .set('X-Forwarded-Proto', 'https');
    expect(res.headers['strict-transport-security']).toBeDefined();
  });

  it('CSP deve bloquear scripts inline', async () => {
    const res = await request(app).get('/');
    expect(res.headers['content-security-policy']).toContain("script-src 'self'");
  });
});

Testes de integração com pytest (Python)

import requests
from django.test import TestCase

class SecurityHeadersTest(TestCase):
    def test_headers_presentes(self):
        response = self.client.get('/')
        self.assertEqual(response['X-Frame-Options'], 'DENY')
        self.assertEqual(response['X-Content-Type-Options'], 'nosniff')
        self.assertIn('Strict-Transport-Security', response)

Verificação em CI/CD com GitHub Actions

name: Security Headers Check

on: [push, pull_request]

jobs:
  check-headers:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Start application
        run: docker-compose up -d

      - name: Wait for app
        run: sleep 10

      - name: Check security headers
        run: |
          HEADERS=$(curl -sI http://localhost:3000)
          echo "$HEADERS" | grep -q "X-Frame-Options: DENY" || exit 1
          echo "$HEADERS" | grep -q "Content-Security-Policy" || exit 1

6. Monitoramento e Alertas de Desvios

Logging de headers ausentes

// Express middleware de monitoramento
app.use((req, res, next) => {
  const originalSend = res.send;
  res.send = function(body) {
    const requiredHeaders = [
      'x-frame-options',
      'x-content-type-options',
      'strict-transport-security'
    ];

    requiredHeaders.forEach(header => {
      if (!res.getHeader(header)) {
        console.warn(`Header ausente: ${header} na rota ${req.path}`);
        // Enviar métrica para Prometheus
        metrics.securityHeadersMissing.inc({ header, route: req.path });
      }
    });

    originalSend.call(this, body);
  };
  next();
});

Métricas com Prometheus

# Prometheus metrics endpoint
const prometheus = require('prom-client');

const headersPresentGauge = new prometheus.Gauge({
  name: 'security_headers_present',
  help: 'Security headers present in responses',
  labelNames: ['header', 'route']
});

// Atualizar métricas a cada requisição
app.use((req, res, next) => {
  res.on('finish', () => {
    const headers = ['x-frame-options', 'content-security-policy', 'strict-transport-security'];
    headers.forEach(header => {
      headersPresentGauge.set(
        { header, route: req.path },
        res.getHeader(header) ? 1 : 0
      );
    });
  });
  next();
});

7. Armadilhas Comuns e Boas Práticas

Cuidado com CDNs e balanceadores

CDNs como Cloudflare podem sobrescrever headers. Configure explicitamente:

# Nginx atrás de CDN
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy $csp always;

# Cloudflare: desabilitar "Auto Minify" que remove headers
# Verificar se "Security Headers" não está conflitando com sua configuração

Headers conflitantes

X-Frame-Options e CSP frame-ancestors podem conflitar. Prefira CSP moderno:

# Erro comum: ambos configurados
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'self'  # Conflito!

# Correto: usar apenas CSP
Content-Security-Policy: frame-ancestors 'none'
# Remove X-Frame-Options se CSP cobrir

Versionamento de políticas

Mantenha políticas versionadas para rollback seguro:

# Configuração versionada
const CSP_POLICIES = {
  v1: "default-src 'self'",
  v2: "default-src 'self'; script-src 'self' https://cdn.example.com",
  v3: "default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'unsafe-inline'"
};

const activePolicy = process.env.CSP_VERSION || 'v3';

app.use(helmet({
  contentSecurityPolicy: {
    directives: parsePolicy(CSP_POLICIES[activePolicy])
  }
}));

Boas práticas finais

  1. Teste em staging primeiro: headers restritivos podem quebrar funcionalidades
  2. Use report-uri/report-to no CSP: capture violações sem bloquear
  3. Documente exceções: cada header personalizado deve ter justificativa
  4. Automatize a rotação de HSTS: nunca remova HSTS sem planejamento
  5. Monitore no Kibana/Grafana: crie dashboards de conformidade de headers

Referências