Skip to content

Commit c6153bb

Browse files
authored
Merge pull request #49 from AV25242/main
updated tools and readme
2 parents 2bf291d + 6f69ad5 commit c6153bb

File tree

4 files changed

+232
-27
lines changed

4 files changed

+232
-27
lines changed

MssqlMcp/Node/README.md

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,6 @@ This server leverages the Model Context Protocol (MCP), a versatile framework th
4646
npm run build
4747
```
4848

49-
3. **Start the Server**
50-
Navigate to the `dist` folder and start the server:
51-
```bash
52-
npm start
53-
```
54-
55-
4. **Confirmation Message**
56-
You should see the following message:
57-
```
58-
MSSQL Database Server running on stdio
59-
```
60-
6149
## Configuration Setup
6250

6351
### Option 1: VS Code Agent Setup

MssqlMcp/Node/src/tools/CreateIndexTool.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ export class CreateIndexTool implements Tool {
2222
default: false
2323
},
2424
isClustered: {
25-
type: "booleam",
25+
type: "boolean",
2626
description: "Whether the index should be clustered (default: false)",
2727
default: false
2828
},
2929
},
30-
required: ["tableName", "indexName", "columnNames"],
30+
required: ["tableName", "indexName", "columns"],
3131
} as any;
3232

3333
async run(params: any) {

MssqlMcp/Node/src/tools/ListTableTool.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@ export class ListTableTool implements Tool {
88
inputSchema = {
99
type: "object",
1010
properties: {
11-
parameters: { type: "array", description: "Schemas" },
11+
parameters: {
12+
type: "array",
13+
description: "Schemas to filter by (optional)",
14+
items: {
15+
type: "string"
16+
},
17+
minItems: 0
18+
},
1219
},
1320
required: [],
1421
} as any;

MssqlMcp/Node/src/tools/ReadDataTool.ts

Lines changed: 222 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,253 @@ import { Tool } from "@modelcontextprotocol/sdk/types.js";
44
export class ReadDataTool implements Tool {
55
[key: string]: any;
66
name = "read_data";
7-
description = "Executes a SELECT query on an MSSQL Database table. The query must start with SELECT for security.";
7+
description = "Executes a SELECT query on an MSSQL Database table. The query must start with SELECT and cannot contain any destructive SQL operations for security reasons.";
8+
89
inputSchema = {
910
type: "object",
1011
properties: {
1112
query: {
1213
type: "string",
13-
description: "SQL SELECT query to execute (must start with SELECT). Example: SELECT * FROM movies WHERE genre = 'comedy'"
14+
description: "SQL SELECT query to execute (must start with SELECT and cannot contain destructive operations). Example: SELECT * FROM movies WHERE genre = 'comedy'"
1415
},
1516
},
1617
required: ["query"],
1718
} as any;
1819

20+
// List of dangerous SQL keywords that should not be allowed
21+
private static readonly DANGEROUS_KEYWORDS = [
22+
'DELETE', 'DROP', 'UPDATE', 'INSERT', 'ALTER', 'CREATE',
23+
'TRUNCATE', 'EXEC', 'EXECUTE', 'MERGE', 'REPLACE',
24+
'GRANT', 'REVOKE', 'COMMIT', 'ROLLBACK', 'TRANSACTION',
25+
'BEGIN', 'DECLARE', 'SET', 'USE', 'BACKUP',
26+
'RESTORE', 'KILL', 'SHUTDOWN', 'WAITFOR', 'OPENROWSET',
27+
'OPENDATASOURCE', 'OPENQUERY', 'OPENXML', 'BULK'
28+
];
29+
30+
// Regex patterns to detect common SQL injection techniques
31+
private static readonly DANGEROUS_PATTERNS = [
32+
// Semicolon followed by dangerous keywords
33+
/;\s*(DELETE|DROP|UPDATE|INSERT|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE|MERGE|REPLACE|GRANT|REVOKE)/i,
34+
35+
// UNION injection attempts with dangerous keywords
36+
/UNION\s+(?:ALL\s+)?SELECT.*?(DELETE|DROP|UPDATE|INSERT|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE)/i,
37+
38+
// Comment-based injection attempts
39+
/--.*?(DELETE|DROP|UPDATE|INSERT|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE)/i,
40+
/\/\*.*?(DELETE|DROP|UPDATE|INSERT|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE).*?\*\//i,
41+
42+
// Stored procedure execution patterns
43+
/EXEC\s*\(/i,
44+
/EXECUTE\s*\(/i,
45+
/sp_/i,
46+
/xp_/i,
47+
48+
// Dynamic SQL construction
49+
/EXEC\s*\(/i,
50+
/EXECUTE\s*\(/i,
51+
52+
// Bulk operations
53+
/BULK\s+INSERT/i,
54+
/OPENROWSET/i,
55+
/OPENDATASOURCE/i,
56+
57+
// System functions that could be dangerous
58+
/@@/,
59+
/SYSTEM_USER/i,
60+
/USER_NAME/i,
61+
/DB_NAME/i,
62+
/HOST_NAME/i,
63+
64+
// Time delay attacks
65+
/WAITFOR\s+DELAY/i,
66+
/WAITFOR\s+TIME/i,
67+
68+
// Multiple statements (semicolon not at end)
69+
/;\s*\w/,
70+
71+
// String concatenation that might hide malicious code
72+
/\+\s*CHAR\s*\(/i,
73+
/\+\s*NCHAR\s*\(/i,
74+
/\+\s*ASCII\s*\(/i,
75+
];
76+
77+
/**
78+
* Validates the SQL query for security issues
79+
* @param query The SQL query to validate
80+
* @returns Validation result with success flag and error message if invalid
81+
*/
82+
private validateQuery(query: string): { isValid: boolean; error?: string } {
83+
if (!query || typeof query !== 'string') {
84+
return {
85+
isValid: false,
86+
error: 'Query must be a non-empty string'
87+
};
88+
}
89+
90+
// Remove comments and normalize whitespace for analysis
91+
const cleanQuery = query
92+
.replace(/--.*$/gm, '') // Remove line comments
93+
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments
94+
.replace(/\s+/g, ' ') // Normalize whitespace
95+
.trim();
96+
97+
if (!cleanQuery) {
98+
return {
99+
isValid: false,
100+
error: 'Query cannot be empty after removing comments'
101+
};
102+
}
103+
104+
const upperQuery = cleanQuery.toUpperCase();
105+
106+
// Must start with SELECT
107+
if (!upperQuery.startsWith('SELECT')) {
108+
return {
109+
isValid: false,
110+
error: 'Query must start with SELECT for security reasons'
111+
};
112+
}
113+
114+
// Check for dangerous keywords in the cleaned query using word boundaries
115+
for (const keyword of ReadDataTool.DANGEROUS_KEYWORDS) {
116+
// Use word boundary regex to match only complete keywords, not parts of words
117+
const keywordRegex = new RegExp(`(^|\\s|[^A-Za-z0-9_])${keyword}($|\\s|[^A-Za-z0-9_])`, 'i');
118+
if (keywordRegex.test(upperQuery)) {
119+
return {
120+
isValid: false,
121+
error: `Dangerous keyword '${keyword}' detected in query. Only SELECT operations are allowed.`
122+
};
123+
}
124+
}
125+
126+
// Check for dangerous patterns using regex
127+
for (const pattern of ReadDataTool.DANGEROUS_PATTERNS) {
128+
if (pattern.test(query)) {
129+
return {
130+
isValid: false,
131+
error: 'Potentially malicious SQL pattern detected. Only simple SELECT queries are allowed.'
132+
};
133+
}
134+
}
135+
136+
// Additional validation: Check for multiple statements
137+
const statements = cleanQuery.split(';').filter(stmt => stmt.trim().length > 0);
138+
if (statements.length > 1) {
139+
return {
140+
isValid: false,
141+
error: 'Multiple SQL statements are not allowed. Use only a single SELECT statement.'
142+
};
143+
}
144+
145+
// Check for suspicious string patterns that might indicate obfuscation
146+
if (query.includes('CHAR(') || query.includes('NCHAR(') || query.includes('ASCII(')) {
147+
return {
148+
isValid: false,
149+
error: 'Character conversion functions are not allowed as they may be used for obfuscation.'
150+
};
151+
}
152+
153+
// Limit query length to prevent potential DoS
154+
if (query.length > 10000) {
155+
return {
156+
isValid: false,
157+
error: 'Query is too long. Maximum allowed length is 10,000 characters.'
158+
};
159+
}
160+
161+
return { isValid: true };
162+
}
163+
164+
/**
165+
* Sanitizes the query result to prevent any potential security issues
166+
* @param data The query result data
167+
* @returns Sanitized data
168+
*/
169+
private sanitizeResult(data: any[]): any[] {
170+
if (!Array.isArray(data)) {
171+
return [];
172+
}
173+
174+
// Limit the number of returned records to prevent memory issues
175+
const maxRecords = 10000;
176+
if (data.length > maxRecords) {
177+
console.warn(`Query returned ${data.length} records, limiting to ${maxRecords}`);
178+
return data.slice(0, maxRecords);
179+
}
180+
181+
return data.map(record => {
182+
if (typeof record === 'object' && record !== null) {
183+
const sanitized: any = {};
184+
for (const [key, value] of Object.entries(record)) {
185+
// Sanitize column names (remove any suspicious characters)
186+
const sanitizedKey = key.replace(/[^\w\s-_.]/g, '');
187+
if (sanitizedKey !== key) {
188+
console.warn(`Column name sanitized: ${key} -> ${sanitizedKey}`);
189+
}
190+
sanitized[sanitizedKey] = value;
191+
}
192+
return sanitized;
193+
}
194+
return record;
195+
});
196+
}
197+
198+
/**
199+
* Executes the validated SQL query
200+
* @param params Query parameters
201+
* @returns Query execution result
202+
*/
19203
async run(params: any) {
20204
try {
21205
const { query } = params;
22206

23-
// Basic validation: ensure query starts with SELECT (case insensitive)
24-
const trimmedQuery = query.trim();
25-
if (!trimmedQuery.toUpperCase().startsWith('SELECT')) {
26-
throw new Error("Query must start with SELECT for security reasons");
207+
// Validate the query for security issues
208+
const validation = this.validateQuery(query);
209+
if (!validation.isValid) {
210+
console.warn(`Security validation failed for query: ${query.substring(0, 100)}...`);
211+
return {
212+
success: false,
213+
message: `Security validation failed: ${validation.error}`,
214+
error: 'SECURITY_VALIDATION_FAILED'
215+
};
27216
}
28217

218+
// Log the query for audit purposes (in production, consider more secure logging)
219+
console.log(`Executing validated SELECT query: ${query.substring(0, 200)}${query.length > 200 ? '...' : ''}`);
220+
221+
// Execute the query
29222
const request = new sql.Request();
30-
const result = await request.query(trimmedQuery);
223+
const result = await request.query(query);
224+
225+
// Sanitize the result
226+
const sanitizedData = this.sanitizeResult(result.recordset);
31227

32228
return {
33229
success: true,
34-
message: `Query executed successfully. Retrieved ${result.recordset.length} record(s)`,
35-
data: result.recordset,
36-
recordCount: result.recordset.length,
230+
message: `Query executed successfully. Retrieved ${sanitizedData.length} record(s)${
231+
result.recordset.length !== sanitizedData.length
232+
? ` (limited from ${result.recordset.length} total records)`
233+
: ''
234+
}`,
235+
data: sanitizedData,
236+
recordCount: sanitizedData.length,
237+
totalRecords: result.recordset.length
37238
};
239+
38240
} catch (error) {
39241
console.error("Error executing query:", error);
242+
243+
// Don't expose internal error details to prevent information leakage
244+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
245+
const safeErrorMessage = errorMessage.includes('Invalid object name')
246+
? errorMessage
247+
: 'Database query execution failed';
248+
40249
return {
41250
success: false,
42-
message: `Failed to execute query: ${error}`,
251+
message: `Failed to execute query: ${safeErrorMessage}`,
252+
error: 'QUERY_EXECUTION_FAILED'
43253
};
44254
}
45255
}
46-
}
256+
}

0 commit comments

Comments
 (0)