- 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
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:
- [NEW] auth-context.ts
- [NEW] permissions.ts
Ficheiros a Modificar:
- [MODIFY] types/tools.ts
- [MODIFY] index.ts
- [MODIFY] Todos os 33 módulos em tools/
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:
- [NEW] migrations/001_create_audit_log.sql
- [NEW] 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:
-- 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
- [MODIFY] logger.ts
- [MODIFY] pg-client.ts
- [MODIFY] .env.example
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:
- [NEW] 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:
/**
* 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:
- [MODIFY] security.ts
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:
- [NEW] renovate.json
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
- Ir a https://github.com/apps/renovate
- Instalar no repositório
- 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
- [NEW] docs/deployment-guide.md
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_logcriada - 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
validatorintegrada - 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
- Aprovar este plano ✅
- Criar branch
feature/security-improvements - Implementar Fase 1 (P0)
- Code review + testes
- Implementar Fase 2 (P1)
- Code review + testes
- Implementar Fase 3 (P2)
- Implementar Fase 4 (P3)
- Testes finais de segurança
- Merge para
maine release v1.3.0
Plano criado por: Antigravity AI
Data: 2026-01-31
Versão: 1.0