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)
-
.envno.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
- Implemente Caching de forma segura
- Revise Best Practices para código seguro
- Otimize com guia de Performance