@@ -4,43 +4,253 @@ import { Tool } from "@modelcontextprotocol/sdk/types.js";
44export 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 * ( D E L E T E | D R O P | U P D A T E | I N S E R T | A L T E R | C R E A T E | T R U N C A T E | E X E C | E X E C U T E | M E R G E | R E P L A C E | G R A N T | R E V O K E ) / i,
34+
35+ // UNION injection attempts with dangerous keywords
36+ / U N I O N \s + (?: A L L \s + ) ? S E L E C T .* ?( D E L E T E | D R O P | U P D A T E | I N S E R T | A L T E R | C R E A T E | T R U N C A T E | E X E C | E X E C U T E ) / i,
37+
38+ // Comment-based injection attempts
39+ / - - .* ?( D E L E T E | D R O P | U P D A T E | I N S E R T | A L T E R | C R E A T E | T R U N C A T E | E X E C | E X E C U T E ) / i,
40+ / \/ \* .* ?( D E L E T E | D R O P | U P D A T E | I N S E R T | A L T E R | C R E A T E | T R U N C A T E | E X E C | E X E C U T E ) .* ?\* \/ / i,
41+
42+ // Stored procedure execution patterns
43+ / E X E C \s * \( / i,
44+ / E X E C U T E \s * \( / i,
45+ / s p _ / i,
46+ / x p _ / i,
47+
48+ // Dynamic SQL construction
49+ / E X E C \s * \( / i,
50+ / E X E C U T E \s * \( / i,
51+
52+ // Bulk operations
53+ / B U L K \s + I N S E R T / i,
54+ / O P E N R O W S E T / i,
55+ / O P E N D A T A S O U R C E / i,
56+
57+ // System functions that could be dangerous
58+ / @ @ / ,
59+ / S Y S T E M _ U S E R / i,
60+ / U S E R _ N A M E / i,
61+ / D B _ N A M E / i,
62+ / H O S T _ N A M E / i,
63+
64+ // Time delay attacks
65+ / W A I T F O R \s + D E L A Y / i,
66+ / W A I T F O R \s + T I M E / i,
67+
68+ // Multiple statements (semicolon not at end)
69+ / ; \s * \w / ,
70+
71+ // String concatenation that might hide malicious code
72+ / \+ \s * C H A R \s * \( / i,
73+ / \+ \s * N C H A R \s * \( / i,
74+ / \+ \s * A S C I I \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