@chiranthmoger/fortifyjs 1.1.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.
Files changed (50) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +186 -0
  3. package/bin/fortifyjs.js +135 -0
  4. package/examples/fastify.js +18 -0
  5. package/examples/hono.js +13 -0
  6. package/examples/kitchen-sink.js +50 -0
  7. package/examples/koa.js +14 -0
  8. package/examples/minimal-express.js +13 -0
  9. package/examples/production-express.js +20 -0
  10. package/index.d.ts +101 -0
  11. package/package.json +70 -0
  12. package/src/adapters/express.js +1027 -0
  13. package/src/adapters/fastify.js +56 -0
  14. package/src/adapters/generic.js +15 -0
  15. package/src/adapters/hono.js +77 -0
  16. package/src/adapters/koa.js +58 -0
  17. package/src/adapters/nestjs.js +15 -0
  18. package/src/adapters/nextjs.js +84 -0
  19. package/src/analyzers/adaptive.js +92 -0
  20. package/src/analyzers/behavioral.js +264 -0
  21. package/src/core/confidence.js +23 -0
  22. package/src/core/engine.js +162 -0
  23. package/src/core/normalizer.js +149 -0
  24. package/src/core/whitelist.js +40 -0
  25. package/src/dashboard/handler.js +307 -0
  26. package/src/detectors/cmdi.js +82 -0
  27. package/src/detectors/crlf.js +33 -0
  28. package/src/detectors/graphql.js +56 -0
  29. package/src/detectors/hpp.js +38 -0
  30. package/src/detectors/ldap.js +50 -0
  31. package/src/detectors/nosqli.js +134 -0
  32. package/src/detectors/open-redirect.js +42 -0
  33. package/src/detectors/path-traversal.js +55 -0
  34. package/src/detectors/prototype-pollution.js +65 -0
  35. package/src/detectors/sqli.js +447 -0
  36. package/src/detectors/sqli.js.bak +446 -0
  37. package/src/detectors/ssrf.js +64 -0
  38. package/src/detectors/template-injection.js +34 -0
  39. package/src/detectors/xss.js +191 -0
  40. package/src/detectors/xxe.js +45 -0
  41. package/src/forensics/reporter.js +74 -0
  42. package/src/index.js +132 -0
  43. package/src/logger.js +110 -0
  44. package/src/presets.js +183 -0
  45. package/src/shields/bot-detector.js +88 -0
  46. package/src/shields/cors.js +95 -0
  47. package/src/shields/csrf.js +120 -0
  48. package/src/shields/file-upload.js +160 -0
  49. package/src/shields/headers.js +99 -0
  50. package/src/shields/rate-limiter.js +70 -0
@@ -0,0 +1,447 @@
1
+ 'use strict';
2
+
3
+ const SQL_IDENTIFIER = '(?:`[^`]+`|"[^"]+"|\\[[^\\]]+\\]|[A-Za-z_][\\w$]*)';
4
+ const SQL_WORD_BOOLEAN_OPERATOR = '(?:OR|AND|XOR)';
5
+ const SQL_SYMBOL_BOOLEAN_OPERATOR = '(?:\\|\\||&&)';
6
+ const SQL_BOOLEAN_OPERATOR = `(?:(?:\\b${SQL_WORD_BOOLEAN_OPERATOR}\\b)|${SQL_SYMBOL_BOOLEAN_OPERATOR})`;
7
+ const SQL_COMPARISON_OPERATOR = '(?:=|LIKE|!=|<>|<=|>=|<|>)';
8
+ const SQL_FUNCTION_CALL = `${SQL_IDENTIFIER}\\s*\\((?:[^()]|\\([^()]{0,120}\\)){0,240}\\)`;
9
+ const SQL_CONSTANT_VALUE = `(?:${SQL_FUNCTION_CALL}|\\d+(?:\\.\\d+)?|N?[\'"][^\'"]{0,80}[\'"]?|NULL)`;
10
+ const SQL_VALUE = `(?:${SQL_FUNCTION_CALL}|\\d+(?:\\.\\d+)?|N?[\'"][^\'"]{0,80}[\'"]?|[A-Za-z_][\\w.]*|NULL)`;
11
+ const SQL_CONSTANT_COMPARISON_EXPRESSION = `${SQL_CONSTANT_VALUE}\\s*${SQL_COMPARISON_OPERATOR}\\s*${SQL_CONSTANT_VALUE}`;
12
+ const SQL_COMPARISON_EXPRESSION = `${SQL_VALUE}\\s*${SQL_COMPARISON_OPERATOR}\\s*${SQL_VALUE}`;
13
+ const SQL_BETWEEN_EXPRESSION = `${SQL_VALUE}\\s+BETWEEN\\s+${SQL_VALUE}\\s+AND\\s+${SQL_VALUE}`;
14
+ const SQL_CONSTANT_BETWEEN = `${SQL_CONSTANT_VALUE}\\s+BETWEEN\\s+${SQL_CONSTANT_VALUE}\\s+AND\\s+${SQL_CONSTANT_VALUE}`;
15
+ const SQL_IS_EXPRESSION = `${SQL_VALUE}\\s+IS\\s+(?:NOT\\s+)?NULL`;
16
+ const SQL_CONSTANT_IS = `${SQL_CONSTANT_VALUE}\\s+IS\\s+(?:NOT\\s+)?NULL`;
17
+ const SQL_EXISTS_EXPRESSION = `EXISTS\\s*\\(\\s*SELECT\\b`;
18
+ const SQL_BOOLEAN_LITERAL_EXPRESSION = '(?:TRUE|FALSE|UNKNOWN|NULL)';
19
+ const SQL_BOOLEAN_EXPRESSION = `(?:${SQL_COMPARISON_EXPRESSION}|${SQL_BETWEEN_EXPRESSION}|${SQL_IS_EXPRESSION}|${SQL_EXISTS_EXPRESSION}|${SQL_BOOLEAN_LITERAL_EXPRESSION})`;
20
+ const SQL_CONSTANT_BOOLEAN_EXPRESSION = `(?:${SQL_CONSTANT_COMPARISON_EXPRESSION}|${SQL_CONSTANT_BETWEEN}|${SQL_CONSTANT_IS}|${SQL_EXISTS_EXPRESSION}|${SQL_BOOLEAN_LITERAL_EXPRESSION})`;
21
+ const SQL_STACKED_STATEMENT_KEYWORD = '(?:SELECT|WITH|UNION|DROP|INSERT|UPDATE|DELETE|ALTER|CREATE|EXEC|EXECUTE|CALL|MERGE|TRUNCATE)';
22
+ const SQL_METADATA_OBJECT = '(?:information_schema(?:\\.[A-Za-z_][\\w$]*)?|sysobjects|sys\\.(?:tables|columns|objects|databases|schemas|indexes|all_columns)|sqlite_master|sqlite_schema|pg_catalog(?:\\.[A-Za-z_][\\w$]*)?|pg_(?:class|tables|namespace|attribute|database|user)|mysql\\.(?:innodb_table_stats|innodb_index_stats|user|db|tables_priv|columns_priv|proc|tables)|(?:all|user|dba)_(?:tables|tab_columns|objects|users|catalog|constraints|cons_columns|views))';
23
+ const SQL_METADATA_QUERY_CONTEXT = '(?:SELECT|FROM|JOIN|WHERE|COUNT\\s*\\(|EXISTS\\s*\\(|SHOW\\s+(?:FULL\\s+)?(?:TABLES|COLUMNS)|DESCRIBE|DESC)';
24
+ const sqlWord = (word) => word.split('').join('[\\s\\u00a0]*');
25
+ const unionSelectPattern = (wordBuilder = word => word) => `\\b${wordBuilder('UNION')}(?:\\s+(?:${wordBuilder('ALL')}|${wordBuilder('DISTINCT')})\\s+|\\s+|\\s*\\(\\s*)${wordBuilder('SELECT')}\\b`;
26
+ const unionValuesPattern = (wordBuilder = word => word) => `\\b${wordBuilder('UNION')}(?:\\s+(?:${wordBuilder('ALL')}|${wordBuilder('DISTINCT')})\\s+|\\s+|\\s*\\(\\s*)${wordBuilder('VALUES')}\\s*\\(`;
27
+ const SQL_BOOLEAN_WORDS = new Set(['OR', 'AND', 'XOR']);
28
+ const SQL_COMPARISON_WORDS = new Set(['LIKE']);
29
+ const SQL_COMPARISON_OPERATORS = new Set(['=', '!=', '<>', '<=', '>=', '<', '>']);
30
+ const SQL_LITERAL_WORDS = new Set(['TRUE', 'FALSE', 'UNKNOWN', 'NULL']);
31
+ const SQL_STACKED_WORDS = new Set(['SELECT', 'WITH', 'UNION', 'DROP', 'INSERT', 'UPDATE', 'DELETE', 'ALTER', 'CREATE', 'EXEC', 'EXECUTE', 'CALL', 'MERGE', 'TRUNCATE']);
32
+ const SQL_QUERY_CONTEXT_WORDS = new Set(['SELECT', 'FROM', 'JOIN', 'WHERE', 'DESCRIBE', 'DESC', 'EXISTS']);
33
+ const SQL_PG_CATALOGS = new Set(['pg_class', 'pg_tables', 'pg_namespace', 'pg_attribute', 'pg_database', 'pg_user']);
34
+ const SQL_MYSQL_CATALOGS = new Set(['innodb_table_stats', 'innodb_index_stats', 'user', 'db', 'tables_priv', 'columns_priv', 'proc', 'tables']);
35
+ const SQL_SERVER_CATALOGS = new Set(['tables', 'columns', 'objects', 'databases', 'schemas', 'indexes', 'all_columns']);
36
+ const ORACLE_CATALOG_SUFFIXES = new Set(['tables', 'tab_columns', 'objects', 'users', 'catalog', 'constraints', 'cons_columns', 'views']);
37
+ function isAsciiLetter(ch) {
38
+ return /[A-Za-z]/.test(ch);
39
+ }
40
+
41
+ function isAsciiDigit(ch) {
42
+ return /[0-9]/.test(ch);
43
+ }
44
+
45
+ function isSqlWordStart(ch) {
46
+ return isAsciiLetter(ch) || ch === '_' || ch === '$';
47
+ }
48
+
49
+ function isSqlWordPart(ch) {
50
+ return isSqlWordStart(ch) || isAsciiDigit(ch);
51
+ }
52
+
53
+ function tokenizeSqlFragment(value) {
54
+ const text = String(value);
55
+ const tokens = [];
56
+ let i = 0;
57
+
58
+ while (i < text.length && tokens.length < 1200) {
59
+ const ch = text[i];
60
+ if (/\s/.test(ch)) {
61
+ i++;
62
+ continue;
63
+ }
64
+
65
+ if (isSqlWordStart(ch)) {
66
+ const start = i;
67
+ i++;
68
+ while (i < text.length && isSqlWordPart(text[i])) i++;
69
+ const word = text.slice(start, i);
70
+ tokens.push({ type: 'word', value: word, upper: word.toUpperCase() });
71
+ continue;
72
+ }
73
+
74
+ if (isAsciiDigit(ch)) {
75
+ const start = i;
76
+ i++;
77
+ while (i < text.length && /[0-9.]/.test(text[i])) i++;
78
+ tokens.push({ type: 'number', value: text.slice(start, i) });
79
+ continue;
80
+ }
81
+
82
+ if (ch === "'" || ch === '"' || ch === '`') {
83
+ tokens.push({ type: 'quote', value: ch });
84
+ i++;
85
+ continue;
86
+ }
87
+
88
+ const twoChar = text.slice(i, i + 2);
89
+ if (['!=', '<>', '<=', '>=', '||', '&&', '--'].includes(twoChar)) {
90
+ tokens.push({ type: 'operator', value: twoChar });
91
+ i += 2;
92
+ continue;
93
+ }
94
+
95
+ if ('=<>!|&+-*/%'.includes(ch)) {
96
+ tokens.push({ type: 'operator', value: ch });
97
+ i++;
98
+ continue;
99
+ }
100
+
101
+ if (';(),.[]{}'.includes(ch)) {
102
+ tokens.push({ type: 'punct', value: ch });
103
+ i++;
104
+ continue;
105
+ }
106
+
107
+ i++;
108
+ }
109
+
110
+ return tokens;
111
+ }
112
+
113
+ function isSqlBooleanToken(token) {
114
+ return (
115
+ (token.type === 'word' && SQL_BOOLEAN_WORDS.has(token.upper)) ||
116
+ (token.type === 'operator' && (token.value === '||' || token.value === '&&'))
117
+ );
118
+ }
119
+
120
+ function isSqlComparisonToken(token) {
121
+ return (
122
+ (token.type === 'operator' && SQL_COMPARISON_OPERATORS.has(token.value)) ||
123
+ (token.type === 'word' && SQL_COMPARISON_WORDS.has(token.upper))
124
+ );
125
+ }
126
+
127
+ function isSqlValueLike(token) {
128
+ return token && (
129
+ token.type === 'word' ||
130
+ token.type === 'number' ||
131
+ token.type === 'quote' ||
132
+ (token.type === 'punct' && token.value === ')')
133
+ );
134
+ }
135
+
136
+ function isSqlConstantLike(token) {
137
+ return token && (
138
+ token.type === 'number' ||
139
+ token.type === 'quote' ||
140
+ (token.type === 'word' && SQL_LITERAL_WORDS.has(token.upper)) ||
141
+ (token.type === 'punct' && token.value === ')')
142
+ );
143
+ }
144
+
145
+ function isSqlConstantLikeAt(tokens, index) {
146
+ const token = tokens[index];
147
+ if (isSqlConstantLike(token)) return true;
148
+ return token?.type === 'word' && nextToken(tokens, index)?.value === '(';
149
+ }
150
+
151
+ function previousToken(tokens, index) {
152
+ return index > 0 ? tokens[index - 1] : null;
153
+ }
154
+
155
+ function nextToken(tokens, index) {
156
+ return index + 1 < tokens.length ? tokens[index + 1] : null;
157
+ }
158
+
159
+ function hasSqlComparison(tokens, start, end, constantOnly = false) {
160
+ for (let i = start; i < Math.min(tokens.length, end); i++) {
161
+ if (!isSqlComparisonToken(tokens[i])) continue;
162
+ const left = previousToken(tokens, i);
163
+ const right = nextToken(tokens, i);
164
+ if (constantOnly) {
165
+ if (isSqlConstantLikeAt(tokens, i - 1) && isSqlConstantLikeAt(tokens, i + 1)) return true;
166
+ } else if (isSqlValueLike(left) && isSqlValueLike(right)) {
167
+ return true;
168
+ }
169
+ }
170
+ return false;
171
+ }
172
+
173
+ function hasStrongSqlBreakoutContext(tokens, booleanIndex) {
174
+ let quoteCount = 0;
175
+ for (let i = 0; i < booleanIndex; i++) {
176
+ const token = tokens[i];
177
+ if (token.type === 'quote') quoteCount++;
178
+ if (token.type === 'punct' && [';', ')', '('].includes(token.value)) return true;
179
+ if (token.type === 'operator' && ['||', '&&', '--'].includes(token.value)) return true;
180
+ }
181
+ return (quoteCount % 2 !== 0);
182
+ }
183
+
184
+ function rightSideSqlPredicate(tokens, booleanIndex) {
185
+ const start = booleanIndex + 1;
186
+ const end = Math.min(tokens.length, booleanIndex + 18);
187
+ let hasPredicate = false;
188
+ let hasConstantPredicate = false;
189
+
190
+ for (let i = start; i < end; i++) {
191
+ const token = tokens[i];
192
+ if (token.type !== 'word') continue;
193
+ if (SQL_LITERAL_WORDS.has(token.upper)) {
194
+ hasPredicate = true;
195
+ hasConstantPredicate = true;
196
+ }
197
+ if (token.upper === 'EXISTS') {
198
+ hasPredicate = true;
199
+ hasConstantPredicate = true;
200
+ }
201
+ if (token.upper === 'BETWEEN') {
202
+ hasPredicate = true;
203
+ hasConstantPredicate = true;
204
+ }
205
+ }
206
+
207
+ if (hasSqlComparison(tokens, start, end, true)) {
208
+ hasPredicate = true;
209
+ hasConstantPredicate = true;
210
+ } else if (hasSqlComparison(tokens, start, end, false)) {
211
+ hasPredicate = true;
212
+ }
213
+
214
+ return { hasPredicate, hasConstantPredicate };
215
+ }
216
+
217
+ function hasStructuralSqlBooleanAbuse(value) {
218
+ const tokens = tokenizeSqlFragment(value);
219
+ for (let i = 0; i < tokens.length; i++) {
220
+ if (!isSqlBooleanToken(tokens[i])) continue;
221
+ const right = rightSideSqlPredicate(tokens, i);
222
+ if (!right.hasPredicate) continue;
223
+ if (hasStrongSqlBreakoutContext(tokens, i)) return true;
224
+ }
225
+ return false;
226
+ }
227
+
228
+ function hasStructuralSqlStackedStatement(value) {
229
+ const tokens = tokenizeSqlFragment(value);
230
+ for (let i = 0; i < tokens.length; i++) {
231
+ if (tokens[i].type !== 'punct' || tokens[i].value !== ';') continue;
232
+ let j = i + 1;
233
+ while (j < tokens.length && tokens[j].type === 'punct' && tokens[j].value === '(') j++;
234
+ if (j < tokens.length && tokens[j].type === 'word' && SQL_STACKED_WORDS.has(tokens[j].upper)) return true;
235
+ }
236
+ return false;
237
+ }
238
+
239
+ function metadataNameAt(tokens, index) {
240
+ if (!tokens[index] || tokens[index].type !== 'word') return null;
241
+ const parts = [tokens[index].value.toLowerCase()];
242
+ let end = index;
243
+ let cursor = index + 1;
244
+
245
+ while (
246
+ cursor + 1 < tokens.length &&
247
+ tokens[cursor].type === 'punct' &&
248
+ tokens[cursor].value === '.' &&
249
+ tokens[cursor + 1].type === 'word'
250
+ ) {
251
+ parts.push(tokens[cursor + 1].value.toLowerCase());
252
+ end = cursor + 1;
253
+ cursor += 2;
254
+ }
255
+
256
+ return { name: parts.join('.'), parts, end };
257
+ }
258
+
259
+ function isSqlMetadataName(nameInfo) {
260
+ if (!nameInfo) return false;
261
+ const { name, parts } = nameInfo;
262
+ if (name === 'sysobjects' || name === 'sqlite_master' || name === 'sqlite_schema') return true;
263
+ if (name === 'information_schema' || name.startsWith('information_schema.')) return true;
264
+ if (name === 'pg_catalog' || name.startsWith('pg_catalog.') || SQL_PG_CATALOGS.has(name)) return true;
265
+ if (parts[0] === 'sys' && SQL_SERVER_CATALOGS.has(parts[1])) return true;
266
+ if (parts[0] === 'mysql' && SQL_MYSQL_CATALOGS.has(parts[1])) return true;
267
+ if (['all', 'user', 'dba'].includes(parts[0]) && ORACLE_CATALOG_SUFFIXES.has(parts.slice(1).join('_'))) return true;
268
+ for (const prefix of ['all', 'user', 'dba']) {
269
+ const marker = `${prefix}_`;
270
+ if (name.startsWith(marker) && ORACLE_CATALOG_SUFFIXES.has(name.slice(marker.length))) return true;
271
+ }
272
+ return false;
273
+ }
274
+
275
+ function isSqlQueryContext(tokens, index) {
276
+ const token = tokens[index];
277
+ if (!token || token.type !== 'word') return false;
278
+ if (SQL_QUERY_CONTEXT_WORDS.has(token.upper)) return true;
279
+ if (token.upper === 'COUNT' && nextToken(tokens, index)?.value === '(') return true;
280
+ if (token.upper === 'SHOW') {
281
+ const next = nextToken(tokens, index);
282
+ const afterNext = nextToken(tokens, index + 1);
283
+ return (
284
+ next?.upper === 'TABLES' ||
285
+ next?.upper === 'COLUMNS' ||
286
+ (next?.upper === 'FULL' && (afterNext?.upper === 'TABLES' || afterNext?.upper === 'COLUMNS'))
287
+ );
288
+ }
289
+ return false;
290
+ }
291
+
292
+ function hasStructuralSqlMetadataQuery(value) {
293
+ const tokens = tokenizeSqlFragment(value);
294
+ const queryContextIndexes = [];
295
+ const metadataIndexes = [];
296
+
297
+ for (let i = 0; i < tokens.length; i++) {
298
+ if (isSqlQueryContext(tokens, i)) queryContextIndexes.push(i);
299
+ const metadata = metadataNameAt(tokens, i);
300
+ if (isSqlMetadataName(metadata)) {
301
+ metadataIndexes.push(i);
302
+ i = metadata.end;
303
+ }
304
+ }
305
+
306
+ return metadataIndexes.some(metadataIndex =>
307
+ queryContextIndexes.some(contextIndex => Math.abs(contextIndex - metadataIndex) <= 40)
308
+ );
309
+ }
310
+
311
+ function hasRepeatedQuotedLiteralComparison(value) {
312
+ const comparison = /(['"])([^'"]{1,80})\1\s*=\s*(['"])([^'"]{1,80})\3/g;
313
+ let match;
314
+ while ((match = comparison.exec(value)) !== null) {
315
+ if (match[2] === match[4]) return true;
316
+ }
317
+ return false;
318
+ }
319
+
320
+ module.exports = {
321
+ name: 'sqli',
322
+ label: 'sqli',
323
+ getSignals() {
324
+ return [
325
+ {
326
+ id: 'union-select',
327
+ confidence: 0.8,
328
+ pattern: new RegExp(unionSelectPattern(), 'i')
329
+ },
330
+ {
331
+ id: 'comment-fragmented-union-select',
332
+ confidence: 0.8,
333
+ pattern: new RegExp(unionSelectPattern(sqlWord), 'i')
334
+ },
335
+ {
336
+ id: 'union-values',
337
+ confidence: 0.8,
338
+ pattern: new RegExp(unionValuesPattern(), 'i')
339
+ },
340
+ {
341
+ id: 'comment-fragmented-union-values',
342
+ confidence: 0.8,
343
+ pattern: new RegExp(unionValuesPattern(sqlWord), 'i')
344
+ },
345
+ {
346
+ id: 'mysql-versioned-comment',
347
+ confidence: 0.55,
348
+ pattern: /(?:\/\*!\d{0,6}|MYSQL_VERSIONED_COMMENT)/i
349
+ },
350
+ {
351
+ id: 'sql-structural-boolean',
352
+ confidence: 0.75,
353
+ test: hasStructuralSqlBooleanAbuse
354
+ },
355
+ {
356
+ id: 'boolean-tautology',
357
+ confidence: 0.75,
358
+ pattern: new RegExp(`['"\`)]\\s*${SQL_BOOLEAN_OPERATOR}\\s+(?:NOT\\s+)?${SQL_CONSTANT_BOOLEAN_EXPRESSION}|${SQL_SYMBOL_BOOLEAN_OPERATOR}\\s+(?:NOT\\s+)?${SQL_CONSTANT_BOOLEAN_EXPRESSION}|\\b${SQL_WORD_BOOLEAN_OPERATOR}\\b\\s+(?:NOT\\s+)?${SQL_CONSTANT_BOOLEAN_EXPRESSION}|\\b\\d+\\s+\\b${SQL_WORD_BOOLEAN_OPERATOR}\\b\\s+(?:NOT\\s+)?${SQL_CONSTANT_BOOLEAN_EXPRESSION}`, 'i')
359
+ },
360
+ {
361
+ id: 'drop-table',
362
+ confidence: 0.75,
363
+ pattern: new RegExp(`(?:^|[;\\s])DROP\\s+TABLE\\s+(?:IF\\s+EXISTS\\s+)?${SQL_IDENTIFIER}\\s*(?:CASCADE\\s*|RESTRICT\\s*)?(?:;|--|#|$)`, 'i')
364
+ },
365
+ {
366
+ id: 'insert-values',
367
+ confidence: 0.7,
368
+ pattern: new RegExp(`\\bINSERT\\s+INTO\\s+${SQL_IDENTIFIER}\\s*(?:\\([^)]*\\)\\s*)?VALUES\\s*\\(`, 'i')
369
+ },
370
+ {
371
+ id: 'update-set',
372
+ confidence: 0.7,
373
+ pattern: new RegExp(`\\bUPDATE\\s+${SQL_IDENTIFIER}\\s+SET\\s+${SQL_IDENTIFIER}\\s*=`, 'i')
374
+ },
375
+ {
376
+ id: 'delete-from',
377
+ confidence: 0.65,
378
+ pattern: new RegExp(`\\bDELETE\\s+FROM\\s+${SQL_IDENTIFIER}\\s*(?:WHERE\\b|;|--|#|$)`, 'i')
379
+ },
380
+ {
381
+ id: 'stacked-sql-statement',
382
+ confidence: 0.65,
383
+ pattern: new RegExp(`;\\s*(?:\\(\\s*)*${SQL_STACKED_STATEMENT_KEYWORD}\\b`, 'i')
384
+ },
385
+ {
386
+ id: 'sql-structural-stacked-statement',
387
+ confidence: 0.65,
388
+ test: hasStructuralSqlStackedStatement
389
+ },
390
+ {
391
+ id: 'sql-comment-breakout',
392
+ confidence: 0.55,
393
+ pattern: /(?:['"`]\s*(?:--|#)(?:\s|$)|--\s*$|#\s*$)/i
394
+ },
395
+ {
396
+ id: 'sql-unclosed-block-comment-breakout',
397
+ confidence: 0.55,
398
+ pattern: /(?:['"`)]\s*\/\*(?![\s\S]*\*\/)|\/\*\s*$)/i
399
+ },
400
+ {
401
+ id: 'time-delay',
402
+ confidence: 0.75,
403
+ pattern: /\b(?:SLEEP\s*\(|WAITFOR\s+DELAY\b|BENCHMARK\s*\(|PG_SLEEP\s*\(|DBMS_PIPE\.RECEIVE_MESSAGE\s*\()/i
404
+ },
405
+ {
406
+ id: 'out-of-band',
407
+ confidence: 0.8,
408
+ pattern: /\b(?:LOAD_FILE\s*\(|UTL_HTTP\.REQUEST\s*\(|UTL_INADDR\.GET_HOST_ADDRESS\s*\(|INTO\s+(?:DUMP|OUT)FILE\b)/i
409
+ },
410
+ {
411
+ id: 'error-based-sqli',
412
+ confidence: 0.75,
413
+ pattern: /\b(?:CAST\s*\(\s*(?:\(\s*SELECT\b|@@[a-z_]+)|CONVERT\s*\(\s*[a-z_]+\s*,\s*(?:\(\s*SELECT\b|@@[a-z_]+))/i
414
+ },
415
+ {
416
+ id: 'nosql-operator',
417
+ confidence: 0.65,
418
+ pattern: /(?:^|[{,\s])["']?\$(?:where|ne|gt|lt|gte|lte|in|nin|regex|expr|or|and)["']?\s*:/i
419
+ },
420
+ {
421
+ id: 'nosql-operator-key',
422
+ confidence: 0.65,
423
+ pattern: /^\$(?:where|ne|gt|lt|gte|lte|in|nin|regex|expr|or|and)$/i
424
+ },
425
+ {
426
+ id: 'repeated-quoted-literal-comparison',
427
+ confidence: 0.3,
428
+ test: hasRepeatedQuotedLiteralComparison
429
+ },
430
+ {
431
+ id: 'sql-metadata-probe',
432
+ confidence: 0.45,
433
+ pattern: new RegExp(`\\b${SQL_METADATA_OBJECT}\\b`, 'i')
434
+ },
435
+ {
436
+ id: 'sql-metadata-query',
437
+ confidence: 0.65,
438
+ pattern: new RegExp(`\\b${SQL_METADATA_QUERY_CONTEXT}[\\s\\S]{0,240}\\b${SQL_METADATA_OBJECT}\\b|\\b${SQL_METADATA_OBJECT}\\b[\\s\\S]{0,240}\\b${SQL_METADATA_QUERY_CONTEXT}`, 'i')
439
+ },
440
+ {
441
+ id: 'sql-structural-metadata-query',
442
+ confidence: 0.65,
443
+ test: hasStructuralSqlMetadataQuery
444
+ }
445
+ ];
446
+ }
447
+ };