# Plano de Melhorias de Segurança ## MCP Outline PostgreSQL v1.2.2 → v1.3.0 **Data:** 2026-01-31 **Objectivo:** Implementar melhorias de segurança identificadas na auditoria **Score Actual:** 8.5/10 **Score Alvo:** 9.5/10 **Esforço Total Estimado:** 10-15 dias --- ## Visão Geral Este plano implementa as recomendações da auditoria de segurança, priorizadas em 4 níveis: - **P0 (Crítico):** Implementar ANTES de produção - **P1 (Alto):** Implementar em 1-2 semanas - **P2 (Médio):** Implementar em 1 mês - **P3 (Baixo):** Backlog --- ## Fase 1: P0 - Crítico (5-8 dias) ### Tarefa 1.1: Sistema de Autenticação/Autorização **Objectivo:** Implementar contexto de utilizador e verificação de permissões em todas as tools. **Esforço:** 3-5 dias **Prioridade:** P0 - Crítico #### Ficheiros a Modificar **Novos Ficheiros:** - [NEW] [auth-context.ts](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/src/utils/auth-context.ts) - [NEW] [permissions.ts](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/src/utils/permissions.ts) **Ficheiros a Modificar:** - [MODIFY] [types/tools.ts](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/src/types/tools.ts) - [MODIFY] [index.ts](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/src/index.ts) - [MODIFY] Todos os 33 módulos em [tools/](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/src/tools/) #### Passos de Implementação **1.1.1 - Criar Interface de Contexto** Criar `src/utils/auth-context.ts`: ```typescript /** * MCP Outline PostgreSQL - Authentication Context * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 */ export interface MCPContext { userId: string; role: 'admin' | 'member' | 'viewer'; teamId: string; email: string; name: string; } /** * Extract context from MCP request metadata */ export function extractContext(metadata?: Record): MCPContext { if (!metadata || !metadata.userId) { throw new Error('Authentication required: No user context provided'); } return { userId: String(metadata.userId), role: (metadata.role as 'admin' | 'member' | 'viewer') || 'member', teamId: String(metadata.teamId || ''), email: String(metadata.email || ''), name: String(metadata.name || 'Unknown') }; } /** * Validate context has required role */ export function requireRole(context: MCPContext, requiredRole: 'admin' | 'member' | 'viewer'): void { const roleHierarchy = { admin: 3, member: 2, viewer: 1 }; if (roleHierarchy[context.role] < roleHierarchy[requiredRole]) { throw new Error(`Unauthorized: ${requiredRole} role required`); } } ``` **1.1.2 - Criar Sistema de Permissões** Criar `src/utils/permissions.ts`: ```typescript /** * MCP Outline PostgreSQL - Permissions System * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 */ import { Pool } from 'pg'; import { MCPContext } from './auth-context.js'; export type Resource = 'document' | 'collection' | 'user' | 'team'; export type Action = 'read' | 'write' | 'delete' | 'admin'; /** * Check if user has permission for action on resource */ export async function checkPermission( pgClient: Pool, context: MCPContext, resource: Resource, resourceId: string, action: Action ): Promise { // Admins have all permissions if (context.role === 'admin') { return true; } // Viewers can only read if (context.role === 'viewer' && action !== 'read') { return false; } // Check resource-specific permissions switch (resource) { case 'document': return await checkDocumentPermission(pgClient, context, resourceId, action); case 'collection': return await checkCollectionPermission(pgClient, context, resourceId, action); case 'user': return await checkUserPermission(pgClient, context, resourceId, action); case 'team': return context.role === 'admin'; default: return false; } } async function checkDocumentPermission( pgClient: Pool, context: MCPContext, documentId: string, action: Action ): Promise { // Check if user is creator const result = await pgClient.query( `SELECT "createdById", "collectionId" FROM documents WHERE id = $1 AND "deletedAt" IS NULL`, [documentId] ); if (result.rows.length === 0) return false; const doc = result.rows[0]; // Creator can do anything except admin actions if (doc.createdById === context.userId && action !== 'admin') { return true; } // Check collection permissions return await checkCollectionPermission(pgClient, context, doc.collectionId, action); } async function checkCollectionPermission( pgClient: Pool, context: MCPContext, collectionId: string, action: Action ): Promise { const result = await pgClient.query( `SELECT permission FROM collection_users WHERE "userId" = $1 AND "collectionId" = $2`, [context.userId, collectionId] ); if (result.rows.length === 0) return false; const permission = result.rows[0].permission; // Map permissions to actions const permissionMap: Record = { admin: ['read', 'write', 'delete', 'admin'], read_write: ['read', 'write'], read: ['read'] }; return permissionMap[permission]?.includes(action) || false; } async function checkUserPermission( pgClient: Pool, context: MCPContext, userId: string, action: Action ): Promise { // Users can read/write their own profile if (userId === context.userId && action !== 'delete' && action !== 'admin') { return true; } // Only admins can modify other users return false; } /** * Require permission or throw error */ export async function requirePermission( pgClient: Pool, context: MCPContext, resource: Resource, resourceId: string, action: Action ): Promise { const hasPermission = await checkPermission(pgClient, context, resourceId, action); if (!hasPermission) { throw new Error(`Unauthorized: ${action} permission required for ${resource} ${resourceId}`); } } ``` **1.1.3 - Actualizar Interface BaseTool** Modificar `src/types/tools.ts`: ```typescript import { MCPContext } from '../utils/auth-context.js'; export interface BaseTool { name: string; description: string; inputSchema: { type: 'object'; properties: Record; required?: string[]; }; // Adicionar contexto ao handler handler: (args: T, pgClient: any, context: MCPContext) => Promise; } ``` **1.1.4 - Actualizar Exemplo de Tool** Modificar `src/tools/documents.ts` (exemplo): ```typescript import { requireRole } from '../utils/auth-context.js'; import { requirePermission } from '../utils/permissions.js'; const deleteDocument: BaseTool<{ id: string; permanent?: boolean }> = { name: 'delete_document', description: 'Eliminar documento (soft delete por default, permanente se especificado).', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'UUID do documento' }, permanent: { type: 'boolean', description: 'Eliminação permanente (irreversível, default: false)' } }, required: ['id'] }, handler: async (args, pgClient, context): Promise => { try { if (!isValidUUID(args.id)) { throw new Error('id inválido (deve ser UUID)'); } // ✅ NOVO: Verificar permissões await requirePermission( pgClient, context, 'document', args.id, args.permanent ? 'admin' : 'delete' ); let query: string; if (args.permanent) { // ✅ NOVO: Apenas admins podem fazer delete permanente requireRole(context, 'admin'); query = `DELETE FROM documents WHERE id = $1 RETURNING id`; } else { query = `UPDATE documents SET "deletedAt" = NOW(), "deletedById" = $2 WHERE id = $1 AND "deletedAt" IS NULL RETURNING id, "deletedAt"`; } const result = await pgClient.query( query, args.permanent ? [args.id] : [args.id, context.userId] // ✅ NOVO: Usar userId real ); if (result.rows.length === 0) { return { content: [{ type: 'text', text: JSON.stringify({ error: 'Documento não encontrado ou já eliminado' }, null, 2) }] }; } return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: args.permanent ? 'Documento eliminado permanentemente' : 'Documento eliminado (soft delete)', id: result.rows[0].id }, null, 2) }] }; } catch (error) { return { content: [{ type: 'text', text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) }] }; } } }; ``` **1.1.5 - Actualizar MCP Entry Point** Modificar `src/index.ts`: ```typescript import { extractContext } from './utils/auth-context.js'; // No handler de cada tool call server.setRequestHandler(CallToolRequestSchema, async (request) => { try { // ✅ NOVO: Extrair contexto de autenticação const context = extractContext(request.params._meta); const tool = allTools.find(t => t.name === request.params.name); if (!tool) { throw new Error(`Tool not found: ${request.params.name}`); } // ✅ NOVO: Passar contexto ao handler const result = await tool.handler(request.params.arguments || {}, pgClient, context); return result; } catch (error) { return { content: [{ type: 'text', text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }) }], isError: true }; } }); ``` #### Testes de Verificação ```bash # 1. Testar autenticação obrigatória # Deve falhar sem contexto curl -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -d '{"method": "tools/call", "params": {"name": "delete_document", "arguments": {"id": "..."}}}' # 2. Testar verificação de permissões # Deve falhar se utilizador não tiver permissão curl -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -d '{"method": "tools/call", "params": {"name": "delete_document", "arguments": {"id": "..."}, "_meta": {"userId": "...", "role": "viewer"}}}' # 3. Testar operação autorizada # Deve ter sucesso curl -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -d '{"method": "tools/call", "params": {"name": "delete_document", "arguments": {"id": "..."}, "_meta": {"userId": "...", "role": "admin"}}}' ``` --- ### Tarefa 1.2: Implementar Audit Log **Objectivo:** Registar todas as operações sensíveis numa tabela de auditoria. **Esforço:** 2-3 dias **Prioridade:** P0 - Crítico #### Ficheiros a Criar/Modificar **Novos Ficheiros:** - [NEW] [migrations/001_create_audit_log.sql](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/migrations/001_create_audit_log.sql) - [NEW] [audit-log.ts](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/src/utils/audit-log.ts) **Ficheiros a Modificar:** - [MODIFY] Todos os tools que fazem operações de escrita #### Passos de Implementação **1.2.1 - Criar Tabela de Audit Log** Criar `migrations/001_create_audit_log.sql`: ```sql -- Audit Log Table CREATE TABLE IF NOT EXISTS audit_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), user_id UUID REFERENCES users(id), user_email VARCHAR(255), user_role VARCHAR(50), action VARCHAR(100) NOT NULL, resource VARCHAR(50) NOT NULL, resource_id UUID, result VARCHAR(20) NOT NULL, details JSONB, ip_address INET, user_agent TEXT, duration_ms INTEGER ); -- Indexes for performance CREATE INDEX idx_audit_log_timestamp ON audit_log(timestamp DESC); CREATE INDEX idx_audit_log_user_id ON audit_log(user_id); CREATE INDEX idx_audit_log_resource ON audit_log(resource, resource_id); CREATE INDEX idx_audit_log_action ON audit_log(action); CREATE INDEX idx_audit_log_result ON audit_log(result); -- Partition by month (optional, for large volumes) -- CREATE TABLE audit_log_2026_01 PARTITION OF audit_log -- FOR VALUES FROM ('2026-01-01') TO ('2026-02-01'); COMMENT ON TABLE audit_log IS 'Audit trail of all sensitive operations'; COMMENT ON COLUMN audit_log.action IS 'Operation performed (e.g., delete_document, update_user)'; COMMENT ON COLUMN audit_log.result IS 'Operation result: success, failure, unauthorized'; COMMENT ON COLUMN audit_log.details IS 'Additional context (before/after values, error messages)'; ``` **1.2.2 - Criar Módulo de Audit Log** Criar `src/utils/audit-log.ts`: ```typescript /** * MCP Outline PostgreSQL - Audit Log * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 */ import { Pool } from 'pg'; import { MCPContext } from './auth-context.js'; import { logger } from './logger.js'; export interface AuditLogEntry { userId: string; userEmail: string; userRole: string; action: string; resource: string; resourceId?: string; result: 'success' | 'failure' | 'unauthorized'; details?: Record; ipAddress?: string; userAgent?: string; durationMs?: number; } /** * Log operation to audit_log table */ export async function logAudit( pgClient: Pool, context: MCPContext, entry: Omit ): Promise { try { await pgClient.query( `INSERT INTO audit_log ( user_id, user_email, user_role, action, resource, resource_id, result, details, ip_address, user_agent, duration_ms ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, [ context.userId, context.email, context.role, entry.action, entry.resource, entry.resourceId || null, entry.result, entry.details ? JSON.stringify(entry.details) : null, entry.ipAddress || null, entry.userAgent || null, entry.durationMs || null ] ); } catch (error) { // Don't fail operation if audit log fails, but log error logger.error('Failed to write audit log', { error: error instanceof Error ? error.message : String(error), entry }); } } /** * Helper to wrap operation with audit logging */ export async function withAudit( pgClient: Pool, context: MCPContext, action: string, resource: string, resourceId: string | undefined, operation: () => Promise ): Promise { const startTime = Date.now(); try { const result = await operation(); await logAudit(pgClient, context, { action, resource, resourceId, result: 'success', durationMs: Date.now() - startTime }); return result; } catch (error) { await logAudit(pgClient, context, { action, resource, resourceId, result: error instanceof Error && error.message.includes('Unauthorized') ? 'unauthorized' : 'failure', details: { error: error instanceof Error ? error.message : String(error) }, durationMs: Date.now() - startTime }); throw error; } } ``` **1.2.3 - Integrar Audit Log nas Tools** Modificar `src/tools/documents.ts` (exemplo): ```typescript import { withAudit } from '../utils/audit-log.js'; const deleteDocument: BaseTool<{ id: string; permanent?: boolean }> = { // ... schema ... handler: async (args, pgClient, context): Promise => { return await withAudit( pgClient, context, args.permanent ? 'delete_document_permanent' : 'delete_document', 'document', args.id, async () => { // Lógica existente aqui // ... } ); } }; ``` **1.2.4 - Executar Migration** ```bash # Executar migration psql $DATABASE_URL -f migrations/001_create_audit_log.sql # Verificar tabela criada psql $DATABASE_URL -c "\d audit_log" ``` #### Testes de Verificação ```sql -- 1. Verificar logs de operações SELECT timestamp, user_email, action, resource, result, duration_ms FROM audit_log ORDER BY timestamp DESC LIMIT 10; -- 2. Verificar operações falhadas SELECT timestamp, user_email, action, details->>'error' as error_message FROM audit_log WHERE result = 'failure' ORDER BY timestamp DESC; -- 3. Verificar tentativas não autorizadas SELECT timestamp, user_email, user_role, action, resource FROM audit_log WHERE result = 'unauthorized' ORDER BY timestamp DESC; -- 4. Estatísticas por utilizador SELECT user_email, COUNT(*) as total_operations, COUNT(*) FILTER (WHERE result = 'success') as successful, COUNT(*) FILTER (WHERE result = 'failure') as failed, COUNT(*) FILTER (WHERE result = 'unauthorized') as unauthorized FROM audit_log GROUP BY user_email ORDER BY total_operations DESC; ``` --- ## Fase 2: P1 - Alto (3-4 dias) ### Tarefa 2.1: Activar Query Logging **Objectivo:** Registar todas as queries de escrita para debugging e auditoria. **Esforço:** 1 dia **Prioridade:** P1 - Alto #### Ficheiros a Modificar - [MODIFY] [logger.ts](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/src/utils/logger.ts) - [MODIFY] [pg-client.ts](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/src/pg-client.ts) - [MODIFY] [.env.example](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/.env.example) #### Passos de Implementação **2.1.1 - Melhorar Logger** Modificar `src/utils/logger.ts`: ```typescript export function logQuery( sql: string, params?: any[], duration?: number, userId?: string ): void { // ✅ NOVO: Activar em produção para queries de escrita const isWriteQuery = /^(INSERT|UPDATE|DELETE|CREATE|DROP|ALTER)/i.test(sql.trim()); if (process.env.ENABLE_AUDIT_LOG === 'true' || (process.env.NODE_ENV === 'production' && isWriteQuery)) { logger.info('SQL', { sql: sql.substring(0, 200), // Aumentar limite params: params ? params.length : 0, duration, userId, type: isWriteQuery ? 'WRITE' : 'READ' }); } } ``` **2.1.2 - Actualizar PgClient** Modificar `src/pg-client.ts`: ```typescript async query(sql: string, params?: any[]): Promise { const start = Date.now(); try { const result = await this.pool.query(sql, params); const duration = Date.now() - start; // ✅ NOVO: Log com mais detalhes logQuery(sql, params, duration); // ✅ NOVO: Alertar queries lentas if (duration > 1000) { logger.warn('Slow query detected', { sql: sql.substring(0, 200), duration, rowCount: result.rowCount }); } return result.rows; } catch (error) { const duration = Date.now() - start; logger.error('Query failed', { sql: sql.substring(0, 200), duration, error: error instanceof Error ? error.message : String(error) }); throw error; } } ``` **2.1.3 - Actualizar .env.example** ```bash # Logging LOG_LEVEL=info # error, warn, info, debug ENABLE_AUDIT_LOG=true SLOW_QUERY_THRESHOLD_MS=1000 ``` #### Testes de Verificação ```bash # 1. Verificar logs de queries tail -f logs/app.log | grep SQL # 2. Verificar queries lentas tail -f logs/app.log | grep "Slow query" # 3. Verificar queries de escrita tail -f logs/app.log | grep "WRITE" ``` --- ### Tarefa 2.2: Melhorar Gestão de Erros **Objectivo:** Sanitizar mensagens de erro para não expor detalhes internos. **Esforço:** 2 dias **Prioridade:** P1 - Alto #### Ficheiros a Criar/Modificar **Novos Ficheiros:** - [NEW] [error-handler.ts](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/src/utils/error-handler.ts) **Ficheiros a Modificar:** - [MODIFY] Todos os tools (handlers) #### Passos de Implementação **2.2.1 - Criar Error Handler** Criar `src/utils/error-handler.ts`: ```typescript /** * MCP Outline PostgreSQL - Error Handler * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 */ import { logger } from './logger.js'; export class AppError extends Error { constructor( public message: string, public statusCode: number = 500, public code: string = 'INTERNAL_ERROR', public details?: Record ) { super(message); this.name = 'AppError'; } } export class ValidationError extends AppError { constructor(message: string, details?: Record) { super(message, 400, 'VALIDATION_ERROR', details); this.name = 'ValidationError'; } } export class UnauthorizedError extends AppError { constructor(message: string = 'Unauthorized') { super(message, 401, 'UNAUTHORIZED'); this.name = 'UnauthorizedError'; } } export class NotFoundError extends AppError { constructor(resource: string, id?: string) { super( id ? `${resource} with id ${id} not found` : `${resource} not found`, 404, 'NOT_FOUND' ); this.name = 'NotFoundError'; } } /** * Sanitize error for client response */ export function sanitizeError(error: unknown): { error: string; code?: string; details?: Record } { if (error instanceof AppError) { return { error: error.message, code: error.code, ...(process.env.NODE_ENV !== 'production' && error.details && { details: error.details }) }; } if (error instanceof Error) { // Log full error internally logger.error('Unhandled error', { name: error.name, message: error.message, stack: error.stack }); // Return generic message to client in production if (process.env.NODE_ENV === 'production') { return { error: 'An internal error occurred', code: 'INTERNAL_ERROR' }; } else { return { error: error.message, code: 'INTERNAL_ERROR' }; } } return { error: 'An unknown error occurred', code: 'UNKNOWN_ERROR' }; } ``` **2.2.2 - Actualizar Tools** Modificar `src/tools/documents.ts` (exemplo): ```typescript import { sanitizeError, NotFoundError, ValidationError } from '../utils/error-handler.js'; const deleteDocument: BaseTool<{ id: string; permanent?: boolean }> = { // ... schema ... handler: async (args, pgClient, context): Promise => { try { if (!isValidUUID(args.id)) { throw new ValidationError('Invalid document ID format'); } // ... lógica ... if (result.rows.length === 0) { throw new NotFoundError('Document', args.id); } return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: args.permanent ? 'Document permanently deleted' : 'Document deleted (soft delete)', id: result.rows[0].id }, null, 2) }] }; } catch (error) { return { content: [{ type: 'text', text: JSON.stringify(sanitizeError(error), null, 2) }], isError: true }; } } }; ``` #### Testes de Verificação ```bash # 1. Testar erro de validação # Deve retornar mensagem limpa curl -X POST http://localhost:3000/mcp \ -d '{"method": "tools/call", "params": {"name": "delete_document", "arguments": {"id": "invalid"}}}' # 2. Testar erro de não encontrado # Deve retornar mensagem limpa curl -X POST http://localhost:3000/mcp \ -d '{"method": "tools/call", "params": {"name": "delete_document", "arguments": {"id": "00000000-0000-0000-0000-000000000000"}}}' # 3. Verificar que detalhes internos não são expostos em produção NODE_ENV=production npm start ``` --- ## Fase 3: P2 - Médio (2-3 dias) ### Tarefa 3.1: Rate Limiting Distribuído **Objectivo:** Migrar rate limiting para PostgreSQL para suportar múltiplas instâncias. **Esforço:** 2-3 dias **Prioridade:** P2 - Médio #### Ficheiros a Criar/Modificar **Novos Ficheiros:** - [NEW] [migrations/002_create_rate_limit.sql](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/migrations/002_create_rate_limit.sql) **Ficheiros a Modificar:** - [MODIFY] [security.ts](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/src/utils/security.ts) #### Passos de Implementação **3.1.1 - Criar Tabela de Rate Limiting** Criar `migrations/002_create_rate_limit.sql`: ```sql CREATE TABLE IF NOT EXISTS rate_limit ( key VARCHAR(255) PRIMARY KEY, count INTEGER NOT NULL DEFAULT 1, reset_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_rate_limit_reset_at ON rate_limit(reset_at); COMMENT ON TABLE rate_limit IS 'Distributed rate limiting store'; ``` **3.1.2 - Actualizar Security.ts** Modificar `src/utils/security.ts`: ```typescript import { Pool } from 'pg'; /** * Check rate limit using PostgreSQL (distributed) */ export async function checkRateLimitDistributed( pgClient: Pool, type: string, clientId: string ): Promise { const key = `${type}:${clientId}`; const now = new Date(); const resetAt = new Date(now.getTime() + RATE_LIMIT_WINDOW); try { // Upsert rate limit entry const result = await pgClient.query(` INSERT INTO rate_limit (key, count, reset_at) VALUES ($1, 1, $2) ON CONFLICT (key) DO UPDATE SET count = CASE WHEN rate_limit.reset_at < $3 THEN 1 ELSE rate_limit.count + 1 END, reset_at = CASE WHEN rate_limit.reset_at < $3 THEN $2 ELSE rate_limit.reset_at END, updated_at = NOW() RETURNING count, reset_at `, [key, resetAt, now]); const { count } = result.rows[0]; return count <= RATE_LIMIT_MAX; } catch (error) { logger.error('Rate limit check failed', { error }); // Fail open - allow request if rate limit check fails return true; } } /** * Cleanup expired rate limit entries (run periodically) */ export async function cleanupRateLimitDistributed(pgClient: Pool): Promise { try { await pgClient.query(` DELETE FROM rate_limit WHERE reset_at < NOW() - INTERVAL '1 hour' `); } catch (error) { logger.error('Rate limit cleanup failed', { error }); } } ``` **3.1.3 - Executar Migration** ```bash psql $DATABASE_URL -f migrations/002_create_rate_limit.sql ``` #### Testes de Verificação ```sql -- Verificar entradas de rate limiting SELECT * FROM rate_limit ORDER BY updated_at DESC LIMIT 10; -- Verificar cleanup SELECT COUNT(*) FROM rate_limit WHERE reset_at < NOW(); ``` --- ### Tarefa 3.2: Melhorar Validações **Objectivo:** Usar biblioteca robusta para validação de emails e adicionar validações de comprimento. **Esforço:** 1-2 dias **Prioridade:** P2 - Médio #### Passos de Implementação **3.2.1 - Instalar Dependência** ```bash npm install validator npm install --save-dev @types/validator ``` **3.2.2 - Actualizar Security.ts** Modificar `src/utils/security.ts`: ```typescript import validator from 'validator'; /** * Validate email format (improved) */ export function isValidEmail(email: string): boolean { return validator.isEmail(email, { allow_utf8_local_part: false, require_tld: true }); } /** * Validate and sanitize string input */ export function validateString( input: string, fieldName: string, minLength: number = 1, maxLength: number = 1000 ): string { if (typeof input !== 'string') { throw new ValidationError(`${fieldName} must be a string`); } const sanitized = sanitizeInput(input); if (sanitized.length < minLength) { throw new ValidationError(`${fieldName} must be at least ${minLength} characters`); } if (sanitized.length > maxLength) { throw new ValidationError(`${fieldName} must be at most ${maxLength} characters`); } return sanitized; } /** * Enhanced sanitizeInput */ export function sanitizeInput(input: string): string { if (typeof input !== 'string') return input; // Remove null bytes let sanitized = input.replace(/\0/g, ''); // Trim whitespace sanitized = sanitized.trim(); // Remove control characters (except newlines and tabs) sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); return sanitized; } ``` **3.2.3 - Actualizar Tools** ```typescript // Exemplo em users.ts const createUser: BaseTool = { handler: async (args, pgClient, context) => { const name = validateString(args.name, 'name', 2, 100); const email = validateString(args.email, 'email', 5, 255); if (!isValidEmail(email)) { throw new ValidationError('Invalid email format'); } // ... resto da lógica } }; ``` --- ## Fase 4: P3 - Baixo (1-2 dias) ### Tarefa 4.1: Automatizar Updates de Dependências **Objectivo:** Configurar Renovate para automatizar verificação de updates. **Esforço:** 1 dia **Prioridade:** P3 - Baixo #### Ficheiros a Criar **Novos Ficheiros:** - [NEW] [renovate.json](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/renovate.json) #### Passos de Implementação **4.1.1 - Criar Configuração Renovate** Criar `renovate.json`: ```json { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["config:base"], "packageRules": [ { "matchUpdateTypes": ["minor", "patch"], "automerge": true }, { "matchUpdateTypes": ["major"], "automerge": false, "labels": ["dependencies", "major-update"] }, { "matchPackagePatterns": ["*"], "matchUpdateTypes": ["patch"], "schedule": ["before 3am on Monday"] } ], "vulnerabilityAlerts": { "enabled": true, "labels": ["security"] } } ``` **4.1.2 - Activar Renovate** 1. Ir a https://github.com/apps/renovate 2. Instalar no repositório 3. Verificar primeiro PR de configuração --- ### Tarefa 4.2: Documentação de Segurança **Objectivo:** Criar guia de deployment seguro. **Esforço:** 2 dias **Prioridade:** P3 - Baixo #### Ficheiros a Criar **Novos Ficheiros:** - [NEW] [SECURITY.md](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/SECURITY.md) - [NEW] [docs/deployment-guide.md](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/docs/deployment-guide.md) #### Conteúdo de SECURITY.md ```markdown # Security Policy ## Reporting a Vulnerability If you discover a security vulnerability, please email security@descomplicar.pt. ## Supported Versions | Version | Supported | | ------- | ------------------ | | 1.3.x | :white_check_mark: | | 1.2.x | :white_check_mark: | | < 1.2 | :x: | ## Security Features - SQL Injection Prevention: All queries use parameterized statements - Authentication & Authorization: Role-based access control - Audit Logging: All sensitive operations logged - Rate Limiting: Distributed rate limiting via PostgreSQL - Input Validation: Comprehensive validation of all inputs ## Security Best Practices 1. **Environment Variables**: Never commit `.env` files 2. **Database Access**: Use least-privilege PostgreSQL user 3. **Network**: Run MCP server in private network only 4. **Logging**: Enable audit logging in production 5. **Updates**: Keep dependencies up to date ``` --- ## Plano de Verificação Final ### Checklist de Implementação **Fase 1 - P0:** - [ ] Sistema de autenticação/autorização implementado - [ ] Contexto de utilizador em todas as tools - [ ] Verificação de permissões funcional - [ ] Tabela `audit_log` criada - [ ] Audit logging em operações sensíveis - [ ] Testes de autenticação passam - [ ] Testes de audit log passam **Fase 2 - P1:** - [ ] Query logging activado - [ ] Queries lentas detectadas - [ ] Error handler implementado - [ ] Mensagens de erro sanitizadas - [ ] Testes de error handling passam **Fase 3 - P2:** - [ ] Rate limiting distribuído implementado - [ ] Validações melhoradas - [ ] Biblioteca `validator` integrada - [ ] Testes de rate limiting passam **Fase 4 - P3:** - [ ] Renovate configurado - [ ] Documentação de segurança criada - [ ] Guia de deployment criado ### Testes de Segurança Finais ```bash # 1. Testar autenticação npm run test:auth # 2. Testar autorização npm run test:permissions # 3. Testar audit log npm run test:audit # 4. Testar rate limiting npm run test:rate-limit # 5. Testar validações npm run test:validation # 6. Verificar dependências npm audit # 7. Build de produção npm run build # 8. Smoke tests npm run test:smoke ``` ### Métricas de Sucesso | Métrica | Antes (v1.2.2) | Depois (v1.3.0) | Alvo | |---------|----------------|-----------------|------| | Score de Segurança | 8.5/10 | 9.5/10 | ≥ 9.0 | | Vulnerabilidades P0 | 0 | 0 | 0 | | Vulnerabilidades P1 | 3 | 0 | 0 | | Cobertura de Audit Log | 0% | 100% | 100% | | Operações com Autenticação | 0% | 100% | 100% | | Testes de Segurança | 0 | 50+ | ≥ 30 | --- ## Cronograma | Semana | Tarefas | Esforço | |--------|---------|---------| | 1 | Tarefa 1.1 (Autenticação) | 3-5 dias | | 1-2 | Tarefa 1.2 (Audit Log) | 2-3 dias | | 2 | Tarefa 2.1 (Query Logging) | 1 dia | | 2 | Tarefa 2.2 (Error Handling) | 2 dias | | 3 | Tarefa 3.1 (Rate Limiting) | 2-3 dias | | 3 | Tarefa 3.2 (Validações) | 1-2 dias | | 4 | Tarefa 4.1 (Renovate) | 1 dia | | 4 | Tarefa 4.2 (Documentação) | 2 dias | | **Total** | **8 tarefas** | **10-15 dias** | --- ## Próximos Passos 1. **Aprovar este plano** ✅ 2. **Criar branch `feature/security-improvements`** 3. **Implementar Fase 1 (P0)** 4. **Code review + testes** 5. **Implementar Fase 2 (P1)** 6. **Code review + testes** 7. **Implementar Fase 3 (P2)** 8. **Implementar Fase 4 (P3)** 9. **Testes finais de segurança** 10. **Merge para `main` e release v1.3.0** --- **Plano criado por:** Antigravity AI **Data:** 2026-01-31 **Versão:** 1.0