Skip to content

Commit 8feaa3a

Browse files
committed
🔒 Add comprehensive security manager for MCP server
- Implemented JWT authentication and API key validation - Added request validation with suspicious pattern detection - Created FEN string validation for chess positions - Added parameter range validation for depth and time limits - Integrated bcrypt password hashing for user authentication - Added protection against code injection and oversized requests
1 parent 351af7e commit 8feaa3a

File tree

1 file changed

+177
-0
lines changed

1 file changed

+177
-0
lines changed

mcp-server/src/security.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/**
2+
* Security Manager for MCP Chess Server
3+
*
4+
* Provides authentication, authorization, and security validation
5+
* for the agentic chess API.
6+
*/
7+
8+
import jwt from 'jsonwebtoken';
9+
import bcrypt from 'bcryptjs';
10+
import { z } from 'zod';
11+
12+
const RequestSchema = z.object({
13+
params: z.object({
14+
name: z.string(),
15+
arguments: z.any(),
16+
}),
17+
meta: z.object({
18+
clientId: z.string().optional(),
19+
apiKey: z.string().optional(),
20+
userAgent: z.string().optional(),
21+
}).optional(),
22+
});
23+
24+
export class SecurityManager {
25+
private readonly jwtSecret: string;
26+
private readonly apiKeys: Set<string>;
27+
private readonly rateLimits: Map<string, number>;
28+
29+
constructor() {
30+
this.jwtSecret = process.env.JWT_SECRET || 'chess-engine-secret-key-change-in-production';
31+
this.apiKeys = new Set([
32+
'chess-api-key-demo', // Demo API key
33+
'satware-ai-internal', // Internal satware AI key
34+
]);
35+
this.rateLimits = new Map();
36+
}
37+
38+
validateRequest(request: any): void {
39+
try {
40+
// Validate request structure
41+
RequestSchema.parse(request);
42+
43+
// Check for suspicious patterns
44+
this.checkSuspiciousPatterns(request);
45+
46+
// Validate tool name
47+
this.validateToolName(request.params.name);
48+
49+
// Validate arguments
50+
this.validateArguments(request.params.arguments);
51+
52+
} catch (error) {
53+
throw new Error(`Security validation failed: ${error.message}`);
54+
}
55+
}
56+
57+
authenticateApiKey(apiKey: string): boolean {
58+
return this.apiKeys.has(apiKey);
59+
}
60+
61+
generateJWT(payload: any): string {
62+
return jwt.sign(payload, this.jwtSecret, { expiresIn: '24h' });
63+
}
64+
65+
verifyJWT(token: string): any {
66+
try {
67+
return jwt.verify(token, this.jwtSecret);
68+
} catch (error) {
69+
throw new Error('Invalid JWT token');
70+
}
71+
}
72+
73+
hashPassword(password: string): string {
74+
return bcrypt.hashSync(password, 10);
75+
}
76+
77+
verifyPassword(password: string, hash: string): boolean {
78+
return bcrypt.compareSync(password, hash);
79+
}
80+
81+
private checkSuspiciousPatterns(request: any): void {
82+
const suspicious = [
83+
'eval(',
84+
'Function(',
85+
'require(',
86+
'import(',
87+
'process.',
88+
'global.',
89+
'__dirname',
90+
'__filename',
91+
'fs.',
92+
'child_process',
93+
];
94+
95+
const requestStr = JSON.stringify(request);
96+
for (const pattern of suspicious) {
97+
if (requestStr.includes(pattern)) {
98+
throw new Error(`Suspicious pattern detected: ${pattern}`);
99+
}
100+
}
101+
}
102+
103+
private validateToolName(toolName: string): void {
104+
const allowedTools = [
105+
'analyze_position',
106+
'generate_moves',
107+
'play_move',
108+
'evaluate_position',
109+
'simulate_game',
110+
'opening_book',
111+
];
112+
113+
if (!allowedTools.includes(toolName)) {
114+
throw new Error(`Unknown tool: ${toolName}`);
115+
}
116+
}
117+
118+
private validateArguments(args: any): void {
119+
if (!args || typeof args !== 'object') {
120+
return; // Allow empty or simple arguments
121+
}
122+
123+
// Check for excessively large arguments
124+
const argsStr = JSON.stringify(args);
125+
if (argsStr.length > 10000) {
126+
throw new Error('Arguments too large');
127+
}
128+
129+
// Validate FEN strings if present
130+
if (args.fen && typeof args.fen === 'string') {
131+
this.validateFEN(args.fen);
132+
}
133+
134+
// Validate numeric ranges
135+
if (args.depth !== undefined) {
136+
if (typeof args.depth !== 'number' || args.depth < 1 || args.depth > 20) {
137+
throw new Error('Invalid depth parameter');
138+
}
139+
}
140+
141+
if (args.time_limit !== undefined) {
142+
if (typeof args.time_limit !== 'number' || args.time_limit < 100 || args.time_limit > 30000) {
143+
throw new Error('Invalid time_limit parameter');
144+
}
145+
}
146+
}
147+
148+
private validateFEN(fen: string): void {
149+
// Basic FEN validation
150+
const parts = fen.split(' ');
151+
if (parts.length < 4 || parts.length > 6) {
152+
throw new Error('Invalid FEN format');
153+
}
154+
155+
// Validate board position
156+
const board = parts[0];
157+
const ranks = board.split('/');
158+
if (ranks.length !== 8) {
159+
throw new Error('Invalid FEN board format');
160+
}
161+
162+
// Validate side to move
163+
if (!['w', 'b'].includes(parts[1])) {
164+
throw new Error('Invalid side to move in FEN');
165+
}
166+
167+
// Basic castling rights validation
168+
if (!/^[KQkq-]+$/.test(parts[2])) {
169+
throw new Error('Invalid castling rights in FEN');
170+
}
171+
172+
// Basic en passant validation
173+
if (!/^([a-h][36]|-)$/.test(parts[3])) {
174+
throw new Error('Invalid en passant square in FEN');
175+
}
176+
}
177+
}

0 commit comments

Comments
 (0)