Files
mcp-outline-postgresql/docs/SECURITY-IMPROVEMENTS-PLAN.md
Emanuel Almeida 0329a1179a fix: corrigir bugs críticos de segurança e memory leaks (v1.2.4)
- fix(pagination): SQL injection em cursor pagination - validação de nomes de campos
- fix(transaction): substituir Math.random() por crypto.randomBytes() para jitter
- fix(monitoring): memory leak - adicionar .unref() ao setInterval
- docs: adicionar relatório completo de bugs (BUG-REPORT-2026-01-31.md)
- chore: actualizar versão para 1.2.4
2026-01-31 16:09:25 +00:00

33 KiB

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:

Ficheiros a Modificar:

Passos de Implementação

1.1.1 - Criar Interface de Contexto

Criar src/utils/auth-context.ts:

/**
 * 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<string, unknown>): 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:

/**
 * 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<boolean> {
  // 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<boolean> {
  // 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<boolean> {
  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<string, Action[]> = {
    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<boolean> {
  // 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<void> {
  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:

import { MCPContext } from '../utils/auth-context.js';

export interface BaseTool<T = any> {
  name: string;
  description: string;
  inputSchema: {
    type: 'object';
    properties: Record<string, any>;
    required?: string[];
  };
  // Adicionar contexto ao handler
  handler: (args: T, pgClient: any, context: MCPContext) => Promise<ToolResponse>;
}

1.1.4 - Actualizar Exemplo de Tool

Modificar src/tools/documents.ts (exemplo):

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<ToolResponse> => {
    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:

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

# 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:

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:

-- 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:

/**
 * 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<string, unknown>;
  ipAddress?: string;
  userAgent?: string;
  durationMs?: number;
}

/**
 * Log operation to audit_log table
 */
export async function logAudit(
  pgClient: Pool,
  context: MCPContext,
  entry: Omit<AuditLogEntry, 'userId' | 'userEmail' | 'userRole'>
): Promise<void> {
  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<T>(
  pgClient: Pool,
  context: MCPContext,
  action: string,
  resource: string,
  resourceId: string | undefined,
  operation: () => Promise<T>
): Promise<T> {
  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):

import { withAudit } from '../utils/audit-log.js';

const deleteDocument: BaseTool<{ id: string; permanent?: boolean }> = {
  // ... schema ...
  handler: async (args, pgClient, context): Promise<ToolResponse> => {
    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

# 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

-- 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

Passos de Implementação

2.1.1 - Melhorar Logger

Modificar src/utils/logger.ts:

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:

async query<T extends QueryResultRow = any>(sql: string, params?: any[]): Promise<T[]> {
  const start = Date.now();
  try {
    const result = await this.pool.query<T>(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

# Logging
LOG_LEVEL=info  # error, warn, info, debug
ENABLE_AUDIT_LOG=true
SLOW_QUERY_THRESHOLD_MS=1000

Testes de Verificação

# 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:

Ficheiros a Modificar:

  • [MODIFY] Todos os tools (handlers)

Passos de Implementação

2.2.1 - Criar Error Handler

Criar src/utils/error-handler.ts:

/**
 * 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<string, unknown>
  ) {
    super(message);
    this.name = 'AppError';
  }
}

export class ValidationError extends AppError {
  constructor(message: string, details?: Record<string, unknown>) {
    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<string, unknown> } {
  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):

import { sanitizeError, NotFoundError, ValidationError } from '../utils/error-handler.js';

const deleteDocument: BaseTool<{ id: string; permanent?: boolean }> = {
  // ... schema ...
  handler: async (args, pgClient, context): Promise<ToolResponse> => {
    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

# 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:

Ficheiros a Modificar:

Passos de Implementação

3.1.1 - Criar Tabela de Rate Limiting

Criar migrations/002_create_rate_limit.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:

import { Pool } from 'pg';

/**
 * Check rate limit using PostgreSQL (distributed)
 */
export async function checkRateLimitDistributed(
  pgClient: Pool,
  type: string,
  clientId: string
): Promise<boolean> {
  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<void> {
  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

psql $DATABASE_URL -f migrations/002_create_rate_limit.sql

Testes de Verificação

-- 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

npm install validator
npm install --save-dev @types/validator

3.2.2 - Actualizar Security.ts

Modificar src/utils/security.ts:

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

// Exemplo em users.ts
const createUser: BaseTool<CreateUserArgs> = {
  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:

Passos de Implementação

4.1.1 - Criar Configuração Renovate

Criar renovate.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:

Conteúdo de SECURITY.md

# 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

# 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