proxql 0.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.
- package/LICENSE +201 -0
- package/README.md +189 -0
- package/dist/index.d.mts +261 -0
- package/dist/index.d.ts +261 -0
- package/dist/index.js +766 -0
- package/dist/index.mjs +734 -0
- package/package.json +62 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
RuleSeverity: () => RuleSeverity,
|
|
24
|
+
SecurityConfig: () => SecurityConfig,
|
|
25
|
+
ValidationResult: () => ValidationResult,
|
|
26
|
+
Validator: () => Validator,
|
|
27
|
+
default: () => index_default,
|
|
28
|
+
isSafe: () => isSafe,
|
|
29
|
+
validate: () => validate
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
|
|
33
|
+
// src/validator.ts
|
|
34
|
+
var import_node_sql_parser = require("node-sql-parser");
|
|
35
|
+
|
|
36
|
+
// src/result.ts
|
|
37
|
+
var ValidationResult = class _ValidationResult {
|
|
38
|
+
/** Whether the query passed all validation checks */
|
|
39
|
+
isSafe;
|
|
40
|
+
/** Explanation if the query was blocked (undefined if safe) */
|
|
41
|
+
reason;
|
|
42
|
+
/** The type of SQL statement (SELECT, INSERT, DROP, etc.) */
|
|
43
|
+
statementType;
|
|
44
|
+
/** Tables referenced in the query */
|
|
45
|
+
tables;
|
|
46
|
+
constructor(data) {
|
|
47
|
+
this.isSafe = data.isSafe;
|
|
48
|
+
this.reason = data.reason;
|
|
49
|
+
this.statementType = data.statementType;
|
|
50
|
+
this.tables = data.tables ?? [];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Create a safe validation result.
|
|
54
|
+
*/
|
|
55
|
+
static safe(meta = {}) {
|
|
56
|
+
return new _ValidationResult({ isSafe: true, ...meta });
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create an unsafe validation result with a reason.
|
|
60
|
+
*/
|
|
61
|
+
static unsafe(reason, meta = {}) {
|
|
62
|
+
return new _ValidationResult({ isSafe: false, reason, ...meta });
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Allow using ValidationResult in boolean context.
|
|
66
|
+
* Note: This is for documentation purposes; JS doesn't support operator overloading.
|
|
67
|
+
* Use result.isSafe directly.
|
|
68
|
+
*/
|
|
69
|
+
valueOf() {
|
|
70
|
+
return this.isSafe;
|
|
71
|
+
}
|
|
72
|
+
toString() {
|
|
73
|
+
if (this.isSafe) {
|
|
74
|
+
return `ValidationResult(safe, type=${this.statementType ?? "unknown"})`;
|
|
75
|
+
}
|
|
76
|
+
return `ValidationResult(unsafe, reason="${this.reason}")`;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// src/security.ts
|
|
81
|
+
var RuleSeverity = /* @__PURE__ */ ((RuleSeverity2) => {
|
|
82
|
+
RuleSeverity2["LOW"] = "LOW";
|
|
83
|
+
RuleSeverity2["MEDIUM"] = "MEDIUM";
|
|
84
|
+
RuleSeverity2["HIGH"] = "HIGH";
|
|
85
|
+
RuleSeverity2["CRITICAL"] = "CRITICAL";
|
|
86
|
+
return RuleSeverity2;
|
|
87
|
+
})(RuleSeverity || {});
|
|
88
|
+
var SEVERITY_ORDER = {
|
|
89
|
+
["LOW" /* LOW */]: 1,
|
|
90
|
+
["MEDIUM" /* MEDIUM */]: 2,
|
|
91
|
+
["HIGH" /* HIGH */]: 3,
|
|
92
|
+
["CRITICAL" /* CRITICAL */]: 4
|
|
93
|
+
};
|
|
94
|
+
function compareSeverity(a, b) {
|
|
95
|
+
return SEVERITY_ORDER[a] - SEVERITY_ORDER[b];
|
|
96
|
+
}
|
|
97
|
+
var SecurityConfig = class {
|
|
98
|
+
/** Whether security checks are enabled */
|
|
99
|
+
enabled;
|
|
100
|
+
/** Minimum severity level to check */
|
|
101
|
+
minimumSeverity;
|
|
102
|
+
/** Rule IDs that are disabled */
|
|
103
|
+
disabledRules;
|
|
104
|
+
/** If set, only these rules run (whitelist mode) */
|
|
105
|
+
enabledRules;
|
|
106
|
+
/** Whether LOW severity should block queries */
|
|
107
|
+
failOnLow;
|
|
108
|
+
constructor(options = {}) {
|
|
109
|
+
this.enabled = options.enabled ?? true;
|
|
110
|
+
if (typeof options.minimumSeverity === "string") {
|
|
111
|
+
this.minimumSeverity = RuleSeverity[options.minimumSeverity];
|
|
112
|
+
} else {
|
|
113
|
+
this.minimumSeverity = options.minimumSeverity ?? "HIGH" /* HIGH */;
|
|
114
|
+
}
|
|
115
|
+
if (Array.isArray(options.disabledRules)) {
|
|
116
|
+
this.disabledRules = new Set(options.disabledRules);
|
|
117
|
+
} else {
|
|
118
|
+
this.disabledRules = options.disabledRules ?? /* @__PURE__ */ new Set();
|
|
119
|
+
}
|
|
120
|
+
if (Array.isArray(options.enabledRules)) {
|
|
121
|
+
this.enabledRules = new Set(options.enabledRules);
|
|
122
|
+
} else if (options.enabledRules instanceof Set) {
|
|
123
|
+
this.enabledRules = options.enabledRules;
|
|
124
|
+
} else {
|
|
125
|
+
this.enabledRules = null;
|
|
126
|
+
}
|
|
127
|
+
this.failOnLow = options.failOnLow ?? false;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Check if a rule should run based on this configuration.
|
|
131
|
+
*/
|
|
132
|
+
shouldRunRule(ruleId, severity) {
|
|
133
|
+
if (!this.enabled) return false;
|
|
134
|
+
if (this.disabledRules.has(ruleId)) return false;
|
|
135
|
+
if (this.enabledRules !== null && !this.enabledRules.has(ruleId)) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if (compareSeverity(severity, this.minimumSeverity) < 0) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Check if a finding at this severity should fail validation.
|
|
145
|
+
*/
|
|
146
|
+
shouldFail(severity) {
|
|
147
|
+
if (severity === "LOW" /* LOW */ && !this.failOnLow) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// src/rules/index.ts
|
|
155
|
+
var systemCommandRule = {
|
|
156
|
+
ruleId: "system-command",
|
|
157
|
+
name: "System Command Detection",
|
|
158
|
+
description: "Detects xp_cmdshell, xp_regread, and other system command functions",
|
|
159
|
+
severity: "CRITICAL" /* CRITICAL */,
|
|
160
|
+
check(sql) {
|
|
161
|
+
const patterns = [
|
|
162
|
+
/\bxp_cmdshell\s*\(/i,
|
|
163
|
+
/\bxp_regread\s*\(/i,
|
|
164
|
+
/\bxp_regwrite\s*\(/i,
|
|
165
|
+
/\bxp_servicecontrol\s*\(/i,
|
|
166
|
+
/\bsp_oacreate\s*\(/i,
|
|
167
|
+
/\bsp_oamethod\s*\(/i
|
|
168
|
+
];
|
|
169
|
+
for (const pattern of patterns) {
|
|
170
|
+
const match = sql.match(pattern);
|
|
171
|
+
if (match) {
|
|
172
|
+
const funcName = match[0].replace(/\s*\($/, "");
|
|
173
|
+
return {
|
|
174
|
+
ruleId: this.ruleId,
|
|
175
|
+
message: `System command function '${funcName}' detected`,
|
|
176
|
+
severity: this.severity
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
var fileAccessRule = {
|
|
184
|
+
ruleId: "file-access",
|
|
185
|
+
name: "File Access Detection",
|
|
186
|
+
description: "Detects INTO OUTFILE, LOAD DATA, COPY, pg_read_file, etc.",
|
|
187
|
+
severity: "CRITICAL" /* CRITICAL */,
|
|
188
|
+
check(sql) {
|
|
189
|
+
const patterns = [
|
|
190
|
+
{ pattern: /\bINTO\s+OUTFILE\b/i, message: "INTO OUTFILE clause detected" },
|
|
191
|
+
{ pattern: /\bINTO\s+DUMPFILE\b/i, message: "INTO DUMPFILE clause detected" },
|
|
192
|
+
{ pattern: /\bLOAD\s+DATA\s+INFILE\b/i, message: "LOAD DATA INFILE detected" },
|
|
193
|
+
{ pattern: /\bLOAD_FILE\s*\(/i, message: "LOAD_FILE function detected" },
|
|
194
|
+
{ pattern: /\bpg_read_file\s*\(/i, message: "Dangerous file function 'pg_read_file' detected" },
|
|
195
|
+
{ pattern: /\bpg_read_binary_file\s*\(/i, message: "pg_read_binary_file function detected" },
|
|
196
|
+
{ pattern: /\bCOPY\s+\w+\s+(TO|FROM)\b/i, message: "COPY command detected" }
|
|
197
|
+
];
|
|
198
|
+
for (const { pattern, message } of patterns) {
|
|
199
|
+
if (pattern.test(sql)) {
|
|
200
|
+
return {
|
|
201
|
+
ruleId: this.ruleId,
|
|
202
|
+
message,
|
|
203
|
+
severity: this.severity
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
var dynamicSqlRule = {
|
|
211
|
+
ruleId: "dynamic-sql",
|
|
212
|
+
name: "Dynamic SQL Detection",
|
|
213
|
+
description: "Detects EXEC, EXECUTE, PREPARE statements",
|
|
214
|
+
severity: "CRITICAL" /* CRITICAL */,
|
|
215
|
+
check(sql) {
|
|
216
|
+
const patterns = [
|
|
217
|
+
{ pattern: /\bEXEC\s*\(/i, message: "EXEC statement detected" },
|
|
218
|
+
{ pattern: /\bEXECUTE\s+/i, message: "EXECUTE statement detected" },
|
|
219
|
+
{ pattern: /\bPREPARE\s+\w+\s+FROM\b/i, message: "PREPARE statement detected" },
|
|
220
|
+
{ pattern: /\bsp_executesql\b/i, message: "sp_executesql detected" }
|
|
221
|
+
];
|
|
222
|
+
for (const { pattern, message } of patterns) {
|
|
223
|
+
if (pattern.test(sql)) {
|
|
224
|
+
return {
|
|
225
|
+
ruleId: this.ruleId,
|
|
226
|
+
message,
|
|
227
|
+
severity: this.severity
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
var privilegeEscalationRule = {
|
|
235
|
+
ruleId: "privilege-escalation",
|
|
236
|
+
name: "Privilege Escalation Detection",
|
|
237
|
+
description: "Detects CREATE USER, ALTER USER, SET ROLE",
|
|
238
|
+
severity: "CRITICAL" /* CRITICAL */,
|
|
239
|
+
check(sql) {
|
|
240
|
+
const patterns = [
|
|
241
|
+
{ pattern: /\bCREATE\s+USER\b/i, message: "CREATE USER detected" },
|
|
242
|
+
{ pattern: /\bALTER\s+USER\b/i, message: "ALTER USER detected" },
|
|
243
|
+
{ pattern: /\bSET\s+ROLE\b/i, message: "SET ROLE detected" },
|
|
244
|
+
{ pattern: /\bGRANT\b/i, message: "GRANT statement detected" },
|
|
245
|
+
{ pattern: /\bSUPERUSER\b/i, message: "SUPERUSER privilege detected" }
|
|
246
|
+
];
|
|
247
|
+
for (const { pattern, message } of patterns) {
|
|
248
|
+
if (pattern.test(sql)) {
|
|
249
|
+
return {
|
|
250
|
+
ruleId: this.ruleId,
|
|
251
|
+
message,
|
|
252
|
+
severity: this.severity
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
var storedProcedureRule = {
|
|
260
|
+
ruleId: "stored-procedure",
|
|
261
|
+
name: "Stored Procedure Detection",
|
|
262
|
+
description: "Detects CALL statements",
|
|
263
|
+
severity: "HIGH" /* HIGH */,
|
|
264
|
+
check(sql) {
|
|
265
|
+
if (/\bCALL\s+\w+/i.test(sql)) {
|
|
266
|
+
return {
|
|
267
|
+
ruleId: this.ruleId,
|
|
268
|
+
message: "CALL statement detected",
|
|
269
|
+
severity: this.severity
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
var dangerousFunctionsRule = {
|
|
276
|
+
ruleId: "dangerous-functions",
|
|
277
|
+
name: "Dangerous Functions Detection",
|
|
278
|
+
description: "Detects SLEEP, BENCHMARK, and similar DoS-prone functions",
|
|
279
|
+
severity: "MEDIUM" /* MEDIUM */,
|
|
280
|
+
check(sql) {
|
|
281
|
+
const patterns = [
|
|
282
|
+
{ pattern: /\bSLEEP\s*\(/i, message: "SLEEP function detected (timing attack)" },
|
|
283
|
+
{ pattern: /\bpg_sleep\s*\(/i, message: "pg_sleep function detected (timing attack)" },
|
|
284
|
+
{ pattern: /\bBENCHMARK\s*\(/i, message: "BENCHMARK function detected (DoS vector)" },
|
|
285
|
+
{ pattern: /\bWAITFOR\s+DELAY\b/i, message: "WAITFOR DELAY detected (timing attack)" }
|
|
286
|
+
];
|
|
287
|
+
for (const { pattern, message } of patterns) {
|
|
288
|
+
if (pattern.test(sql)) {
|
|
289
|
+
return {
|
|
290
|
+
ruleId: this.ruleId,
|
|
291
|
+
message,
|
|
292
|
+
severity: this.severity
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
var hexEncodingRule = {
|
|
300
|
+
ruleId: "hex-encoding",
|
|
301
|
+
name: "Hex Encoding Detection",
|
|
302
|
+
description: "Detects hex-encoded SQL keywords like 0x44524F50 (DROP)",
|
|
303
|
+
severity: "MEDIUM" /* MEDIUM */,
|
|
304
|
+
check(sql) {
|
|
305
|
+
const hexPattern = /0x([0-9A-Fa-f]{6,})/g;
|
|
306
|
+
const dangerousKeywords = ["DROP", "DELETE", "TRUNCATE", "ALTER", "EXEC", "UNION", "SELECT"];
|
|
307
|
+
let match;
|
|
308
|
+
while ((match = hexPattern.exec(sql)) !== null) {
|
|
309
|
+
try {
|
|
310
|
+
const hexValue = match[1];
|
|
311
|
+
let decoded = "";
|
|
312
|
+
for (let i = 0; i < hexValue.length; i += 2) {
|
|
313
|
+
decoded += String.fromCharCode(parseInt(hexValue.substr(i, 2), 16));
|
|
314
|
+
}
|
|
315
|
+
const decodedUpper = decoded.toUpperCase();
|
|
316
|
+
for (const keyword of dangerousKeywords) {
|
|
317
|
+
if (decodedUpper.includes(keyword)) {
|
|
318
|
+
return {
|
|
319
|
+
ruleId: this.ruleId,
|
|
320
|
+
message: `Hex-encoded SQL keyword detected: '${keyword}' in ${match[0]}`,
|
|
321
|
+
severity: this.severity
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} catch {
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
var charFunctionRule = {
|
|
332
|
+
ruleId: "char-function",
|
|
333
|
+
name: "CHAR Function Detection",
|
|
334
|
+
description: "Detects CHAR() function constructing SQL keywords",
|
|
335
|
+
severity: "MEDIUM" /* MEDIUM */,
|
|
336
|
+
check(sql) {
|
|
337
|
+
const dangerousKeywords = ["DROP", "DELETE", "TRUNCATE", "ALTER", "EXEC", "UNION"];
|
|
338
|
+
const charPattern = /\bCH(?:AR|R)\s*\(\s*(\d+)\s*\)/gi;
|
|
339
|
+
const chars = [];
|
|
340
|
+
let match;
|
|
341
|
+
while ((match = charPattern.exec(sql)) !== null) {
|
|
342
|
+
chars.push(parseInt(match[1], 10));
|
|
343
|
+
}
|
|
344
|
+
if (chars.length >= 4) {
|
|
345
|
+
const constructed = chars.map((c) => String.fromCharCode(c)).join("");
|
|
346
|
+
for (const keyword of dangerousKeywords) {
|
|
347
|
+
if (constructed.toUpperCase().includes(keyword)) {
|
|
348
|
+
return {
|
|
349
|
+
ruleId: this.ruleId,
|
|
350
|
+
message: `CHAR()-constructed SQL keyword detected: '${keyword}'`,
|
|
351
|
+
severity: this.severity
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
var stringConcatRule = {
|
|
360
|
+
ruleId: "string-concat",
|
|
361
|
+
name: "String Concatenation Detection",
|
|
362
|
+
description: "Detects string concatenation building keywords like 'DR' || 'OP'",
|
|
363
|
+
severity: "MEDIUM" /* MEDIUM */,
|
|
364
|
+
check(sql) {
|
|
365
|
+
const dangerousKeywords = ["DROP", "DELETE", "TRUNCATE", "ALTER", "EXEC", "UNION"];
|
|
366
|
+
const concatPattern = /'([^']+)'\s*\|\|\s*'([^']+)'/g;
|
|
367
|
+
let match;
|
|
368
|
+
while ((match = concatPattern.exec(sql)) !== null) {
|
|
369
|
+
const combined = (match[1] + match[2]).toUpperCase();
|
|
370
|
+
for (const keyword of dangerousKeywords) {
|
|
371
|
+
if (combined.includes(keyword)) {
|
|
372
|
+
return {
|
|
373
|
+
ruleId: this.ruleId,
|
|
374
|
+
message: `String concatenation building '${keyword}' detected`,
|
|
375
|
+
severity: this.severity
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
var unicodeObfuscationRule = {
|
|
384
|
+
ruleId: "unicode-obfuscation",
|
|
385
|
+
name: "Unicode Obfuscation Detection",
|
|
386
|
+
description: "Detects Cyrillic/Greek characters masquerading as ASCII",
|
|
387
|
+
severity: "HIGH" /* HIGH */,
|
|
388
|
+
check(sql) {
|
|
389
|
+
const homoglyphs = {
|
|
390
|
+
"\u0430": "a",
|
|
391
|
+
// Cyrillic а
|
|
392
|
+
"\u0435": "e",
|
|
393
|
+
// Cyrillic е
|
|
394
|
+
"\u043E": "o",
|
|
395
|
+
// Cyrillic о
|
|
396
|
+
"\u0440": "p",
|
|
397
|
+
// Cyrillic р
|
|
398
|
+
"\u0441": "c",
|
|
399
|
+
// Cyrillic с
|
|
400
|
+
"\u0445": "x",
|
|
401
|
+
// Cyrillic х
|
|
402
|
+
"\u0443": "y",
|
|
403
|
+
// Cyrillic у
|
|
404
|
+
"\u0456": "i",
|
|
405
|
+
// Cyrillic і
|
|
406
|
+
"\u0391": "A",
|
|
407
|
+
// Greek Α
|
|
408
|
+
"\u0392": "B",
|
|
409
|
+
// Greek Β
|
|
410
|
+
"\u0395": "E",
|
|
411
|
+
// Greek Ε
|
|
412
|
+
"\u0397": "H",
|
|
413
|
+
// Greek Η
|
|
414
|
+
"\u0399": "I",
|
|
415
|
+
// Greek Ι
|
|
416
|
+
"\u039A": "K",
|
|
417
|
+
// Greek Κ
|
|
418
|
+
"\u039C": "M",
|
|
419
|
+
// Greek Μ
|
|
420
|
+
"\u039D": "N",
|
|
421
|
+
// Greek Ν
|
|
422
|
+
"\u039F": "O",
|
|
423
|
+
// Greek Ο
|
|
424
|
+
"\u03A1": "P",
|
|
425
|
+
// Greek Ρ
|
|
426
|
+
"\u03A4": "T",
|
|
427
|
+
// Greek Τ
|
|
428
|
+
"\u03A7": "X",
|
|
429
|
+
// Greek Χ
|
|
430
|
+
"\u03A5": "Y",
|
|
431
|
+
// Greek Υ
|
|
432
|
+
"\u0417": "Z"
|
|
433
|
+
// Greek Ζ
|
|
434
|
+
};
|
|
435
|
+
for (const char of sql) {
|
|
436
|
+
if (homoglyphs[char]) {
|
|
437
|
+
return {
|
|
438
|
+
ruleId: this.ruleId,
|
|
439
|
+
message: `Unicode homoglyphs detected - possible keyword obfuscation`,
|
|
440
|
+
severity: this.severity
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
var transactionAbuseRule = {
|
|
448
|
+
ruleId: "transaction-abuse",
|
|
449
|
+
name: "Transaction Abuse Detection",
|
|
450
|
+
description: "Detects LOCK TABLE and other DoS vectors",
|
|
451
|
+
severity: "MEDIUM" /* MEDIUM */,
|
|
452
|
+
check(sql) {
|
|
453
|
+
if (/\bLOCK\s+TABLE\b/i.test(sql)) {
|
|
454
|
+
return {
|
|
455
|
+
ruleId: this.ruleId,
|
|
456
|
+
message: "LOCK TABLE detected (DoS vector)",
|
|
457
|
+
severity: this.severity
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
var metadataAccessRule = {
|
|
464
|
+
ruleId: "metadata-access",
|
|
465
|
+
name: "Metadata Access Detection",
|
|
466
|
+
description: "Detects access to information_schema, pg_catalog, etc.",
|
|
467
|
+
severity: "LOW" /* LOW */,
|
|
468
|
+
check(sql) {
|
|
469
|
+
const patterns = [
|
|
470
|
+
{ pattern: /\binformation_schema\b/i, message: "Access to information_schema detected" },
|
|
471
|
+
{ pattern: /\bpg_catalog\b/i, message: "Access to pg_catalog detected" },
|
|
472
|
+
{ pattern: /\bpg_\w+\b/i, message: "Access to PostgreSQL system table detected" },
|
|
473
|
+
{ pattern: /\bmysql\.\w+\b/i, message: "Access to MySQL system table detected" },
|
|
474
|
+
{ pattern: /\bsys\.\w+\b/i, message: "Access to sys schema detected" }
|
|
475
|
+
];
|
|
476
|
+
for (const { pattern, message } of patterns) {
|
|
477
|
+
if (pattern.test(sql)) {
|
|
478
|
+
return {
|
|
479
|
+
ruleId: this.ruleId,
|
|
480
|
+
message,
|
|
481
|
+
severity: this.severity
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
var schemaCommandRule = {
|
|
489
|
+
ruleId: "schema-commands",
|
|
490
|
+
name: "Schema Command Detection",
|
|
491
|
+
description: "Detects SHOW TABLES, DESCRIBE, EXPLAIN",
|
|
492
|
+
severity: "LOW" /* LOW */,
|
|
493
|
+
check(sql) {
|
|
494
|
+
const patterns = [
|
|
495
|
+
{ pattern: /\bSHOW\s+TABLES\b/i, message: "SHOW TABLES detected" },
|
|
496
|
+
{ pattern: /\bSHOW\s+DATABASES\b/i, message: "SHOW DATABASES detected" },
|
|
497
|
+
{ pattern: /\bDESCRIBE\s+\w+\b/i, message: "DESCRIBE command detected" },
|
|
498
|
+
{ pattern: /\bDESC\s+\w+\b/i, message: "DESC command detected" }
|
|
499
|
+
];
|
|
500
|
+
for (const { pattern, message } of patterns) {
|
|
501
|
+
if (pattern.test(sql)) {
|
|
502
|
+
return {
|
|
503
|
+
ruleId: this.ruleId,
|
|
504
|
+
message,
|
|
505
|
+
severity: this.severity
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
var ALL_RULES = [
|
|
513
|
+
systemCommandRule,
|
|
514
|
+
fileAccessRule,
|
|
515
|
+
dynamicSqlRule,
|
|
516
|
+
privilegeEscalationRule,
|
|
517
|
+
storedProcedureRule,
|
|
518
|
+
dangerousFunctionsRule,
|
|
519
|
+
hexEncodingRule,
|
|
520
|
+
charFunctionRule,
|
|
521
|
+
stringConcatRule,
|
|
522
|
+
unicodeObfuscationRule,
|
|
523
|
+
transactionAbuseRule,
|
|
524
|
+
metadataAccessRule,
|
|
525
|
+
schemaCommandRule
|
|
526
|
+
];
|
|
527
|
+
function runSecurityRules(sql, ast, config) {
|
|
528
|
+
for (const rule of ALL_RULES) {
|
|
529
|
+
if (!config.shouldRunRule(rule.ruleId, rule.severity)) {
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
const violation = rule.check(sql, ast);
|
|
533
|
+
if (violation && config.shouldFail(violation.severity)) {
|
|
534
|
+
return ValidationResult.unsafe(violation.message);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return ValidationResult.safe();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/validator.ts
|
|
541
|
+
var READ_ONLY_ALLOWED = /* @__PURE__ */ new Set(["SELECT"]);
|
|
542
|
+
var WRITE_SAFE_ALLOWED = /* @__PURE__ */ new Set(["SELECT", "INSERT", "UPDATE"]);
|
|
543
|
+
var WRITE_SAFE_BLOCKED = /* @__PURE__ */ new Set([
|
|
544
|
+
"DELETE",
|
|
545
|
+
"DROP",
|
|
546
|
+
"TRUNCATE",
|
|
547
|
+
"ALTER",
|
|
548
|
+
"CREATE",
|
|
549
|
+
"GRANT",
|
|
550
|
+
"REVOKE"
|
|
551
|
+
]);
|
|
552
|
+
var Validator = class {
|
|
553
|
+
mode;
|
|
554
|
+
allowedTables;
|
|
555
|
+
allowedStatements;
|
|
556
|
+
blockedStatements;
|
|
557
|
+
dialect;
|
|
558
|
+
securityConfig;
|
|
559
|
+
parser;
|
|
560
|
+
constructor(options = {}) {
|
|
561
|
+
this.mode = options.mode ?? "read_only";
|
|
562
|
+
this.dialect = options.dialect ?? "postgresql";
|
|
563
|
+
this.parser = new import_node_sql_parser.Parser();
|
|
564
|
+
if (options.allowedTables) {
|
|
565
|
+
this.allowedTables = new Set(options.allowedTables.map((t) => t.toLowerCase()));
|
|
566
|
+
} else {
|
|
567
|
+
this.allowedTables = null;
|
|
568
|
+
}
|
|
569
|
+
if (options.securityConfig === false) {
|
|
570
|
+
this.securityConfig = null;
|
|
571
|
+
} else if (options.securityConfig === true || options.securityConfig === void 0) {
|
|
572
|
+
this.securityConfig = new SecurityConfig();
|
|
573
|
+
} else if (options.securityConfig instanceof SecurityConfig) {
|
|
574
|
+
this.securityConfig = options.securityConfig;
|
|
575
|
+
} else {
|
|
576
|
+
this.securityConfig = new SecurityConfig(options.securityConfig);
|
|
577
|
+
}
|
|
578
|
+
this.allowedStatements = this.initAllowedStatements(options);
|
|
579
|
+
this.blockedStatements = this.initBlockedStatements(options);
|
|
580
|
+
}
|
|
581
|
+
initAllowedStatements(options) {
|
|
582
|
+
switch (this.mode) {
|
|
583
|
+
case "read_only":
|
|
584
|
+
return READ_ONLY_ALLOWED;
|
|
585
|
+
case "write_safe":
|
|
586
|
+
return WRITE_SAFE_ALLOWED;
|
|
587
|
+
case "custom":
|
|
588
|
+
if (options.allowedStatements) {
|
|
589
|
+
return new Set(options.allowedStatements.map((s) => s.toUpperCase()));
|
|
590
|
+
}
|
|
591
|
+
return null;
|
|
592
|
+
default:
|
|
593
|
+
return READ_ONLY_ALLOWED;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
initBlockedStatements(options) {
|
|
597
|
+
switch (this.mode) {
|
|
598
|
+
case "read_only":
|
|
599
|
+
return null;
|
|
600
|
+
// read_only uses allowlist only
|
|
601
|
+
case "write_safe":
|
|
602
|
+
return WRITE_SAFE_BLOCKED;
|
|
603
|
+
case "custom":
|
|
604
|
+
if (options.blockedStatements) {
|
|
605
|
+
return new Set(options.blockedStatements.map((s) => s.toUpperCase()));
|
|
606
|
+
}
|
|
607
|
+
return null;
|
|
608
|
+
default:
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Validate a SQL query.
|
|
614
|
+
*/
|
|
615
|
+
validate(sql) {
|
|
616
|
+
if (!sql || !sql.trim()) {
|
|
617
|
+
return ValidationResult.unsafe("Query is empty or contains only whitespace");
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
const ast = this.parser.astify(sql, { database: this.mapDialect(this.dialect) });
|
|
621
|
+
const statements = Array.isArray(ast) ? ast : [ast];
|
|
622
|
+
if (statements.length === 0) {
|
|
623
|
+
return ValidationResult.unsafe("No valid SQL statements found");
|
|
624
|
+
}
|
|
625
|
+
for (const stmt of statements) {
|
|
626
|
+
const typeResult = this.checkStatementType(stmt);
|
|
627
|
+
if (!typeResult.isSafe) return typeResult;
|
|
628
|
+
const tableResult = this.checkTables(stmt, sql);
|
|
629
|
+
if (!tableResult.isSafe) return tableResult;
|
|
630
|
+
}
|
|
631
|
+
if (this.securityConfig) {
|
|
632
|
+
const securityResult = runSecurityRules(sql, ast, this.securityConfig);
|
|
633
|
+
if (!securityResult.isSafe) return securityResult;
|
|
634
|
+
}
|
|
635
|
+
const firstStmt = statements[0] ?? null;
|
|
636
|
+
return ValidationResult.safe({
|
|
637
|
+
statementType: this.getStatementType(firstStmt),
|
|
638
|
+
tables: this.extractAllTables(statements)
|
|
639
|
+
});
|
|
640
|
+
} catch (error) {
|
|
641
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
642
|
+
return ValidationResult.unsafe(`SQL parse error: ${message}`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
mapDialect(dialect) {
|
|
646
|
+
const dialectMap = {
|
|
647
|
+
postgres: "postgresql",
|
|
648
|
+
postgresql: "postgresql",
|
|
649
|
+
mysql: "mysql",
|
|
650
|
+
sqlite: "sqlite",
|
|
651
|
+
snowflake: "snowflake",
|
|
652
|
+
bigquery: "bigquery",
|
|
653
|
+
redshift: "redshift",
|
|
654
|
+
hive: "hive",
|
|
655
|
+
spark: "spark",
|
|
656
|
+
trino: "trino",
|
|
657
|
+
presto: "trino",
|
|
658
|
+
flinksql: "flinksql",
|
|
659
|
+
transactsql: "transactsql",
|
|
660
|
+
mssql: "transactsql",
|
|
661
|
+
sqlserver: "transactsql"
|
|
662
|
+
};
|
|
663
|
+
return dialectMap[dialect.toLowerCase()] ?? "postgresql";
|
|
664
|
+
}
|
|
665
|
+
getStatementType(stmt) {
|
|
666
|
+
if (!stmt) return "UNKNOWN";
|
|
667
|
+
return stmt.type?.toUpperCase() ?? "UNKNOWN";
|
|
668
|
+
}
|
|
669
|
+
checkStatementType(stmt) {
|
|
670
|
+
const stmtType = this.getStatementType(stmt);
|
|
671
|
+
if (this.blockedStatements?.has(stmtType)) {
|
|
672
|
+
return ValidationResult.unsafe(
|
|
673
|
+
`Statement type '${stmtType}' is blocked in ${this.mode} mode`
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
if (this.allowedStatements && !this.allowedStatements.has(stmtType)) {
|
|
677
|
+
return ValidationResult.unsafe(
|
|
678
|
+
`Statement type '${stmtType}' is not allowed in ${this.mode} mode`
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
return ValidationResult.safe();
|
|
682
|
+
}
|
|
683
|
+
checkTables(stmt, _sql) {
|
|
684
|
+
if (!this.allowedTables) return ValidationResult.safe();
|
|
685
|
+
const tables = this.extractTables(stmt);
|
|
686
|
+
for (const table of tables) {
|
|
687
|
+
const tableLower = table.toLowerCase();
|
|
688
|
+
if (!this.allowedTables.has(tableLower)) {
|
|
689
|
+
return ValidationResult.unsafe(
|
|
690
|
+
`Table '${table}' is not in allowed tables list`
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return ValidationResult.safe();
|
|
695
|
+
}
|
|
696
|
+
extractTables(stmt) {
|
|
697
|
+
const tables = [];
|
|
698
|
+
this.walkAst(stmt, (node) => {
|
|
699
|
+
if (node && typeof node === "object") {
|
|
700
|
+
if (node.table && typeof node.table === "string") {
|
|
701
|
+
tables.push(node.table);
|
|
702
|
+
}
|
|
703
|
+
if (Array.isArray(node.from)) {
|
|
704
|
+
for (const item of node.from) {
|
|
705
|
+
if (item?.table) tables.push(item.table);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
return [...new Set(tables)];
|
|
711
|
+
}
|
|
712
|
+
extractAllTables(statements) {
|
|
713
|
+
const allTables = [];
|
|
714
|
+
for (const stmt of statements) {
|
|
715
|
+
allTables.push(...this.extractTables(stmt));
|
|
716
|
+
}
|
|
717
|
+
return [...new Set(allTables)];
|
|
718
|
+
}
|
|
719
|
+
walkAst(node, callback) {
|
|
720
|
+
if (!node || typeof node !== "object") return;
|
|
721
|
+
callback(node);
|
|
722
|
+
if (Array.isArray(node)) {
|
|
723
|
+
for (const item of node) {
|
|
724
|
+
this.walkAst(item, callback);
|
|
725
|
+
}
|
|
726
|
+
} else {
|
|
727
|
+
for (const key of Object.keys(node)) {
|
|
728
|
+
this.walkAst(node[key], callback);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
// src/index.ts
|
|
735
|
+
var defaultValidator = new Validator({ mode: "read_only" });
|
|
736
|
+
function validate(sql, options) {
|
|
737
|
+
const isDefault = !options || (options.mode === "read_only" || options.mode === void 0) && !options.allowedTables && !options.dialect && (options.security === true || options.security === void 0);
|
|
738
|
+
if (isDefault) {
|
|
739
|
+
return defaultValidator.validate(sql);
|
|
740
|
+
}
|
|
741
|
+
const validatorOptions = {
|
|
742
|
+
mode: options?.mode ?? "read_only",
|
|
743
|
+
allowedTables: options?.allowedTables,
|
|
744
|
+
dialect: options?.dialect,
|
|
745
|
+
securityConfig: options?.security
|
|
746
|
+
};
|
|
747
|
+
const validator = new Validator(validatorOptions);
|
|
748
|
+
return validator.validate(sql);
|
|
749
|
+
}
|
|
750
|
+
function isSafe(sql, options) {
|
|
751
|
+
return validate(sql, options).isSafe;
|
|
752
|
+
}
|
|
753
|
+
var index_default = {
|
|
754
|
+
validate,
|
|
755
|
+
isSafe,
|
|
756
|
+
Validator
|
|
757
|
+
};
|
|
758
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
759
|
+
0 && (module.exports = {
|
|
760
|
+
RuleSeverity,
|
|
761
|
+
SecurityConfig,
|
|
762
|
+
ValidationResult,
|
|
763
|
+
Validator,
|
|
764
|
+
isSafe,
|
|
765
|
+
validate
|
|
766
|
+
});
|