@berthojoris/mcp-mysql-server 1.18.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/DOCUMENTATIONS.md +2 -0
- package/README.md +3 -11
- package/dist/db/connection.d.ts +22 -0
- package/dist/db/connection.js +118 -15
- package/dist/index.d.ts +1 -3
- package/dist/index.js +2 -2
- package/dist/security/securityLayer.d.ts +4 -0
- package/dist/security/securityLayer.js +6 -0
- package/dist/tools/ddlTools.d.ts +7 -1
- package/dist/tools/ddlTools.js +57 -6
- package/dist/tools/storedProcedureTools.d.ts +4 -0
- package/dist/tools/storedProcedureTools.js +59 -3
- package/dist/tools/transactionTools.d.ts +3 -1
- package/dist/tools/transactionTools.js +21 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,10 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Security
|
|
11
|
+
- Fixed critical SQL execution bypass in transactions by adding comprehensive security validation
|
|
12
|
+
- Enhanced stored procedure creation with body content validation and injection prevention
|
|
13
|
+
- Improved DDL operations with proper default value sanitization to prevent SQL injection
|
|
14
|
+
- Added transaction timeout mechanism (30 minutes) with automatic cleanup to prevent resource exhaustion
|
|
15
|
+
- Integrated security layer across all transaction operations for complete coverage
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Updated TransactionTools to require SecurityLayer for proper validation
|
|
19
|
+
- Enhanced DdlTools with comprehensive input sanitization
|
|
20
|
+
- Improved database connection management with timeout and cleanup features
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- Resolved TypeScript compilation error in SecurityLayer access patterns
|
|
24
|
+
- Eliminated all security bypass paths through the system
|
|
25
|
+
|
|
10
26
|
### Removed
|
|
11
27
|
- Preset-based access control configuration: CLI `--preset` flag and `MCP_PRESET` / `MCP_PERMISSION_PRESET` environment variables. Use `MCP_PERMISSIONS` and optionally `MCP_CATEGORIES`.
|
|
12
28
|
- Global masking configuration via `MCP_MASKING_PROFILE`. If you need enforced masking for exports, use the `safe_export_table` macro's `masking_profile` argument.
|
|
13
29
|
|
|
30
|
+
|
|
31
|
+
## [1.18.2] - 2025-12-13
|
|
32
|
+
|
|
33
|
+
### Removed
|
|
34
|
+
- Removed outdated comparison docs under `docs/comparison` (and the related README section). The canonical documentation is `DOCUMENTATIONS.md`.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## [1.18.1] - 2025-12-13
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- Updated documentation files with current datetime stamps
|
|
41
|
+
|
|
14
42
|
## [1.17.0] - 2025-12-12
|
|
15
43
|
|
|
16
44
|
### Added
|
package/DOCUMENTATIONS.md
CHANGED
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
**A production-ready Model Context Protocol (MCP) server for MySQL database integration with AI agents**
|
|
6
6
|
|
|
7
|
+
**Last Updated:** 2025-12-13T03:05:56.256Z
|
|
8
|
+
|
|
7
9
|
[](https://www.npmjs.com/package/@berthojoris/mysql-mcp)
|
|
8
10
|
[](https://www.npmjs.com/package/@berthojoris/mysql-mcp)
|
|
9
11
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -333,17 +335,7 @@ For comprehensive documentation, see **[DOCUMENTATIONS.md](DOCUMENTATIONS.md)**:
|
|
|
333
335
|
|
|
334
336
|
This MySQL MCP is a **powerful intermediary layer** between AI assistants and MySQL databases.
|
|
335
337
|
|
|
336
|
-
|
|
337
|
-
|------------------|---------------|
|
|
338
|
-
| Data Access & Querying | [docs/comparison/data-access-querying.md](docs/comparison/data-access-querying.md) |
|
|
339
|
-
| Data Analysis | [docs/comparison/data-analysis.md](docs/comparison/data-analysis.md) |
|
|
340
|
-
| Data Validation | [docs/comparison/data-validation.md](docs/comparison/data-validation.md) |
|
|
341
|
-
| Schema Inspection | [docs/comparison/schema-inspection.md](docs/comparison/schema-inspection.md) |
|
|
342
|
-
| Debugging & Diagnostics | [docs/comparison/debugging-diagnostics.md](docs/comparison/debugging-diagnostics.md) |
|
|
343
|
-
| Advanced Operations | [docs/comparison/advanced-operations.md](docs/comparison/advanced-operations.md) |
|
|
344
|
-
| Key Benefits | [docs/comparison/key-benefits.md](docs/comparison/key-benefits.md) |
|
|
345
|
-
| Example Workflows | [docs/comparison/example-workflows.md](docs/comparison/example-workflows.md) |
|
|
346
|
-
| When to Use | [docs/comparison/when-to-use.md](docs/comparison/when-to-use.md) |
|
|
338
|
+
For full feature coverage and usage examples, see **[DOCUMENTATIONS.md](DOCUMENTATIONS.md)**.
|
|
347
339
|
|
|
348
340
|
---
|
|
349
341
|
|
package/dist/db/connection.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ declare class DatabaseConnection {
|
|
|
4
4
|
private pool;
|
|
5
5
|
private activeTransactions;
|
|
6
6
|
private queryCache;
|
|
7
|
+
private readonly TRANSACTION_TIMEOUT_MS;
|
|
7
8
|
private constructor();
|
|
8
9
|
static getInstance(): DatabaseConnection;
|
|
9
10
|
getConnection(): Promise<mysql.PoolConnection>;
|
|
@@ -19,11 +20,32 @@ declare class DatabaseConnection {
|
|
|
19
20
|
errorCode?: string;
|
|
20
21
|
}>;
|
|
21
22
|
closePool(): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Clean up expired transactions to prevent resource exhaustion
|
|
25
|
+
*/
|
|
26
|
+
private cleanupExpiredTransactions;
|
|
27
|
+
/**
|
|
28
|
+
* Force rollback a transaction (used for cleanup)
|
|
29
|
+
*/
|
|
30
|
+
private forceRollbackTransaction;
|
|
31
|
+
/**
|
|
32
|
+
* Reset transaction timeout (call this when there's activity)
|
|
33
|
+
*/
|
|
34
|
+
private resetTransactionTimeout;
|
|
22
35
|
beginTransaction(transactionId: string): Promise<void>;
|
|
23
36
|
commitTransaction(transactionId: string): Promise<void>;
|
|
24
37
|
rollbackTransaction(transactionId: string): Promise<void>;
|
|
25
38
|
getActiveTransactionIds(): string[];
|
|
26
39
|
hasActiveTransaction(transactionId: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Get transaction information for debugging/monitoring
|
|
42
|
+
*/
|
|
43
|
+
getTransactionInfo(transactionId: string): {
|
|
44
|
+
exists: boolean;
|
|
45
|
+
createdAt?: Date;
|
|
46
|
+
lastActivity?: Date;
|
|
47
|
+
ageMs?: number;
|
|
48
|
+
};
|
|
27
49
|
getQueryLogs(): import("./queryLogger").QueryLog[];
|
|
28
50
|
getLastQueryLog(): import("./queryLogger").QueryLog | undefined;
|
|
29
51
|
getFormattedQueryLogs(count?: number): string;
|
package/dist/db/connection.js
CHANGED
|
@@ -9,6 +9,7 @@ const queryLogger_1 = require("./queryLogger");
|
|
|
9
9
|
const queryCache_1 = require("../cache/queryCache");
|
|
10
10
|
class DatabaseConnection {
|
|
11
11
|
constructor() {
|
|
12
|
+
this.TRANSACTION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes default timeout
|
|
12
13
|
this.pool = promise_1.default.createPool({
|
|
13
14
|
host: config_1.dbConfig.host,
|
|
14
15
|
port: config_1.dbConfig.port,
|
|
@@ -21,6 +22,10 @@ class DatabaseConnection {
|
|
|
21
22
|
});
|
|
22
23
|
this.activeTransactions = new Map();
|
|
23
24
|
this.queryCache = queryCache_1.QueryCache.getInstance();
|
|
25
|
+
// Set up periodic cleanup of expired transactions
|
|
26
|
+
setInterval(() => {
|
|
27
|
+
this.cleanupExpiredTransactions();
|
|
28
|
+
}, 5 * 60 * 1000); // Check every 5 minutes
|
|
24
29
|
}
|
|
25
30
|
static getInstance() {
|
|
26
31
|
if (!DatabaseConnection.instance) {
|
|
@@ -110,32 +115,108 @@ class DatabaseConnection {
|
|
|
110
115
|
throw new Error(`Failed to close connection pool: ${error}`);
|
|
111
116
|
}
|
|
112
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Clean up expired transactions to prevent resource exhaustion
|
|
120
|
+
*/
|
|
121
|
+
cleanupExpiredTransactions() {
|
|
122
|
+
const now = new Date();
|
|
123
|
+
const expiredTransactions = [];
|
|
124
|
+
for (const [transactionId, transaction,] of this.activeTransactions.entries()) {
|
|
125
|
+
const timeSinceLastActivity = now.getTime() - transaction.lastActivity.getTime();
|
|
126
|
+
if (timeSinceLastActivity > this.TRANSACTION_TIMEOUT_MS) {
|
|
127
|
+
expiredTransactions.push(transactionId);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const transactionId of expiredTransactions) {
|
|
131
|
+
this.forceRollbackTransaction(transactionId, "Transaction timed out");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Force rollback a transaction (used for cleanup)
|
|
136
|
+
*/
|
|
137
|
+
forceRollbackTransaction(transactionId, reason) {
|
|
138
|
+
const transaction = this.activeTransactions.get(transactionId);
|
|
139
|
+
if (!transaction)
|
|
140
|
+
return;
|
|
141
|
+
try {
|
|
142
|
+
// Clear timeout if exists
|
|
143
|
+
if (transaction.timeout) {
|
|
144
|
+
clearTimeout(transaction.timeout);
|
|
145
|
+
}
|
|
146
|
+
// Attempt to rollback
|
|
147
|
+
transaction.connection.rollback();
|
|
148
|
+
transaction.connection.release();
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
console.error(`Failed to rollback expired transaction ${transactionId}:`, error);
|
|
152
|
+
try {
|
|
153
|
+
// Force release connection even if rollback fails
|
|
154
|
+
transaction.connection.release();
|
|
155
|
+
}
|
|
156
|
+
catch (releaseError) {
|
|
157
|
+
console.error(`Failed to release connection for expired transaction ${transactionId}:`, releaseError);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
this.activeTransactions.delete(transactionId);
|
|
161
|
+
console.warn(`Transaction ${transactionId} force rolled back: ${reason}`);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Reset transaction timeout (call this when there's activity)
|
|
165
|
+
*/
|
|
166
|
+
resetTransactionTimeout(transactionId) {
|
|
167
|
+
const transaction = this.activeTransactions.get(transactionId);
|
|
168
|
+
if (!transaction)
|
|
169
|
+
return;
|
|
170
|
+
// Clear existing timeout
|
|
171
|
+
if (transaction.timeout) {
|
|
172
|
+
clearTimeout(transaction.timeout);
|
|
173
|
+
}
|
|
174
|
+
// Update last activity
|
|
175
|
+
transaction.lastActivity = new Date();
|
|
176
|
+
// Set new timeout
|
|
177
|
+
transaction.timeout = setTimeout(() => {
|
|
178
|
+
this.forceRollbackTransaction(transactionId, "Transaction timeout");
|
|
179
|
+
}, this.TRANSACTION_TIMEOUT_MS);
|
|
180
|
+
}
|
|
113
181
|
// Transaction Management Methods
|
|
114
182
|
async beginTransaction(transactionId) {
|
|
115
183
|
try {
|
|
116
184
|
const connection = await this.getConnection();
|
|
117
185
|
await connection.beginTransaction();
|
|
118
|
-
|
|
186
|
+
const now = new Date();
|
|
187
|
+
const timeout = setTimeout(() => {
|
|
188
|
+
this.forceRollbackTransaction(transactionId, "Transaction timeout");
|
|
189
|
+
}, this.TRANSACTION_TIMEOUT_MS);
|
|
190
|
+
this.activeTransactions.set(transactionId, {
|
|
191
|
+
connection,
|
|
192
|
+
createdAt: now,
|
|
193
|
+
lastActivity: now,
|
|
194
|
+
timeout,
|
|
195
|
+
});
|
|
119
196
|
}
|
|
120
197
|
catch (error) {
|
|
121
198
|
throw new Error(`Failed to begin transaction: ${error}`);
|
|
122
199
|
}
|
|
123
200
|
}
|
|
124
201
|
async commitTransaction(transactionId) {
|
|
125
|
-
const
|
|
126
|
-
if (!
|
|
202
|
+
const transaction = this.activeTransactions.get(transactionId);
|
|
203
|
+
if (!transaction) {
|
|
127
204
|
throw new Error(`No active transaction found with ID: ${transactionId}`);
|
|
128
205
|
}
|
|
129
206
|
try {
|
|
130
|
-
|
|
131
|
-
|
|
207
|
+
// Clear timeout
|
|
208
|
+
if (transaction.timeout) {
|
|
209
|
+
clearTimeout(transaction.timeout);
|
|
210
|
+
}
|
|
211
|
+
await transaction.connection.commit();
|
|
212
|
+
transaction.connection.release();
|
|
132
213
|
this.activeTransactions.delete(transactionId);
|
|
133
214
|
}
|
|
134
215
|
catch (error) {
|
|
135
216
|
// If commit fails, rollback and release connection
|
|
136
217
|
try {
|
|
137
|
-
await connection.rollback();
|
|
138
|
-
connection.release();
|
|
218
|
+
await transaction.connection.rollback();
|
|
219
|
+
transaction.connection.release();
|
|
139
220
|
}
|
|
140
221
|
catch (rollbackError) {
|
|
141
222
|
console.error("Failed to rollback after commit error:", rollbackError);
|
|
@@ -145,17 +226,21 @@ class DatabaseConnection {
|
|
|
145
226
|
}
|
|
146
227
|
}
|
|
147
228
|
async rollbackTransaction(transactionId) {
|
|
148
|
-
const
|
|
149
|
-
if (!
|
|
229
|
+
const transaction = this.activeTransactions.get(transactionId);
|
|
230
|
+
if (!transaction) {
|
|
150
231
|
throw new Error(`No active transaction found with ID: ${transactionId}`);
|
|
151
232
|
}
|
|
152
233
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
234
|
+
// Clear timeout
|
|
235
|
+
if (transaction.timeout) {
|
|
236
|
+
clearTimeout(transaction.timeout);
|
|
237
|
+
}
|
|
238
|
+
await transaction.connection.rollback();
|
|
239
|
+
transaction.connection.release();
|
|
155
240
|
this.activeTransactions.delete(transactionId);
|
|
156
241
|
}
|
|
157
242
|
catch (error) {
|
|
158
|
-
connection.release();
|
|
243
|
+
transaction.connection.release();
|
|
159
244
|
this.activeTransactions.delete(transactionId);
|
|
160
245
|
throw new Error(`Failed to rollback transaction: ${error}`);
|
|
161
246
|
}
|
|
@@ -166,6 +251,22 @@ class DatabaseConnection {
|
|
|
166
251
|
hasActiveTransaction(transactionId) {
|
|
167
252
|
return this.activeTransactions.has(transactionId);
|
|
168
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Get transaction information for debugging/monitoring
|
|
256
|
+
*/
|
|
257
|
+
getTransactionInfo(transactionId) {
|
|
258
|
+
const transaction = this.activeTransactions.get(transactionId);
|
|
259
|
+
if (!transaction) {
|
|
260
|
+
return { exists: false };
|
|
261
|
+
}
|
|
262
|
+
const now = new Date();
|
|
263
|
+
return {
|
|
264
|
+
exists: true,
|
|
265
|
+
createdAt: transaction.createdAt,
|
|
266
|
+
lastActivity: transaction.lastActivity,
|
|
267
|
+
ageMs: now.getTime() - transaction.createdAt.getTime(),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
169
270
|
getQueryLogs() {
|
|
170
271
|
return queryLogger_1.QueryLogger.getLogs();
|
|
171
272
|
}
|
|
@@ -206,15 +307,17 @@ class DatabaseConnection {
|
|
|
206
307
|
this.queryCache.resetStats();
|
|
207
308
|
}
|
|
208
309
|
async executeInTransaction(transactionId, sql, params) {
|
|
209
|
-
const
|
|
210
|
-
if (!
|
|
310
|
+
const transaction = this.activeTransactions.get(transactionId);
|
|
311
|
+
if (!transaction) {
|
|
211
312
|
throw new Error(`No active transaction found with ID: ${transactionId}`);
|
|
212
313
|
}
|
|
213
314
|
const startTime = Date.now();
|
|
214
315
|
try {
|
|
215
|
-
const [results] = await connection.query(sql, params);
|
|
316
|
+
const [results] = await transaction.connection.query(sql, params);
|
|
216
317
|
const duration = Date.now() - startTime;
|
|
217
318
|
queryLogger_1.QueryLogger.log(sql, params, duration, "success");
|
|
319
|
+
// Reset timeout on successful activity
|
|
320
|
+
this.resetTransactionTimeout(transactionId);
|
|
218
321
|
return results;
|
|
219
322
|
}
|
|
220
323
|
catch (error) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1309,9 +1309,7 @@ export declare class MySQLMCP {
|
|
|
1309
1309
|
column_name: string;
|
|
1310
1310
|
pattern_type: string;
|
|
1311
1311
|
description: string;
|
|
1312
|
-
metrics
|
|
1313
|
-
* Get current schema version
|
|
1314
|
-
*/: Record<string, any>;
|
|
1312
|
+
metrics?: Record<string, any>;
|
|
1315
1313
|
recommendations?: string[];
|
|
1316
1314
|
}>;
|
|
1317
1315
|
summary: {
|
package/dist/index.js
CHANGED
|
@@ -51,8 +51,8 @@ class MySQLMCP {
|
|
|
51
51
|
this.crudTools = new crudTools_1.CrudTools(this.security);
|
|
52
52
|
this.queryTools = new queryTools_1.QueryTools(this.security);
|
|
53
53
|
this.utilityTools = new utilityTools_1.UtilityTools();
|
|
54
|
-
this.ddlTools = new ddlTools_1.DdlTools();
|
|
55
|
-
this.transactionTools = new transactionTools_1.TransactionTools();
|
|
54
|
+
this.ddlTools = new ddlTools_1.DdlTools(this.security);
|
|
55
|
+
this.transactionTools = new transactionTools_1.TransactionTools(this.security);
|
|
56
56
|
this.storedProcedureTools = new storedProcedureTools_1.StoredProcedureTools(this.security);
|
|
57
57
|
this.dataExportTools = new dataExportTools_1.DataExportTools(this.security);
|
|
58
58
|
this.viewTools = new viewTools_1.ViewTools(this.security);
|
|
@@ -8,6 +8,10 @@ export declare class SecurityLayer {
|
|
|
8
8
|
private featureConfig;
|
|
9
9
|
masking: MaskingLayer;
|
|
10
10
|
constructor(featureConfig?: FeatureConfig);
|
|
11
|
+
/**
|
|
12
|
+
* Check if a specific tool is enabled in the feature configuration
|
|
13
|
+
*/
|
|
14
|
+
isToolEnabled(toolName: string): boolean;
|
|
11
15
|
/**
|
|
12
16
|
* Check if a query is a read-only information query (SHOW, DESCRIBE, EXPLAIN, etc.)
|
|
13
17
|
*/
|
|
@@ -37,6 +37,12 @@ class SecurityLayer {
|
|
|
37
37
|
// Define DDL operations that require special permission
|
|
38
38
|
this.ddlOperations = ["CREATE", "ALTER", "DROP", "TRUNCATE", "RENAME"];
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Check if a specific tool is enabled in the feature configuration
|
|
42
|
+
*/
|
|
43
|
+
isToolEnabled(toolName) {
|
|
44
|
+
return this.featureConfig.isToolEnabled(toolName);
|
|
45
|
+
}
|
|
40
46
|
/**
|
|
41
47
|
* Check if a query is a read-only information query (SHOW, DESCRIBE, EXPLAIN, etc.)
|
|
42
48
|
*/
|
package/dist/tools/ddlTools.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { SecurityLayer } from "../security/securityLayer";
|
|
1
2
|
export declare class DdlTools {
|
|
2
3
|
private db;
|
|
3
|
-
|
|
4
|
+
private security;
|
|
5
|
+
constructor(security: SecurityLayer);
|
|
6
|
+
/**
|
|
7
|
+
* Sanitize default value for SQL safety
|
|
8
|
+
*/
|
|
9
|
+
private sanitizeDefaultValue;
|
|
4
10
|
/**
|
|
5
11
|
* Create a new table
|
|
6
12
|
*/
|
package/dist/tools/ddlTools.js
CHANGED
|
@@ -6,8 +6,51 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.DdlTools = void 0;
|
|
7
7
|
const connection_1 = __importDefault(require("../db/connection"));
|
|
8
8
|
class DdlTools {
|
|
9
|
-
constructor() {
|
|
9
|
+
constructor(security) {
|
|
10
10
|
this.db = connection_1.default.getInstance();
|
|
11
|
+
this.security = security;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Sanitize default value for SQL safety
|
|
15
|
+
*/
|
|
16
|
+
sanitizeDefaultValue(defaultValue) {
|
|
17
|
+
if (defaultValue === null || defaultValue === undefined) {
|
|
18
|
+
return "NULL";
|
|
19
|
+
}
|
|
20
|
+
if (typeof defaultValue === "number") {
|
|
21
|
+
return String(defaultValue);
|
|
22
|
+
}
|
|
23
|
+
if (typeof defaultValue === "boolean") {
|
|
24
|
+
return defaultValue ? "1" : "0";
|
|
25
|
+
}
|
|
26
|
+
if (typeof defaultValue === "string") {
|
|
27
|
+
// Check for dangerous SQL patterns in default values
|
|
28
|
+
const dangerousPatterns = [
|
|
29
|
+
/;/g, // Statement separators
|
|
30
|
+
/--/g, // SQL comments
|
|
31
|
+
/\/\*/g, // Block comment start
|
|
32
|
+
/\*\//g, // Block comment end
|
|
33
|
+
/\bUNION\b/gi, // UNION operations
|
|
34
|
+
/\bSELECT\b/gi, // SELECT statements
|
|
35
|
+
/\bINSERT\b/gi, // INSERT statements
|
|
36
|
+
/\bUPDATE\b/gi, // UPDATE statements
|
|
37
|
+
/\bDELETE\b/gi, // DELETE statements
|
|
38
|
+
/\bDROP\b/gi, // DROP statements
|
|
39
|
+
/\bCREATE\b/gi, // CREATE statements
|
|
40
|
+
/\bALTER\b/gi, // ALTER statements
|
|
41
|
+
];
|
|
42
|
+
let sanitized = defaultValue;
|
|
43
|
+
for (const pattern of dangerousPatterns) {
|
|
44
|
+
if (pattern.test(sanitized)) {
|
|
45
|
+
throw new Error(`Dangerous SQL pattern detected in default value: ${pattern.source}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Escape single quotes and backslashes
|
|
49
|
+
sanitized = sanitized.replace(/\\/g, "\\\\").replace(/'/g, "''");
|
|
50
|
+
return `'${sanitized}'`;
|
|
51
|
+
}
|
|
52
|
+
// For other types, convert to string and escape
|
|
53
|
+
return `'${String(defaultValue).replace(/\\/g, "\\\\").replace(/'/g, "''")}'`;
|
|
11
54
|
}
|
|
12
55
|
/**
|
|
13
56
|
* Create a new table
|
|
@@ -26,7 +69,9 @@ class DdlTools {
|
|
|
26
69
|
def += " AUTO_INCREMENT";
|
|
27
70
|
}
|
|
28
71
|
if (col.default !== undefined) {
|
|
29
|
-
|
|
72
|
+
// SECURITY: Properly sanitize default values to prevent SQL injection
|
|
73
|
+
const sanitizedDefault = this.sanitizeDefaultValue(col.default);
|
|
74
|
+
def += ` DEFAULT ${sanitizedDefault}`;
|
|
30
75
|
}
|
|
31
76
|
if (col.primary_key) {
|
|
32
77
|
def += " PRIMARY KEY";
|
|
@@ -83,8 +128,11 @@ class DdlTools {
|
|
|
83
128
|
query += ` ADD COLUMN \`${op.column_name}\` ${op.column_type}`;
|
|
84
129
|
if (op.nullable === false)
|
|
85
130
|
query += " NOT NULL";
|
|
86
|
-
if (op.default !== undefined)
|
|
87
|
-
|
|
131
|
+
if (op.default !== undefined) {
|
|
132
|
+
// SECURITY: Properly sanitize default values to prevent SQL injection
|
|
133
|
+
const sanitizedDefault = this.sanitizeDefaultValue(op.default);
|
|
134
|
+
query += ` DEFAULT ${sanitizedDefault}`;
|
|
135
|
+
}
|
|
88
136
|
break;
|
|
89
137
|
case "drop_column":
|
|
90
138
|
if (!op.column_name) {
|
|
@@ -105,8 +153,11 @@ class DdlTools {
|
|
|
105
153
|
query += ` MODIFY COLUMN \`${op.column_name}\` ${op.column_type}`;
|
|
106
154
|
if (op.nullable === false)
|
|
107
155
|
query += " NOT NULL";
|
|
108
|
-
if (op.default !== undefined)
|
|
109
|
-
|
|
156
|
+
if (op.default !== undefined) {
|
|
157
|
+
// SECURITY: Properly sanitize default values to prevent SQL injection
|
|
158
|
+
const sanitizedDefault = this.sanitizeDefaultValue(op.default);
|
|
159
|
+
query += ` DEFAULT ${sanitizedDefault}`;
|
|
160
|
+
}
|
|
110
161
|
break;
|
|
111
162
|
case "rename_column":
|
|
112
163
|
if (!op.column_name || !op.new_column_name || !op.column_type) {
|
|
@@ -294,6 +294,51 @@ class StoredProcedureTools {
|
|
|
294
294
|
};
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Validate stored procedure body content for security
|
|
299
|
+
*/
|
|
300
|
+
validateProcedureBody(body) {
|
|
301
|
+
if (!body || typeof body !== "string") {
|
|
302
|
+
return {
|
|
303
|
+
valid: false,
|
|
304
|
+
error: "Procedure body must be a non-empty string",
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const trimmedBody = body.trim();
|
|
308
|
+
// Check for dangerous SQL patterns in procedure body
|
|
309
|
+
const dangerousPatterns = [
|
|
310
|
+
/\bGRANT\b/i,
|
|
311
|
+
/\bREVOKE\b/i,
|
|
312
|
+
/\bDROP\s+USER\b/i,
|
|
313
|
+
/\bCREATE\s+USER\b/i,
|
|
314
|
+
/\bALTER\s+USER\b/i,
|
|
315
|
+
/\bSET\s+PASSWORD\b/i,
|
|
316
|
+
/\bINTO\s+OUTFILE\b/i,
|
|
317
|
+
/\bINTO\s+DUMPFILE\b/i,
|
|
318
|
+
/\bLOAD\s+DATA\b/i,
|
|
319
|
+
/\bLOAD_FILE\s*\(/i,
|
|
320
|
+
/\bSYSTEM\s*\(/i,
|
|
321
|
+
/\bEXEC\s*\(/i,
|
|
322
|
+
/\bEVAL\s*\(/i,
|
|
323
|
+
];
|
|
324
|
+
for (const pattern of dangerousPatterns) {
|
|
325
|
+
if (pattern.test(trimmedBody)) {
|
|
326
|
+
return {
|
|
327
|
+
valid: false,
|
|
328
|
+
error: `Dangerous SQL pattern detected: ${pattern.source}`,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Check for multiple statement separators that might indicate injection attempts
|
|
333
|
+
const semicolonCount = (trimmedBody.match(/;/g) || []).length;
|
|
334
|
+
if (semicolonCount > 50) {
|
|
335
|
+
return {
|
|
336
|
+
valid: false,
|
|
337
|
+
error: "Too many statements in procedure body",
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return { valid: true };
|
|
341
|
+
}
|
|
297
342
|
/**
|
|
298
343
|
* Create a new stored procedure
|
|
299
344
|
*/
|
|
@@ -325,6 +370,18 @@ class StoredProcedureTools {
|
|
|
325
370
|
error: identifierValidation.error || "Invalid procedure name",
|
|
326
371
|
};
|
|
327
372
|
}
|
|
373
|
+
// SECURITY VALIDATION: Validate procedure body content
|
|
374
|
+
const bodyValidation = this.validateProcedureBody(body);
|
|
375
|
+
if (!bodyValidation.valid) {
|
|
376
|
+
return {
|
|
377
|
+
status: "error",
|
|
378
|
+
error: bodyValidation.error || "Invalid procedure body",
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
// Sanitize comment to prevent SQL injection
|
|
382
|
+
const sanitizedComment = comment
|
|
383
|
+
? comment.replace(/'/g, "''").replace(/\\/g, "\\\\")
|
|
384
|
+
: "";
|
|
328
385
|
// Build parameter list
|
|
329
386
|
const parameterList = parameters
|
|
330
387
|
.map((param) => {
|
|
@@ -336,9 +393,8 @@ class StoredProcedureTools {
|
|
|
336
393
|
.join(", ");
|
|
337
394
|
// Build CREATE PROCEDURE statement
|
|
338
395
|
let createQuery = `CREATE PROCEDURE \`${database}\`.\`${procedure_name}\`(${parameterList})\n`;
|
|
339
|
-
if (
|
|
340
|
-
createQuery += `COMMENT '${
|
|
341
|
-
`;
|
|
396
|
+
if (sanitizedComment) {
|
|
397
|
+
createQuery += `COMMENT '${sanitizedComment}'\n`;
|
|
342
398
|
}
|
|
343
399
|
// Check if body already contains BEGIN/END, if not add them
|
|
344
400
|
const trimmedBody = body.trim();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SecurityLayer } from "../security/securityLayer";
|
|
1
2
|
export interface TransactionResult {
|
|
2
3
|
status: "success" | "error";
|
|
3
4
|
transactionId?: string;
|
|
@@ -7,7 +8,8 @@ export interface TransactionResult {
|
|
|
7
8
|
}
|
|
8
9
|
export declare class TransactionTools {
|
|
9
10
|
private db;
|
|
10
|
-
|
|
11
|
+
private security;
|
|
12
|
+
constructor(security: SecurityLayer);
|
|
11
13
|
/**
|
|
12
14
|
* Begin a new transaction
|
|
13
15
|
*/
|
|
@@ -6,8 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.TransactionTools = void 0;
|
|
7
7
|
const connection_1 = __importDefault(require("../db/connection"));
|
|
8
8
|
class TransactionTools {
|
|
9
|
-
constructor() {
|
|
9
|
+
constructor(security) {
|
|
10
10
|
this.db = connection_1.default.getInstance();
|
|
11
|
+
this.security = security;
|
|
11
12
|
}
|
|
12
13
|
/**
|
|
13
14
|
* Begin a new transaction
|
|
@@ -114,7 +115,25 @@ class TransactionTools {
|
|
|
114
115
|
error: "Query is required",
|
|
115
116
|
};
|
|
116
117
|
}
|
|
117
|
-
|
|
118
|
+
// SECURITY VALIDATION: Check if user has execute permission
|
|
119
|
+
const hasExecutePermission = this.security.isToolEnabled("executeSql");
|
|
120
|
+
// SECURITY VALIDATION: Validate the query before execution
|
|
121
|
+
const queryValidation = this.security.validateQuery(params.query, hasExecutePermission);
|
|
122
|
+
if (!queryValidation.valid) {
|
|
123
|
+
return {
|
|
124
|
+
status: "error",
|
|
125
|
+
error: `Query validation failed: ${queryValidation.error}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// SECURITY VALIDATION: Validate and sanitize parameters
|
|
129
|
+
const paramValidation = this.security.validateParameters(params.params || []);
|
|
130
|
+
if (!paramValidation.valid) {
|
|
131
|
+
return {
|
|
132
|
+
status: "error",
|
|
133
|
+
error: `Parameter validation failed: ${paramValidation.error}`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const result = await this.db.executeInTransaction(params.transactionId, params.query, paramValidation.sanitizedParams);
|
|
118
137
|
return {
|
|
119
138
|
status: "success",
|
|
120
139
|
data: result,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@berthojoris/mcp-mysql-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "Model Context Protocol server for MySQL database integration with dynamic per-project permissions, backup/restore, data import/export, and data migration capabilities",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|