Guides

Segurança

Proteja seus templates, dados e API keys seguindo práticas de segurança essenciais

Este guia cobre as melhores práticas de segurança para usar o RenderHub de forma segura e proteger dados sensíveis.

Proteção de API Keys

NUNCA Exponha API Keys

Sua API key é o acesso à sua conta RenderHub. Proteja-a como uma senha:

// ❌ NUNCA faça isso
const apiKey = 'rh_live_abc123def456'; // Hard-coded no código
const client = new RenderHubClient({ apiKey });

// ❌ NUNCA faça isso
const config = {
  renderhub: {
    apiKey: 'rh_live_abc123def456', // Commitado no git
  },
};

// ❌ NUNCA faça isso
app.get('/config', (req, res) => {
  res.json({
    apiKey: process.env.RENDERHUB_API_KEY, // Expondo no cliente
  });
});

// ✅ SEMPRE faça isso
const apiKey = process.env.RENDERHUB_API_KEY;
if (!apiKey) {
  throw new Error('RENDERHUB_API_KEY não configurada');
}
const client = new RenderHubClient({ apiKey });

Use Variáveis de Ambiente

Configure API keys como variáveis de ambiente:

# .env (NUNCA commite este arquivo!)
RENDERHUB_API_KEY=rh_live_abc123def456
# .env.example (Commite este arquivo como referência)
RENDERHUB_API_KEY=your_api_key_here
// .gitignore (SEMPRE ignore .env)
.env
.env.local
.env.*.local

Ambientes Diferentes

Use keys diferentes para cada ambiente:

# .env.development
RENDERHUB_API_KEY=rh_test_dev123

# .env.staging
RENDERHUB_API_KEY=rh_test_staging456

# .env.production
RENDERHUB_API_KEY=rh_live_prod789
// Configuração por ambiente
const config = {
  development: {
    apiKey: process.env.RENDERHUB_API_KEY,
    baseUrl: 'https://dev-api.renderhub.com',
  },
  production: {
    apiKey: process.env.RENDERHUB_API_KEY,
    baseUrl: 'https://api.renderhub.com',
  },
};

const env = process.env.NODE_ENV || 'development';
const client = new RenderHubClient(config[env]);

Rotação de Keys

Rotacione API keys periodicamente:

// Sistema de rotação de keys
class ApiKeyRotation {
  private currentKey: string;
  private nextKey: string | null = null;
  private rotationDate: Date | null = null;

  constructor() {
    this.currentKey = process.env.RENDERHUB_API_KEY!;
    this.nextKey = process.env.RENDERHUB_API_KEY_NEXT || null;

    if (process.env.KEY_ROTATION_DATE) {
      this.rotationDate = new Date(process.env.KEY_ROTATION_DATE);
    }
  }

  getKey(): string {
    // Se passou da data de rotação, use a nova key
    if (this.rotationDate && new Date() >= this.rotationDate && this.nextKey) {
      logger.info('Rotacionando para nova API key');
      this.currentKey = this.nextKey;
      this.nextKey = null;
      this.rotationDate = null;
    }

    return this.currentKey;
  }
}

const keyRotation = new ApiKeyRotation();
const client = new RenderHubClient({ apiKey: keyRotation.getKey() });

Validação e Sanitização de Dados

Valide Todas as Entradas

Sempre valide dados de usuários antes de processar:

import { z } from 'zod';

// ✅ Schema de validação
const InvoiceDataSchema = z.object({
  customerName: z.string().min(1).max(200),
  customerEmail: z.string().email(),
  items: z.array(
    z.object({
      name: z.string().min(1).max(200),
      quantity: z.number().int().positive().max(10000),
      price: z.number().positive().max(1000000),
    })
  ).min(1).max(100),
  notes: z.string().max(5000).optional(),
});

// Uso
try {
  const validatedData = InvoiceDataSchema.parse(req.body);
  const pdf = await renderHub.generatePDF(templateId, validatedData);
  res.send(pdf);
} catch (error) {
  if (error instanceof z.ZodError) {
    res.status(400).json({ error: 'Dados inválidos', details: error.errors });
  } else {
    throw error;
  }
}

Sanitize HTML e Texto

Sempre sanitize conteúdo que pode conter HTML:

import DOMPurify from 'isomorphic-dompurify';
import validator from 'validator';

// ❌ Evite: Dados não sanitizados
const data = {
  customerName: req.body.name, // Pode conter: <script>alert('XSS')</script>
  notes: req.body.notes,
};

// ✅ Recomendado: Sanitizar entrada
const data = {
  customerName: DOMPurify.sanitize(req.body.name),
  notes: DOMPurify.sanitize(req.body.notes, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
    ALLOWED_ATTR: [],
  }),
  email: validator.normalizeEmail(req.body.email),
};

Previna Injeção de Template

Proteja contra injeção de código Handlebars:

// ❌ Risco: Permitir usuários definirem templates
app.post('/custom-template', async (req, res) => {
  const userTemplate = req.body.template; // Pode conter código malicioso
  const template = await renderHub.createTemplate('user-custom', userTemplate);
  // ❌ PERIGOSO!
});

// ✅ Recomendado: Apenas templates pré-aprovados
const ALLOWED_TEMPLATES = {
  'invoice': 'tmpl_abc123',
  'receipt': 'tmpl_def456',
  'certificate': 'tmpl_ghi789',
};

app.post('/generate', async (req, res) => {
  const templateName = req.body.templateName;

  if (!ALLOWED_TEMPLATES[templateName]) {
    return res.status(400).json({ error: 'Template não permitido' });
  }

  const templateId = ALLOWED_TEMPLATES[templateName];
  const pdf = await renderHub.generatePDF(templateId, req.body.data);
  res.send(pdf);
});

Limite Tamanho de Dados

Proteja contra ataques de exaustão de memória:

import express from 'express';

const app = express();

// ✅ Limite tamanho de payload
app.use(express.json({
  limit: '1mb', // Máximo 1MB por requisição
}));

app.use(express.urlencoded({
  limit: '1mb',
  extended: true,
}));

// ✅ Validação adicional
app.post('/generate', async (req, res) => {
  const data = req.body;

  // Limite número de items
  if (Array.isArray(data.items) && data.items.length > 1000) {
    return res.status(400).json({ error: 'Muitos items' });
  }

  // Limite tamanho de strings
  if (data.notes && data.notes.length > 10000) {
    return res.status(400).json({ error: 'Notas muito longas' });
  }

  const pdf = await renderHub.generatePDF(templateId, data);
  res.send(pdf);
});

Controle de Acesso

Autenticação Obrigatória

Sempre exija autenticação para gerar PDFs:

// ❌ Evite: Endpoint público
app.get('/invoice/:id/pdf', async (req, res) => {
  const pdf = await generateInvoicePDF(req.params.id);
  res.send(pdf);
});
// Qualquer pessoa pode gerar PDFs com qualquer ID!

// ✅ Recomendado: Autenticação obrigatória
import { requireAuth } from './middleware/auth';

app.get('/invoice/:id/pdf', requireAuth, async (req, res) => {
  const pdf = await generateInvoicePDF(req.params.id);
  res.send(pdf);
});

Autorização por Recurso

Verifique se o usuário tem permissão para acessar o recurso:

// ✅ Middleware de autorização
async function authorizeInvoiceAccess(req, res, next) {
  const userId = req.user.id;
  const invoiceId = req.params.id;

  // Verificar se o invoice pertence ao usuário
  const invoice = await db.invoices.findOne({
    where: { id: invoiceId, userId },
  });

  if (!invoice) {
    return res.status(403).json({ error: 'Acesso negado' });
  }

  req.invoice = invoice;
  next();
}

// Uso
app.get(
  '/invoice/:id/pdf',
  requireAuth,
  authorizeInvoiceAccess,
  async (req, res) => {
    const pdf = await generateInvoicePDF(req.invoice);
    res.send(pdf);
  }
);

Rate Limiting por Usuário

Previna abuso com rate limiting:

import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import Redis from 'ioredis';

const redis = new Redis();

// ✅ Rate limiting por usuário
const pdfRateLimiter = rateLimit({
  store: new RedisStore({
    client: redis,
    prefix: 'rl:pdf:',
  }),
  windowMs: 60 * 1000, // 1 minuto
  max: 10, // Máximo 10 PDFs por minuto
  keyGenerator: (req) => req.user.id, // Por usuário
  handler: (req, res) => {
    res.status(429).json({
      error: 'Muitas requisições. Tente novamente em 1 minuto.',
    });
  },
});

app.get(
  '/invoice/:id/pdf',
  requireAuth,
  pdfRateLimiter,
  async (req, res) => {
    const pdf = await generateInvoicePDF(req.params.id);
    res.send(pdf);
  }
);

Proteção de Templates

Armazene Templates com Segurança

Proteja templates armazenados:

// ❌ Evite: Templates em repositório público
// /templates/invoice.html commitado em repo público

// ✅ Recomendado: Templates em storage seguro
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';

const s3 = new S3Client({ region: 'us-east-1' });

async function getSecureTemplate(templateName: string): Promise<string> {
  const command = new GetObjectCommand({
    Bucket: 'secure-templates-bucket',
    Key: `templates/${templateName}.html`,
  });

  const response = await s3.send(command);
  const body = await response.Body?.transformToString();

  if (!body) {
    throw new Error('Template não encontrado');
  }

  return body;
}

// IAM policy para bucket (somente leitura para app)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::secure-templates-bucket/templates/*"
    }
  ]
}

Versionamento de Templates

Mantenha histórico de mudanças:

// ✅ Sistema de versionamento
interface TemplateVersion {
  id: string;
  name: string;
  version: number;
  html: string;
  createdAt: Date;
  createdBy: string;
  approved: boolean;
}

async function createTemplateVersion(
  name: string,
  html: string,
  userId: string
): Promise<TemplateVersion> {
  // Validar template
  validateTemplateHTML(html);

  // Obter última versão
  const lastVersion = await db.templateVersions.findOne({
    where: { name },
    order: [['version', 'DESC']],
  });

  const newVersion = (lastVersion?.version || 0) + 1;

  // Criar nova versão (não aprovada)
  const version = await db.templateVersions.create({
    name,
    version: newVersion,
    html,
    createdBy: userId,
    approved: false,
  });

  logger.info('Template version criada', {
    name,
    version: newVersion,
    createdBy: userId,
  });

  return version;
}

// Aprovar versão (apenas admins)
async function approveTemplateVersion(versionId: string, adminId: string) {
  const version = await db.templateVersions.findByPk(versionId);

  if (!version) {
    throw new Error('Versão não encontrada');
  }

  version.approved = true;
  await version.save();

  // Upload para RenderHub
  const template = await renderHub.createTemplate(
    `${version.name}-v${version.version}`,
    version.html
  );

  logger.info('Template aprovado e deployed', {
    versionId,
    approvedBy: adminId,
    renderhubId: template.id,
  });
}

Proteção de PDFs Gerados

PDFs Sensíveis

Para PDFs com dados sensíveis, adicione proteções:

import { PDFDocument } from 'pdf-lib';

// ✅ Proteger PDF com senha
async function protectPDF(pdfBuffer: Buffer, password: string): Promise<Buffer> {
  const pdfDoc = await PDFDocument.load(pdfBuffer);

  // Adicionar senha de usuário (para abrir)
  pdfDoc.encrypt({
    userPassword: password,
    ownerPassword: generateSecurePassword(), // Senha master interna
    permissions: {
      printing: 'highResolution',
      modifying: false,
      copying: false,
      annotating: false,
      fillingForms: false,
      contentAccessibility: true,
      documentAssembly: false,
    },
  });

  return Buffer.from(await pdfDoc.save());
}

// Uso
const pdf = await renderHub.generatePDF(templateId, data);
const protectedPdf = await protectPDF(pdf, userPassword);

Marca d'Água

Adicione marca d'água para rastreabilidade:

import { PDFDocument, rgb } from 'pdf-lib';

// ✅ Adicionar marca d'água
async function addWatermark(
  pdfBuffer: Buffer,
  userId: string
): Promise<Buffer> {
  const pdfDoc = await PDFDocument.load(pdfBuffer);
  const pages = pdfDoc.getPages();

  const watermarkText = `User: ${userId} - ${new Date().toISOString()}`;

  for (const page of pages) {
    const { width, height } = page.getSize();

    page.drawText(watermarkText, {
      x: 50,
      y: height - 20,
      size: 8,
      color: rgb(0.7, 0.7, 0.7),
      opacity: 0.5,
    });
  }

  return Buffer.from(await pdfDoc.save());
}

URLs Assinadas para Download

Use URLs temporárias e assinadas:

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({ region: 'us-east-1' });

// ✅ Upload e geração de URL assinada
async function uploadAndGetSignedUrl(
  pdf: Buffer,
  key: string
): Promise<string> {
  // Upload para S3
  await s3.send(
    new PutObjectCommand({
      Bucket: 'secure-pdfs-bucket',
      Key: key,
      Body: pdf,
      ServerSideEncryption: 'AES256', // Encriptação em repouso
    })
  );

  // Gerar URL assinada (válida por 1 hora)
  const url = await getSignedUrl(
    s3,
    new GetObjectCommand({
      Bucket: 'secure-pdfs-bucket',
      Key: key,
    }),
    { expiresIn: 3600 } // 1 hora
  );

  return url;
}

// Uso
app.get('/invoice/:id/pdf', requireAuth, async (req, res) => {
  const userId = req.user.id;
  const invoiceId = req.params.id;

  // Gerar PDF
  const data = await getInvoiceData(invoiceId, userId);
  const pdf = await renderHub.generatePDF(templateId, data);

  // Upload e obter URL assinada
  const key = `invoices/${userId}/${invoiceId}.pdf`;
  const signedUrl = await uploadAndGetSignedUrl(pdf, key);

  res.json({ url: signedUrl });
});

Logging e Auditoria

Log de Operações Sensíveis

Registre todas as operações importantes:

// ✅ Logging de auditoria
interface AuditLog {
  timestamp: Date;
  userId: string;
  action: string;
  resource: string;
  resourceId: string;
  ipAddress: string;
  userAgent: string;
  success: boolean;
  errorMessage?: string;
}

async function logAudit(log: AuditLog) {
  await db.auditLogs.create(log);

  logger.info('Audit log', log);
}

// Middleware de auditoria
function auditMiddleware(action: string) {
  return async (req, res, next) => {
    const startTime = Date.now();

    // Override res.send para capturar resposta
    const originalSend = res.send;
    res.send = function (data) {
      const success = res.statusCode < 400;

      logAudit({
        timestamp: new Date(),
        userId: req.user?.id || 'anonymous',
        action,
        resource: req.path,
        resourceId: req.params.id || '',
        ipAddress: req.ip,
        userAgent: req.get('user-agent') || '',
        success,
        errorMessage: success ? undefined : data?.error,
      });

      return originalSend.call(this, data);
    };

    next();
  };
}

// Uso
app.get(
  '/invoice/:id/pdf',
  requireAuth,
  auditMiddleware('generate_pdf'),
  async (req, res) => {
    const pdf = await generateInvoicePDF(req.params.id);
    res.send(pdf);
  }
);

Retenção de Logs

Configure retenção apropriada:

// ✅ Job de limpeza de logs antigos
import cron from 'node-cron';

// Executar diariamente às 2am
cron.schedule('0 2 * * *', async () => {
  const retentionDays = 90; // Manter por 90 dias
  const cutoffDate = new Date();
  cutoffDate.setDate(cutoffDate.getDate() - retentionDays);

  const deleted = await db.auditLogs.destroy({
    where: {
      timestamp: {
        [Op.lt]: cutoffDate,
      },
    },
  });

  logger.info('Logs antigos removidos', {
    deleted,
    cutoffDate,
  });
});

Compliance

LGPD/GDPR

Implemente direito ao esquecimento:

// ✅ Deletar todos os dados de um usuário
async function deleteUserData(userId: string) {
  // Deletar PDFs armazenados
  const pdfs = await db.pdfs.findAll({ where: { userId } });

  for (const pdf of pdfs) {
    await s3.send(
      new DeleteObjectCommand({
        Bucket: 'secure-pdfs-bucket',
        Key: pdf.s3Key,
      })
    );
  }

  await db.pdfs.destroy({ where: { userId } });

  // Deletar dados pessoais
  await db.users.update(
    {
      name: '[DELETED]',
      email: '[DELETED]',
      phone: null,
    },
    { where: { id: userId } }
  );

  // Manter logs de auditoria (anonimizados)
  await db.auditLogs.update(
    {
      userId: '[DELETED]',
    },
    { where: { userId } }
  );

  logger.info('User data deleted', { userId });
}

Encriptação em Trânsito e Repouso

// ✅ Sempre use HTTPS
const https = require('https');
const fs = require('fs');

const options = {
  key: fs.readFileSync('server-key.pem'),
  cert: fs.readFileSync('server-cert.pem'),
};

https.createServer(options, app).listen(443);

// ✅ Encriptação em repouso (S3)
await s3.send(
  new PutObjectCommand({
    Bucket: 'secure-pdfs-bucket',
    Key: key,
    Body: pdf,
    ServerSideEncryption: 'AES256', // Encriptação gerenciada pelo S3
  })
);

// ✅ Encriptação client-side (mais seguro)
import { createCipheriv, randomBytes } from 'crypto';

function encryptPDF(pdfBuffer: Buffer, key: Buffer): Buffer {
  const iv = randomBytes(16);
  const cipher = createCipheriv('aes-256-cbc', key, iv);

  const encrypted = Buffer.concat([
    cipher.update(pdfBuffer),
    cipher.final(),
  ]);

  // Retornar IV + dados encriptados
  return Buffer.concat([iv, encrypted]);
}

Checklist de Segurança

  • API keys em variáveis de ambiente (nunca hard-coded)
  • .env no .gitignore
  • Keys diferentes para cada ambiente
  • Rotação periódica de API keys
  • Validação de todas as entradas de usuários
  • Sanitização de HTML e texto
  • Limite de tamanho de payload
  • Autenticação obrigatória
  • Autorização por recurso
  • Rate limiting implementado
  • Templates armazenados com segurança
  • Versionamento de templates
  • PDFs sensíveis protegidos com senha
  • Marca d'água para rastreabilidade
  • URLs assinadas para downloads
  • Logging de auditoria completo
  • Retenção de logs configurada
  • Compliance com LGPD/GDPR
  • Encriptação em trânsito (HTTPS)
  • Encriptação em repouso
  • Plano de resposta a incidentes

Próximos Passos

On this page