0
# Security & Utilities
1
2
SQL injection protection and utility functions for safe database interactions. Includes escaping functions, raw SQL wrappers, and SQL formatting utilities essential for secure database operations.
3
4
## Capabilities
5
6
### SQL Value Escaping
7
8
Escapes values to prevent SQL injection attacks and ensure proper data handling.
9
10
```javascript { .api }
11
/**
12
* Escape a value for SQL
13
* @param {*} value - The value to escape
14
* @param {boolean} [stringifyObjects=false] - Setting if objects should be stringified
15
* @param {string} [timeZone=local] - Setting for time zone to use for Date conversion
16
* @returns {string} Escaped string value
17
*/
18
function escape(value, stringifyObjects, timeZone);
19
20
// Available on connections and pools too
21
connection.escape(value);
22
pool.escape(value);
23
```
24
25
**Usage Examples:**
26
27
```javascript
28
const mysql = require('mysql');
29
30
// Basic value escaping
31
const escapedString = mysql.escape("That's a string");
32
// Result: '\'That\\\'s a string\''
33
34
const escapedNumber = mysql.escape(123);
35
// Result: '123'
36
37
const escapedNull = mysql.escape(null);
38
// Result: 'NULL'
39
40
const escapedBoolean = mysql.escape(true);
41
// Result: 'true'
42
43
// Array escaping (for IN clauses)
44
const escapedArray = mysql.escape([1, 2, 3, 'hello']);
45
// Result: '1, 2, 3, \'hello\''
46
47
// Date escaping
48
const escapedDate = mysql.escape(new Date('2023-01-01'));
49
// Result: '\'2023-01-01 00:00:00\''
50
51
// Object escaping with stringifyObjects
52
const obj = {name: 'John', age: 30};
53
const escapedObj = mysql.escape(obj, true);
54
// Result: '\'{"name":"John","age":30}\''
55
```
56
57
**Advanced Escaping Examples:**
58
59
```javascript
60
// Timezone-aware date escaping
61
const date = new Date('2023-01-01T12:00:00Z');
62
const escapedUTC = mysql.escape(date, false, 'UTC');
63
const escapedLocal = mysql.escape(date, false, 'local');
64
65
// Object escaping for JSON storage
66
const userData = {
67
preferences: {theme: 'dark', language: 'en'},
68
lastLogin: new Date()
69
};
70
const escapedJSON = mysql.escape(userData, true);
71
72
// Buffer escaping for binary data
73
const buffer = Buffer.from('binary data');
74
const escapedBuffer = mysql.escape(buffer);
75
// Result: X'62696E617279206461746127' (hex string)
76
```
77
78
### SQL Identifier Escaping
79
80
Escapes identifiers (table names, column names) for safe dynamic SQL construction.
81
82
```javascript { .api }
83
/**
84
* Escape an identifier for SQL
85
* @param {*} value - The value to escape
86
* @param {boolean} [forbidQualified=false] - Setting to treat '.' as part of identifier
87
* @returns {string} Escaped string value
88
*/
89
function escapeId(value, forbidQualified);
90
91
// Available on connections and pools too
92
connection.escapeId(value);
93
pool.escapeId(value);
94
```
95
96
**Usage Examples:**
97
98
```javascript
99
// Basic identifier escaping
100
const tableName = mysql.escapeId('user_table');
101
// Result: '`user_table`'
102
103
const columnName = mysql.escapeId('user-name');
104
// Result: '`user-name`'
105
106
// Qualified identifier escaping (database.table.column)
107
const qualifiedId = mysql.escapeId('mydb.users.name');
108
// Result: '`mydb`.`users`.`name`'
109
110
// Forbid qualified identifiers
111
const simpleId = mysql.escapeId('mydb.users', true);
112
// Result: '`mydb.users`' (treats dot as part of identifier)
113
114
// Array of identifiers
115
const columns = ['id', 'name', 'email'].map(mysql.escapeId).join(', ');
116
// Result: '`id`, `name`, `email`'
117
```
118
119
**Dynamic Query Building:**
120
121
```javascript
122
function buildSelectQuery(table, columns, conditions) {
123
const tableName = mysql.escapeId(table);
124
const columnList = columns.map(mysql.escapeId).join(', ');
125
126
let query = `SELECT ${columnList} FROM ${tableName}`;
127
128
if (conditions && Object.keys(conditions).length > 0) {
129
const whereClause = Object.keys(conditions)
130
.map(key => `${mysql.escapeId(key)} = ${mysql.escape(conditions[key])}`)
131
.join(' AND ');
132
query += ` WHERE ${whereClause}`;
133
}
134
135
return query;
136
}
137
138
// Usage
139
const query = buildSelectQuery('users', ['id', 'name', 'email'], {active: true, role: 'admin'});
140
// Result: SELECT `id`, `name`, `email` FROM `users` WHERE `active` = true AND `role` = 'admin'
141
```
142
143
### SQL Formatting
144
145
Formats SQL strings with parameter values for complete query construction.
146
147
```javascript { .api }
148
/**
149
* Format SQL and replacement values into a SQL string
150
* @param {string} sql - The SQL for the query
151
* @param {array} [values] - Any values to insert into placeholders in sql
152
* @param {boolean} [stringifyObjects=false] - Setting if objects should be stringified
153
* @param {string} [timeZone=local] - Setting for time zone to use for Date conversion
154
* @returns {string} Formatted SQL string
155
*/
156
function format(sql, values, stringifyObjects, timeZone);
157
158
// Available on connections and pools too
159
connection.format(sql, values);
160
pool.format(sql, values);
161
```
162
163
**Usage Examples:**
164
165
```javascript
166
// Basic formatting with positional parameters
167
const sql1 = mysql.format('SELECT * FROM users WHERE id = ?', [123]);
168
// Result: SELECT * FROM users WHERE id = 123
169
170
// Multiple parameters
171
const sql2 = mysql.format('SELECT * FROM users WHERE age > ? AND city = ?', [25, 'New York']);
172
// Result: SELECT * FROM users WHERE age > 25 AND city = 'New York'
173
174
// Object parameter formatting
175
const sql3 = mysql.format('INSERT INTO users SET ?', [{name: 'John', email: 'john@example.com'}]);
176
// Result: INSERT INTO users SET `name` = 'John', `email` = 'john@example.com'
177
178
// Mixed object and value parameters
179
const sql4 = mysql.format('UPDATE users SET ? WHERE id = ?', [{name: 'Jane'}, 123]);
180
// Result: UPDATE users SET `name` = 'Jane' WHERE id = 123
181
182
// Array parameters for IN clauses
183
const sql5 = mysql.format('SELECT * FROM users WHERE id IN (?)', [[1, 2, 3, 4]]);
184
// Result: SELECT * FROM users WHERE id IN (1, 2, 3, 4)
185
```
186
187
**Advanced Formatting:**
188
189
```javascript
190
// Named placeholders with object values
191
const sql = 'SELECT * FROM users WHERE name = :name AND age > :minAge';
192
const values = {name: 'John', minAge: 18};
193
194
// Manual replacement (mysql doesn't support named placeholders directly)
195
function formatNamed(sql, values) {
196
return sql.replace(/:(\w+)/g, (match, key) => {
197
return mysql.escape(values[key]);
198
});
199
}
200
201
const formattedSQL = formatNamed(sql, values);
202
// Result: SELECT * FROM users WHERE name = 'John' AND age > 18
203
204
// Complex formatting with timezone
205
const sql6 = mysql.format(
206
'INSERT INTO logs SET created_at = ?, message = ?',
207
[new Date(), 'User login'],
208
false,
209
'UTC'
210
);
211
```
212
213
### Raw SQL Wrapper
214
215
Wraps raw SQL strings to prevent escape processing when needed.
216
217
```javascript { .api }
218
/**
219
* Wrap raw SQL strings from escape overriding
220
* @param {string} sql - The raw SQL
221
* @returns {object} Wrapped object
222
*/
223
function raw(sql);
224
```
225
226
**Usage Examples:**
227
228
```javascript
229
// Raw SQL for functions and expressions
230
const sql1 = mysql.format('INSERT INTO users SET created_at = ?, name = ?', [
231
mysql.raw('NOW()'), // Don't escape this - use as raw SQL
232
'John Doe' // This will be escaped normally
233
]);
234
// Result: INSERT INTO users SET created_at = NOW(), name = 'John Doe'
235
236
// Raw SQL for complex expressions
237
const sql2 = mysql.format('UPDATE users SET login_count = ? WHERE id = ?', [
238
mysql.raw('login_count + 1'), // Increment expression
239
userId
240
]);
241
// Result: UPDATE users SET login_count = login_count + 1 WHERE id = 123
242
243
// Raw SQL for subqueries
244
const subquery = mysql.raw('(SELECT MAX(created_at) FROM posts WHERE user_id = ?)');
245
const sql3 = mysql.format('SELECT * FROM users WHERE last_post_date = ?', [subquery]);
246
// Note: The subquery itself would need separate parameter handling
247
```
248
249
**Raw SQL Best Practices:**
250
251
```javascript
252
// Good: Use raw SQL for MySQL functions
253
const goodExample = mysql.format('INSERT INTO users SET created_at = ?, updated_at = ?', [
254
mysql.raw('NOW()'),
255
mysql.raw('NOW()')
256
]);
257
258
// Bad: Don't use raw SQL for user input (SQL injection risk)
259
// const badExample = mysql.format('SELECT * FROM users WHERE name = ?', [
260
// mysql.raw(userInput) // NEVER do this!
261
// ]);
262
263
// Good: Combine raw SQL with escaped parameters
264
function incrementCounter(userId, tableName) {
265
return mysql.format('UPDATE ?? SET counter = counter + 1 WHERE id = ?', [
266
mysql.raw(mysql.escapeId(tableName)), // Escape identifier first, then mark as raw
267
userId
268
]);
269
}
270
```
271
272
### Security Best Practices
273
274
#### SQL Injection Prevention
275
276
```javascript { .api }
277
// SECURE: Always use parameterized queries
278
function getUser(userId) {
279
return new Promise((resolve, reject) => {
280
connection.query('SELECT * FROM users WHERE id = ?', [userId], (error, results) => {
281
if (error) reject(error);
282
else resolve(results[0]);
283
});
284
});
285
}
286
287
// SECURE: Use format function for dynamic queries
288
function searchUsers(filters) {
289
const conditions = Object.keys(filters)
290
.map(key => `${mysql.escapeId(key)} = ${mysql.escape(filters[key])}`)
291
.join(' AND ');
292
293
const sql = `SELECT * FROM users WHERE ${conditions}`;
294
295
return new Promise((resolve, reject) => {
296
connection.query(sql, (error, results) => {
297
if (error) reject(error);
298
else resolve(results);
299
});
300
});
301
}
302
303
// INSECURE: Never concatenate user input directly
304
// function badGetUser(userId) {
305
// const sql = 'SELECT * FROM users WHERE id = ' + userId; // DON'T DO THIS!
306
// connection.query(sql, callback);
307
// }
308
```
309
310
#### Input Validation
311
312
```javascript { .api }
313
function validateAndEscapeInput(input, type) {
314
switch (type) {
315
case 'email':
316
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input)) {
317
throw new Error('Invalid email format');
318
}
319
return mysql.escape(input);
320
321
case 'integer':
322
const num = parseInt(input, 10);
323
if (isNaN(num)) {
324
throw new Error('Invalid integer');
325
}
326
return mysql.escape(num);
327
328
case 'string':
329
if (typeof input !== 'string') {
330
throw new Error('Input must be a string');
331
}
332
return mysql.escape(input.trim());
333
334
default:
335
return mysql.escape(input);
336
}
337
}
338
339
// Usage with validation
340
function createUser(userData) {
341
const sql = mysql.format('INSERT INTO users SET ?', [{
342
name: validateAndEscapeInput(userData.name, 'string'),
343
email: validateAndEscapeInput(userData.email, 'email'),
344
age: validateAndEscapeInput(userData.age, 'integer')
345
}]);
346
347
return new Promise((resolve, reject) => {
348
connection.query(sql, (error, results) => {
349
if (error) reject(error);
350
else resolve(results);
351
});
352
});
353
}
354
```
355
356
### Utility Helper Functions
357
358
#### Query Builder Helper
359
360
```javascript { .api }
361
class QueryBuilder {
362
constructor() {
363
this.reset();
364
}
365
366
reset() {
367
this._select = '*';
368
this._from = '';
369
this._where = [];
370
this._orderBy = [];
371
this._limit = null;
372
return this;
373
}
374
375
select(columns) {
376
this._select = Array.isArray(columns)
377
? columns.map(mysql.escapeId).join(', ')
378
: mysql.escapeId(columns);
379
return this;
380
}
381
382
from(table) {
383
this._from = mysql.escapeId(table);
384
return this;
385
}
386
387
where(column, operator, value) {
388
this._where.push(`${mysql.escapeId(column)} ${operator} ${mysql.escape(value)}`);
389
return this;
390
}
391
392
orderBy(column, direction = 'ASC') {
393
this._orderBy.push(`${mysql.escapeId(column)} ${direction}`);
394
return this;
395
}
396
397
limit(count) {
398
this._limit = parseInt(count, 10);
399
return this;
400
}
401
402
build() {
403
let sql = `SELECT ${this._select} FROM ${this._from}`;
404
405
if (this._where.length > 0) {
406
sql += ` WHERE ${this._where.join(' AND ')}`;
407
}
408
409
if (this._orderBy.length > 0) {
410
sql += ` ORDER BY ${this._orderBy.join(', ')}`;
411
}
412
413
if (this._limit) {
414
sql += ` LIMIT ${this._limit}`;
415
}
416
417
return sql;
418
}
419
}
420
421
// Usage
422
const query = new QueryBuilder()
423
.select(['id', 'name', 'email'])
424
.from('users')
425
.where('active', '=', true)
426
.where('age', '>', 18)
427
.orderBy('name')
428
.limit(10)
429
.build();
430
```
431
432
### Error Handling
433
434
```javascript { .api }
435
// Common MySQL error codes
436
const MYSQL_ERROR_CODES = {
437
// Connection errors
438
'PROTOCOL_CONNECTION_LOST': 'Database connection was closed',
439
'PROTOCOL_ENQUEUE_AFTER_QUIT': 'Cannot enqueue after invoking quit',
440
'PROTOCOL_ENQUEUE_AFTER_FATAL_ERROR': 'Cannot enqueue after fatal error',
441
'PROTOCOL_ENQUEUE_HANDSHAKE_TWICE': 'Cannot enqueue handshake twice',
442
'ECONNREFUSED': 'Connection refused: MySQL server not reachable',
443
'ENOTFOUND': 'Host not found',
444
'ETIMEDOUT': 'Connection timed out',
445
446
// Authentication errors
447
'ER_ACCESS_DENIED_ERROR': 'Access denied for user',
448
'ER_NOT_SUPPORTED_AUTH_MODE': 'Authentication method not supported',
449
'ER_MUST_CHANGE_PASSWORD': 'Password must be changed',
450
451
// Database/table errors
452
'ER_BAD_DB_ERROR': 'Unknown database',
453
'ER_NO_SUCH_TABLE': 'Table does not exist',
454
'ER_BAD_TABLE_ERROR': 'Unknown table',
455
'ER_NON_UNIQ_ERROR': 'Column is ambiguous',
456
'ER_BAD_FIELD_ERROR': 'Unknown column',
457
458
// Data integrity errors
459
'ER_DUP_ENTRY': 'Duplicate entry for key',
460
'ER_NO_REFERENCED_ROW': 'Foreign key constraint fails (parent missing)',
461
'ER_ROW_IS_REFERENCED': 'Foreign key constraint fails (child exists)',
462
'ER_DATA_TOO_LONG': 'Data too long for column',
463
'ER_TRUNCATED_WRONG_VALUE': 'Incorrect value',
464
'ER_BAD_NULL_ERROR': 'Column cannot be null',
465
466
// SQL syntax errors
467
'ER_PARSE_ERROR': 'SQL syntax error',
468
'ER_NO_DEFAULT_FOR_FIELD': 'Field has no default value',
469
'ER_WRONG_VALUE_COUNT_ON_ROW': 'Column count does not match value count',
470
471
// Transaction errors
472
'ER_LOCK_WAIT_TIMEOUT': 'Lock wait timeout exceeded',
473
'ER_LOCK_DEADLOCK': 'Deadlock found when trying to get lock',
474
475
// Server capacity errors
476
'ER_CON_COUNT_ERROR': 'Too many connections',
477
'ER_OUT_OF_RESOURCES': 'Out of memory',
478
'ER_DISK_FULL': 'Disk full',
479
480
// Query timeout/limits
481
'ER_QUERY_INTERRUPTED': 'Query execution was interrupted',
482
'PROTOCOL_SEQUENCE_TIMEOUT': 'Query timeout'
483
};
484
485
// MySQL error codes and handling
486
function handleMySQLError(error) {
487
const message = MYSQL_ERROR_CODES[error.code];
488
if (message) {
489
return `${message}: ${error.message}`;
490
}
491
492
switch (error.code) {
493
case 'ER_DUP_ENTRY':
494
return 'Duplicate entry: Record already exists';
495
case 'ER_NO_SUCH_TABLE':
496
return 'Table does not exist';
497
case 'ER_ACCESS_DENIED_ERROR':
498
return 'Access denied: Check credentials';
499
case 'ER_BAD_DB_ERROR':
500
return 'Unknown database';
501
case 'PROTOCOL_CONNECTION_LOST':
502
return 'Connection lost: Attempting to reconnect';
503
case 'ECONNREFUSED':
504
return 'Connection refused: MySQL server not available';
505
default:
506
return `MySQL Error (${error.code}): ${error.message}`;
507
}
508
}
509
510
// Secure error reporting (don't expose internal details)
511
function safeErrorHandler(error, res) {
512
console.error('Database error:', error); // Log full error internally
513
514
const userMessage = handleMySQLError(error);
515
res.status(500).json({
516
error: 'Database operation failed',
517
message: userMessage // Safe message for users
518
});
519
}
520
```
521
522
Security utilities provide essential protection against SQL injection attacks and ensure safe database interactions. Always use parameterized queries and proper escaping functions when constructing dynamic SQL statements.