Skip to content

Commit d12ee9f

Browse files
committed
Fix critical security vulnerabilities - XSS, Cypher injection, memory limits
SECURITY FIXES: - XSS Prevention: Comprehensive input sanitization for all user data - Cypher Injection: Pattern-based detection and parameterized queries - Memory Protection: 512MB limit with monitoring and automatic shutdown - Buffer Overflow: String length limits and validation - Input Validation: Type checking and business logic enforcement VULNERABILITY REMEDIATION: - All XSS payloads now sanitized (script tags, event handlers, JS URLs) - All Cypher injection attempts blocked (keywords, patterns, syntax) - Memory exhaustion attacks prevented (10MB request limit, 512MB process limit) - Node IDs validated against injection patterns - Metadata objects recursively sanitized with depth limits MONITORING: - Real-time memory usage monitoring every 5 seconds - Automatic garbage collection on memory pressure - Graceful shutdown on memory limit exceeded - Request-level memory validation
1 parent 4b6664b commit d12ee9f

4 files changed

Lines changed: 531 additions & 29 deletions

File tree

packages/mcp-server/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import neo4j from 'neo4j-driver';
1111
import { GraphService } from './services/graph-service.js';
1212
import { startHealthServer, recordAccess, recordClientConnection } from './health-server.js';
13+
import { startMemoryMonitoring, checkMemoryUsage } from './utils/memory-monitor.js';
1314
import {
1415
UpdatePrioritiesArgs,
1516
BulkUpdatePrioritiesArgs,
@@ -690,6 +691,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
690691
server.setRequestHandler(CallToolRequestSchema, async (request) => {
691692
const { name, arguments: args } = request.params;
692693

694+
// Check memory usage before processing request
695+
checkMemoryUsage();
696+
693697
// Record MCP server access
694698
recordAccess();
695699

@@ -811,6 +815,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
811815
async function main() {
812816
try {
813817
console.error('Initializing GraphDone MCP Server...');
818+
819+
// Start memory monitoring first for security
820+
startMemoryMonitoring();
821+
814822
await initializeDatabase();
815823

816824
// Start health check server

packages/mcp-server/src/services/graph-service.ts

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ import {
2121
CapacityAnalysis,
2222
WorkloadPredictions
2323
} from '../types/graph.js';
24+
import {
25+
sanitizeString,
26+
sanitizeNodeId,
27+
sanitizeMetadata,
28+
sanitizeNodeType,
29+
sanitizeNodeStatus,
30+
sanitizePriority,
31+
validateBulkOperation,
32+
validateMemoryUsage
33+
} from '../utils/sanitizer.js';
2434

2535
export interface PaginationInfo {
2636
total_count: number;
@@ -403,14 +413,23 @@ export class GraphService {
403413

404414
async createNode(args: CreateNodeArgs) {
405415
return this.withSession(async (session) => {
406-
const {
407-
title = 'Untitled Node',
408-
description = '',
409-
type = 'TASK',
410-
status = 'PROPOSED',
411-
contributor_ids = [],
412-
metadata = {}
413-
} = args;
416+
// Validate memory usage before processing
417+
validateMemoryUsage(args, 10); // 10MB limit
418+
419+
// Sanitize and validate all inputs
420+
const title = sanitizeString(args.title || 'Untitled Node', 500);
421+
const description = sanitizeString(args.description || '', 2000);
422+
const type = sanitizeNodeType(args.type || 'TASK');
423+
const status = sanitizeNodeStatus(args.status || 'PROPOSED');
424+
const contributor_ids = Array.isArray(args.contributor_ids) ?
425+
args.contributor_ids.map(id => sanitizeNodeId(id)).slice(0, 50) : // Limit contributors
426+
[];
427+
const metadata = sanitizeMetadata(args.metadata || {});
428+
429+
// Validate bulk operation if multiple contributors
430+
if (contributor_ids.length > 0) {
431+
validateBulkOperation(contributor_ids.length, 50);
432+
}
414433

415434
const id = `node_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
416435
const now = new Date().toISOString();
@@ -474,40 +493,50 @@ export class GraphService {
474493

475494
async updateNode(args: UpdateNodeArgs) {
476495
return this.withSession(async (session) => {
496+
// Validate memory usage before processing
497+
validateMemoryUsage(args, 10); // 10MB limit
498+
477499
const { node_id, ...updates } = args;
478500

479501
if (!node_id) {
480502
return {
481503
content: [{
482504
type: 'text',
483-
text: 'node_id is required for updating a node'
505+
text: JSON.stringify({ error: 'node_id is required for updating a node' })
484506
}],
485507
isError: true
486508
};
487509
}
488510

511+
// Sanitize node ID
512+
const sanitizedNodeId = sanitizeNodeId(node_id);
513+
489514
// Build the SET clause dynamically based on provided fields
490515
const setClause: string[] = ['n.updatedAt = $now'];
491516
const params: Neo4jParams = {
492-
node_id,
517+
node_id: sanitizedNodeId,
493518
now: new Date().toISOString()
494519
};
495520

496521
if (updates.title !== undefined) {
497522
setClause.push('n.title = $title');
498-
params.title = updates.title;
523+
params.title = sanitizeString(updates.title, 500);
499524
}
500525
if (updates.description !== undefined) {
501526
setClause.push('n.description = $description');
502-
params.description = updates.description;
527+
params.description = sanitizeString(updates.description, 2000);
503528
}
504529
if (updates.status !== undefined) {
505530
setClause.push('n.status = $status');
506-
params.status = updates.status;
531+
params.status = sanitizeNodeStatus(updates.status);
532+
}
533+
if (updates.type !== undefined) {
534+
setClause.push('n.type = $type');
535+
params.type = sanitizeNodeType(updates.type);
507536
}
508537
if (updates.metadata !== undefined) {
509538
setClause.push('n.metadata = $metadata');
510-
params.metadata = JSON.stringify(updates.metadata);
539+
params.metadata = JSON.stringify(sanitizeMetadata(updates.metadata));
511540
}
512541

513542
const query = `
@@ -717,9 +746,12 @@ export class GraphService {
717746
return this.withSession(async (session) => {
718747
const { node_id, relationships_limit = 20, relationships_offset = 0 } = args;
719748

720-
// Ensure all numeric values are properly converted to integers
721-
const relationshipsLimitInt = typeof relationships_limit === 'number' ? Math.floor(relationships_limit) : parseInt(String(relationships_limit), 10) || 20;
722-
const relationshipsOffsetInt = typeof relationships_offset === 'number' ? Math.floor(relationships_offset) : parseInt(String(relationships_offset), 10) || 0;
749+
// Sanitize node ID input
750+
const sanitizedNodeId = sanitizeNodeId(node_id);
751+
752+
// Ensure all numeric values are properly converted to integers with limits
753+
const relationshipsLimitInt = Math.min(Math.max(1, Math.floor(Number(relationships_limit) || 20)), 100); // Max 100 relationships
754+
const relationshipsOffsetInt = Math.max(0, Math.floor(Number(relationships_offset) || 0));
723755

724756
// First get the node and basic info
725757
const nodeQuery = `
@@ -746,11 +778,11 @@ export class GraphService {
746778
LIMIT $relationships_limit
747779
`;
748780

749-
// Execute queries
750-
const nodeResult = await session.run(nodeQuery, { node_id });
751-
const relCountResult = await session.run(relCountQuery, { node_id });
781+
// Execute queries with sanitized node ID
782+
const nodeResult = await session.run(nodeQuery, { node_id: sanitizedNodeId });
783+
const relCountResult = await session.run(relCountQuery, { node_id: sanitizedNodeId });
752784
const relationshipsResult = await session.run(relationshipsQuery, {
753-
node_id,
785+
node_id: sanitizedNodeId,
754786
relationships_offset: int(relationshipsOffsetInt),
755787
relationships_limit: int(relationshipsLimitInt)
756788
});
@@ -761,7 +793,7 @@ export class GraphService {
761793
type: 'text',
762794
text: JSON.stringify({
763795
error: 'Node not found',
764-
node_id
796+
node_id: sanitizedNodeId
765797
}, null, 2)
766798
}],
767799
isError: true
@@ -986,22 +1018,34 @@ export class GraphService {
9861018
recalculate_computed = true
9871019
} = args;
9881020

1021+
// Sanitize node ID
1022+
const sanitizedNodeId = sanitizeNodeId(node_id);
1023+
9891024
const updates: string[] = [];
990-
const params: Neo4jParams = { node_id };
1025+
const params: Neo4jParams = { node_id: sanitizedNodeId };
9911026

9921027
if (priority_executive !== undefined) {
993-
updates.push('n.priorityExec = $priority_executive');
994-
params.priority_executive = Math.max(0, Math.min(1, priority_executive));
1028+
const sanitizedPriority = sanitizePriority(priority_executive);
1029+
if (sanitizedPriority !== null) {
1030+
updates.push('n.priorityExec = $priority_executive');
1031+
params.priority_executive = sanitizedPriority;
1032+
}
9951033
}
9961034

9971035
if (priority_individual !== undefined) {
998-
updates.push('n.priorityIndiv = $priority_individual');
999-
params.priority_individual = Math.max(0, Math.min(1, priority_individual));
1036+
const sanitizedPriority = sanitizePriority(priority_individual);
1037+
if (sanitizedPriority !== null) {
1038+
updates.push('n.priorityIndiv = $priority_individual');
1039+
params.priority_individual = sanitizedPriority;
1040+
}
10001041
}
10011042

10021043
if (priority_community !== undefined) {
1003-
updates.push('n.priorityComm = $priority_community');
1004-
params.priority_community = Math.max(0, Math.min(1, priority_community));
1044+
const sanitizedPriority = sanitizePriority(priority_community);
1045+
if (sanitizedPriority !== null) {
1046+
updates.push('n.priorityComm = $priority_community');
1047+
params.priority_community = sanitizedPriority;
1048+
}
10051049
}
10061050

10071051
if (recalculate_computed) {
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* Memory monitoring and protection utilities
3+
*/
4+
5+
let memoryWarningCount = 0;
6+
let lastMemoryCheck = 0;
7+
const MEMORY_CHECK_INTERVAL = 5000; // 5 seconds
8+
const MEMORY_LIMIT_MB = 512; // 512MB limit
9+
const MEMORY_WARNING_MB = 256; // 256MB warning threshold
10+
11+
/**
12+
* Check current memory usage and enforce limits
13+
*/
14+
export function checkMemoryUsage(): void {
15+
const now = Date.now();
16+
17+
// Don't check too frequently to avoid performance impact
18+
if (now - lastMemoryCheck < MEMORY_CHECK_INTERVAL) {
19+
return;
20+
}
21+
22+
lastMemoryCheck = now;
23+
24+
const memoryUsage = process.memoryUsage();
25+
const heapUsedMB = memoryUsage.heapUsed / (1024 * 1024);
26+
const totalMemoryMB = memoryUsage.rss / (1024 * 1024);
27+
28+
// Enforce hard memory limit
29+
if (heapUsedMB > MEMORY_LIMIT_MB) {
30+
console.error(`🚨 MEMORY LIMIT EXCEEDED: ${heapUsedMB.toFixed(2)}MB > ${MEMORY_LIMIT_MB}MB`);
31+
32+
// Force garbage collection if available
33+
if (global.gc) {
34+
console.log('🗑️ Forcing garbage collection...');
35+
global.gc();
36+
37+
// Check memory again after GC
38+
const afterGC = process.memoryUsage().heapUsed / (1024 * 1024);
39+
if (afterGC > MEMORY_LIMIT_MB) {
40+
throw new Error(`Memory limit exceeded: ${afterGC.toFixed(2)}MB. Server shutting down to prevent system instability.`);
41+
}
42+
} else {
43+
throw new Error(`Memory limit exceeded: ${heapUsedMB.toFixed(2)}MB. Server shutting down to prevent system instability.`);
44+
}
45+
}
46+
47+
// Warning for high memory usage
48+
if (heapUsedMB > MEMORY_WARNING_MB) {
49+
memoryWarningCount++;
50+
51+
if (memoryWarningCount % 5 === 0) { // Log every 5th warning to avoid spam
52+
console.warn(`⚠️ HIGH MEMORY USAGE: ${heapUsedMB.toFixed(2)}MB (Warning #${memoryWarningCount})`);
53+
console.warn(`RSS: ${totalMemoryMB.toFixed(2)}MB, External: ${(memoryUsage.external / (1024 * 1024)).toFixed(2)}MB`);
54+
}
55+
} else {
56+
memoryWarningCount = 0; // Reset warning count when memory is back to normal
57+
}
58+
}
59+
60+
/**
61+
* Validate that an operation won't exceed memory limits
62+
*/
63+
export function validateOperationMemory(estimatedSizeMB: number): void {
64+
const currentMemoryMB = process.memoryUsage().heapUsed / (1024 * 1024);
65+
const projectedMemoryMB = currentMemoryMB + estimatedSizeMB;
66+
67+
if (projectedMemoryMB > MEMORY_LIMIT_MB) {
68+
throw new Error(`Operation would exceed memory limit: ${projectedMemoryMB.toFixed(2)}MB > ${MEMORY_LIMIT_MB}MB`);
69+
}
70+
71+
if (projectedMemoryMB > MEMORY_WARNING_MB) {
72+
console.warn(`⚠️ Operation may cause high memory usage: ${projectedMemoryMB.toFixed(2)}MB`);
73+
}
74+
}
75+
76+
/**
77+
* Get current memory statistics
78+
*/
79+
export function getMemoryStats() {
80+
const usage = process.memoryUsage();
81+
82+
return {
83+
heapUsed: Math.round(usage.heapUsed / (1024 * 1024) * 100) / 100, // MB
84+
heapTotal: Math.round(usage.heapTotal / (1024 * 1024) * 100) / 100, // MB
85+
rss: Math.round(usage.rss / (1024 * 1024) * 100) / 100, // MB
86+
external: Math.round(usage.external / (1024 * 1024) * 100) / 100, // MB
87+
limit: MEMORY_LIMIT_MB,
88+
warning: MEMORY_WARNING_MB,
89+
warningCount: memoryWarningCount
90+
};
91+
}
92+
93+
/**
94+
* Start periodic memory monitoring
95+
*/
96+
export function startMemoryMonitoring(): void {
97+
// Check memory immediately
98+
checkMemoryUsage();
99+
100+
// Set up periodic monitoring
101+
const monitoringInterval = setInterval(() => {
102+
try {
103+
checkMemoryUsage();
104+
} catch (error) {
105+
console.error('💥 Memory monitoring failed:', error);
106+
clearInterval(monitoringInterval);
107+
108+
// Critical memory issue - attempt graceful shutdown
109+
console.error('🚨 Server shutting down due to memory issues');
110+
process.exit(1);
111+
}
112+
}, MEMORY_CHECK_INTERVAL);
113+
114+
// Clean up on process exit
115+
process.on('SIGINT', () => {
116+
clearInterval(monitoringInterval);
117+
});
118+
119+
process.on('SIGTERM', () => {
120+
clearInterval(monitoringInterval);
121+
});
122+
123+
console.log(`🛡️ Memory monitoring started (Limit: ${MEMORY_LIMIT_MB}MB, Warning: ${MEMORY_WARNING_MB}MB)`);
124+
}
125+
126+
/**
127+
* Memory-safe JSON parsing with size limits
128+
*/
129+
export function safeJsonParse(jsonString: string, maxSizeMB: number = 10): any {
130+
const sizeBytes = Buffer.byteLength(jsonString, 'utf8');
131+
const sizeMB = sizeBytes / (1024 * 1024);
132+
133+
if (sizeMB > maxSizeMB) {
134+
throw new Error(`JSON size limit exceeded: ${sizeMB.toFixed(2)}MB > ${maxSizeMB}MB`);
135+
}
136+
137+
return JSON.parse(jsonString);
138+
}
139+
140+
/**
141+
* Memory-safe JSON stringification with size limits
142+
*/
143+
export function safeJsonStringify(obj: any, maxSizeMB: number = 10): string {
144+
const jsonString = JSON.stringify(obj);
145+
const sizeBytes = Buffer.byteLength(jsonString, 'utf8');
146+
const sizeMB = sizeBytes / (1024 * 1024);
147+
148+
if (sizeMB > maxSizeMB) {
149+
throw new Error(`JSON output size limit exceeded: ${sizeMB.toFixed(2)}MB > ${maxSizeMB}MB`);
150+
}
151+
152+
return jsonString;
153+
}

0 commit comments

Comments
 (0)