@blamejs/blamejs-shop 0.5.7 → 0.5.8
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 +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/cost-layers.js +86 -49
- package/lib/plan-changes.js +14 -0
- package/lib/vendor/MANIFEST.json +31 -15
- package/lib/vendor/blamejs/.github/workflows/release-container.yml +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/examples/wiki/package-lock.json +1 -1
- package/lib/vendor/blamejs/lib/content-credentials.js +43 -2
- package/lib/vendor/blamejs/lib/guard-sql.js +29 -0
- package/lib/vendor/blamejs/lib/mcp.js +37 -4
- package/lib/vendor/blamejs/lib/metrics.js +15 -3
- package/lib/vendor/blamejs/lib/middleware/tus-upload.js +9 -2
- package/lib/vendor/blamejs/lib/parsers/safe-yaml.js +33 -2
- package/lib/vendor/blamejs/lib/safe-buffer.js +11 -1
- package/lib/vendor/blamejs/package-lock.json +2 -2
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.16.3.json +71 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials-coverage.test.js +319 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cycle2-bugfixes.test.js +142 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-sql-coverage.test.js +432 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mcp-coverage.test.js +443 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/metrics-coverage.test.js +395 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/middleware-tus-upload-coverage.test.js +402 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/parsers-safe-yaml-coverage.test.js +292 -0
- package/package.json +1 -1
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* guard-sql-coverage — error / adversarial-branch coverage for
|
|
6
|
+
* b.guardSql (the raw-SQL content-safety primitive).
|
|
7
|
+
*
|
|
8
|
+
* The smoke + integration suites already exercise the happy path (a
|
|
9
|
+
* clean parameterized fragment serves; a stacked statement refuses).
|
|
10
|
+
* This file drives the ERROR-HANDLING and ADVERSARIAL-INPUT branches
|
|
11
|
+
* those never reach:
|
|
12
|
+
*
|
|
13
|
+
* - opts validation throws (bad contextMode / profile / posture,
|
|
14
|
+
* non-string sanitize input);
|
|
15
|
+
* - the wrong-type validate result (bad-input, not a throw);
|
|
16
|
+
* - the UTF-8 encoding gate's every invalid-sequence branch
|
|
17
|
+
* (overlong / out-of-range lead, stray + truncated + out-of-range
|
|
18
|
+
* continuation, surrogate range, replacement-char decode);
|
|
19
|
+
* - the byte-size cap;
|
|
20
|
+
* - CVE-2025-8715 quoted-identifier hazards (newline / backslash /
|
|
21
|
+
* null / control / over-length);
|
|
22
|
+
* - the leading procedural-execution verb floor (DO / CALL / EXECUTE);
|
|
23
|
+
* - the comment / literal / stacked smuggling floor (incl. the classes
|
|
24
|
+
* that STILL refuse under `permissive`);
|
|
25
|
+
* - fragment / operator-sql / migration structural rules;
|
|
26
|
+
* - the Postgres / SQLite / MySQL OS-reach floor (refuses at EVERY
|
|
27
|
+
* profile) vs the recon / timing families (soften per profile);
|
|
28
|
+
* - sanitize's refuse-throw path and gate's serve / audit-only /
|
|
29
|
+
* refuse / fail-closed dispositions.
|
|
30
|
+
*
|
|
31
|
+
* Run standalone: node test/layer-0-primitives/guard-sql-coverage.test.js
|
|
32
|
+
* Or via smoke: node test/smoke.js
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
var helpers = require("../helpers");
|
|
36
|
+
var b = helpers.b;
|
|
37
|
+
var check = helpers.check;
|
|
38
|
+
|
|
39
|
+
var BYTES = b.constants.BYTES;
|
|
40
|
+
|
|
41
|
+
var NUL = String.fromCharCode(0);
|
|
42
|
+
var LF = String.fromCharCode(10);
|
|
43
|
+
var BEL = String.fromCharCode(7); // 0x07 C0 control
|
|
44
|
+
var FFFD = String.fromCharCode(0xFFFD); // Unicode replacement char
|
|
45
|
+
|
|
46
|
+
// ---- terse assertion helpers (avoid duplicated setup blocks) ----
|
|
47
|
+
|
|
48
|
+
// validate() refuses (ok:false) AND surfaces the named issue kind.
|
|
49
|
+
function refusesWith(label, input, opts, kind) {
|
|
50
|
+
var rv = b.guardSql.validate(input, opts);
|
|
51
|
+
check(label + " -> ok:false", rv.ok === false);
|
|
52
|
+
check(label + " -> kind " + kind,
|
|
53
|
+
rv.issues.some(function (i) { return i.kind === kind; }));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// validate() passes clean (ok:true).
|
|
57
|
+
function passesClean(label, input, opts) {
|
|
58
|
+
var rv = b.guardSql.validate(input, opts);
|
|
59
|
+
check(label + " -> ok:true", rv.ok === true);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// validate() passes (ok:true) but surfaces a warn-level audited issue.
|
|
63
|
+
function auditsWith(label, input, opts, kind) {
|
|
64
|
+
var rv = b.guardSql.validate(input, opts);
|
|
65
|
+
check(label + " -> ok:true (audited)", rv.ok === true);
|
|
66
|
+
check(label + " -> audited kind " + kind,
|
|
67
|
+
rv.issues.some(function (i) { return i.kind === kind && i.action === "audit"; }));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// A call that must throw a GuardSqlError with a specific code.
|
|
71
|
+
function throwsCode(label, fn, code) {
|
|
72
|
+
var threw = null;
|
|
73
|
+
try { fn(); } catch (e) { threw = e; }
|
|
74
|
+
check(label + " throws", threw !== null);
|
|
75
|
+
check(label + " code " + code, threw && threw.code === code);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---- opts validation — entry-point throws (Tier-A: bad config = throw) ----
|
|
79
|
+
|
|
80
|
+
function testBadOptsThrow() {
|
|
81
|
+
throwsCode("validate: unknown contextMode",
|
|
82
|
+
function () { return b.guardSql.validate("x = 1", { contextMode: "nope" }); },
|
|
83
|
+
"sql.bad-opt");
|
|
84
|
+
throwsCode("validate: unknown profile",
|
|
85
|
+
function () { return b.guardSql.validate("x = 1", { profile: "nope" }); },
|
|
86
|
+
"sql.bad-profile");
|
|
87
|
+
throwsCode("validate: unknown compliancePosture",
|
|
88
|
+
function () { return b.guardSql.validate("x = 1", { compliancePosture: "nope" }); },
|
|
89
|
+
"sql.bad-posture");
|
|
90
|
+
throwsCode("sanitize: non-string / non-Buffer input",
|
|
91
|
+
function () { return b.guardSql.sanitize(42); },
|
|
92
|
+
"sql.bad-input");
|
|
93
|
+
throwsCode("compliancePosture: unknown posture",
|
|
94
|
+
function () { return b.guardSql.compliancePosture("nope"); },
|
|
95
|
+
"sql.bad-posture");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---- validate() bad input — returns a bad-input RESULT, never throws ----
|
|
99
|
+
|
|
100
|
+
function testBadInputResult() {
|
|
101
|
+
var kinds = ["number", "null", "object", "undefined"];
|
|
102
|
+
var inputs = [42, null, {}, undefined];
|
|
103
|
+
for (var i = 0; i < inputs.length; i += 1) {
|
|
104
|
+
var rv = b.guardSql.validate(inputs[i]);
|
|
105
|
+
check("validate(" + kinds[i] + ") -> ok:false", rv.ok === false);
|
|
106
|
+
check("validate(" + kinds[i] + ") -> bad-input",
|
|
107
|
+
rv.issues.some(function (x) { return x.kind === "bad-input"; }));
|
|
108
|
+
}
|
|
109
|
+
// A Buffer of valid SQL is accepted (drives the Buffer decode branch).
|
|
110
|
+
var bufRv = b.guardSql.validate(Buffer.from("id = ? AND active = ?", "utf8"));
|
|
111
|
+
check("validate(Buffer clean) -> ok:true", bufRv.ok === true);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---- Stage 1: UTF-8 encoding gate — every invalid-sequence branch ----
|
|
115
|
+
|
|
116
|
+
function testEncodingGate() {
|
|
117
|
+
// Overlong / out-of-range lead bytes (0xC0 / 0xC1 only encode overlong
|
|
118
|
+
// ASCII; 0xF5.. exceeds the Unicode max code point).
|
|
119
|
+
refusesWith("enc: 0xC0 overlong lead",
|
|
120
|
+
Buffer.from([0x78, 0xC0, 0x80]), null, "invalid-encoding");
|
|
121
|
+
refusesWith("enc: 0xC1 overlong lead",
|
|
122
|
+
Buffer.from([0x78, 0xC1, 0x80]), null, "invalid-encoding");
|
|
123
|
+
refusesWith("enc: 0xF5 out-of-range lead",
|
|
124
|
+
Buffer.from([0x78, 0xF5, 0x80, 0x80, 0x80]), null, "invalid-encoding");
|
|
125
|
+
// Stray continuation byte where a lead byte is expected.
|
|
126
|
+
refusesWith("enc: stray continuation 0x80",
|
|
127
|
+
Buffer.from([0x78, 0x80]), null, "invalid-encoding");
|
|
128
|
+
// Truncated multibyte sequence (lead promises more bytes than present).
|
|
129
|
+
refusesWith("enc: truncated 2-byte (0xE0 0xA0)",
|
|
130
|
+
Buffer.from([0xE0, 0xA0]), null, "invalid-encoding");
|
|
131
|
+
refusesWith("enc: truncated 4-byte (0xF0 0x90 0x80)",
|
|
132
|
+
Buffer.from([0x78, 0xF0, 0x90, 0x80]), null, "invalid-encoding");
|
|
133
|
+
// First continuation below the non-shortest bound (E0 requires >= 0xA0).
|
|
134
|
+
refusesWith("enc: non-shortest 3-byte (0xE0 0x80 0x80)",
|
|
135
|
+
Buffer.from([0xE0, 0x80, 0x80]), null, "invalid-encoding");
|
|
136
|
+
// Non-shortest 4-byte (F0 requires >= 0x90).
|
|
137
|
+
refusesWith("enc: non-shortest 4-byte (0xF0 0x80 0x80 0x80)",
|
|
138
|
+
Buffer.from([0xF0, 0x80, 0x80, 0x80]), null, "invalid-encoding");
|
|
139
|
+
// UTF-16 surrogate range encoded in UTF-8 (ED allows only 0x80..0x9F).
|
|
140
|
+
refusesWith("enc: surrogate range (0xED 0xA0 0x80)",
|
|
141
|
+
Buffer.from([0xED, 0xA0, 0x80]), null, "invalid-encoding");
|
|
142
|
+
// A later continuation byte out of the 0x80..0xBF band.
|
|
143
|
+
refusesWith("enc: bad trailing continuation (0xE0 0xA0 0x00)",
|
|
144
|
+
Buffer.from([0xE0, 0xA0, 0x00]), null, "invalid-encoding");
|
|
145
|
+
// Replacement-char belt-and-suspenders — a string literally carrying
|
|
146
|
+
// U+FFFD means a lossy decode happened somewhere upstream.
|
|
147
|
+
refusesWith("enc: decoded replacement char U+FFFD",
|
|
148
|
+
"id = " + FFFD, null, "invalid-encoding");
|
|
149
|
+
// A VALID multibyte sequence is NOT refused (the gate rejects only
|
|
150
|
+
// malformed bytes, never legal UTF-8).
|
|
151
|
+
passesClean("enc: valid multibyte accepted",
|
|
152
|
+
Buffer.from("id = 1", "utf8"), { contextMode: "operator-sql" });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---- byte-size cap ----
|
|
156
|
+
|
|
157
|
+
function testOversizeCap() {
|
|
158
|
+
var long = "x = 1 and y = 2 and z = 3 and w = 4";
|
|
159
|
+
refusesWith("oversize: exceeds maxBytes",
|
|
160
|
+
long, { maxBytes: BYTES.bytes(8) }, "oversize");
|
|
161
|
+
var rv = b.guardSql.validate(long, { maxBytes: BYTES.bytes(8) });
|
|
162
|
+
check("oversize: severity high",
|
|
163
|
+
rv.issues.some(function (i) { return i.kind === "oversize" && i.severity === "high"; }));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---- CVE-2025-8715 quoted-identifier hygiene ----
|
|
167
|
+
|
|
168
|
+
function testIdentifierHygiene() {
|
|
169
|
+
refusesWith("ident: embedded newline",
|
|
170
|
+
'x = "co' + LF + 'l"', { contextMode: "operator-sql" }, "identifier-hazard");
|
|
171
|
+
refusesWith("ident: leading backslash",
|
|
172
|
+
'x = "\\evil"', { contextMode: "operator-sql" }, "identifier-hazard");
|
|
173
|
+
refusesWith("ident: embedded null byte",
|
|
174
|
+
'x = "a' + NUL + 'b"', { contextMode: "operator-sql" }, "identifier-hazard");
|
|
175
|
+
refusesWith("ident: embedded C0 control byte",
|
|
176
|
+
'x = "a' + BEL + 'b"', { contextMode: "operator-sql" }, "identifier-hazard");
|
|
177
|
+
// 64-char identifier exceeds safeSql's 63-char (Postgres NAMEDATALEN) cap.
|
|
178
|
+
var overLong = new Array(65).join("a");
|
|
179
|
+
refusesWith("ident: over-length (> 63 chars)",
|
|
180
|
+
'x = "' + overLong + '"', { contextMode: "operator-sql" }, "identifier-hazard");
|
|
181
|
+
// A legitimately spaced / mixed-case quoted identifier is NOT a hazard.
|
|
182
|
+
passesClean("ident: clean spaced quoted identifier",
|
|
183
|
+
'SELECT "My Col" FROM t', { contextMode: "operator-sql" });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---- leading procedural-execution verb floor (DO / CALL / EXECUTE) ----
|
|
187
|
+
|
|
188
|
+
function testLeadingVerbFloor() {
|
|
189
|
+
refusesWith("verb-floor: DO anonymous block",
|
|
190
|
+
"DO $body$ BEGIN PERFORM 1; END $body$", { contextMode: "operator-sql" },
|
|
191
|
+
"procedural-exec");
|
|
192
|
+
refusesWith("verb-floor: CALL a routine",
|
|
193
|
+
"CALL do_thing()", { contextMode: "operator-sql" }, "procedural-exec");
|
|
194
|
+
refusesWith("verb-floor: EXECUTE a prepared statement",
|
|
195
|
+
"EXECUTE stmt", { contextMode: "operator-sql" }, "procedural-exec");
|
|
196
|
+
// Floor — refuses even under permissive.
|
|
197
|
+
refusesWith("verb-floor: CALL still refuses under permissive",
|
|
198
|
+
"CALL do_thing()", { contextMode: "operator-sql", profile: "permissive" },
|
|
199
|
+
"procedural-exec");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---- comment / literal / stacked smuggling floor ----
|
|
203
|
+
|
|
204
|
+
function testSmugglingFloor() {
|
|
205
|
+
refusesWith("stacked: second statement after ';'",
|
|
206
|
+
"1; DROP TABLE users", { profile: "strict" }, "stacked-statement");
|
|
207
|
+
// Stacked is an irreducible floor — refuses even under permissive.
|
|
208
|
+
refusesWith("stacked: still refuses under permissive",
|
|
209
|
+
"1; DROP TABLE users", { profile: "permissive" }, "stacked-statement");
|
|
210
|
+
refusesWith("comment: unterminated /* block",
|
|
211
|
+
"id = 1 /* open", { contextMode: "fragment" }, "unterminated-comment");
|
|
212
|
+
refusesWith("comment: unterminated /* still refuses under permissive",
|
|
213
|
+
"id = 1 /* open", { contextMode: "fragment", profile: "permissive" },
|
|
214
|
+
"unterminated-comment");
|
|
215
|
+
refusesWith("comment: MySQL executable /*! */",
|
|
216
|
+
"id = 1 /*! x */", { contextMode: "fragment" }, "executable-comment");
|
|
217
|
+
refusesWith("comment: executable /*! still refuses under permissive",
|
|
218
|
+
"id = 1 /*! x */", { contextMode: "fragment", profile: "permissive" },
|
|
219
|
+
"executable-comment");
|
|
220
|
+
refusesWith("literal: unterminated string literal",
|
|
221
|
+
"id = 'open", { contextMode: "fragment" }, "unterminated-literal");
|
|
222
|
+
refusesWith("literal: unterminated dollar-quote",
|
|
223
|
+
"id = $t$open", { contextMode: "fragment" }, "unterminated-literal");
|
|
224
|
+
// Ordinary comment: strict/balanced refuse, permissive audits.
|
|
225
|
+
refusesWith("comment: ordinary -- refused under strict",
|
|
226
|
+
"id = ? -- note", { contextMode: "fragment", profile: "strict" }, "comment");
|
|
227
|
+
auditsWith("comment: ordinary -- audited under permissive",
|
|
228
|
+
"id = ? -- note", { contextMode: "fragment", profile: "permissive" }, "comment");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---- fragment-mode structural rules ----
|
|
232
|
+
|
|
233
|
+
function testFragmentMode() {
|
|
234
|
+
refusesWith("fragment: statement verb refused",
|
|
235
|
+
"SELECT 1", { contextMode: "fragment" }, "verb-in-fragment");
|
|
236
|
+
refusesWith("fragment: embedded string literal refused",
|
|
237
|
+
"name = 'alice'", { contextMode: "fragment", profile: "strict" }, "embedded-literal");
|
|
238
|
+
// allowLiterals opts back in a deliberate static literal.
|
|
239
|
+
passesClean("fragment: allowLiterals permits a static '...'",
|
|
240
|
+
"name = 'alice'", { contextMode: "fragment", allowLiterals: true });
|
|
241
|
+
refusesWith("fragment: UNION set-operation refused (exfil shape)",
|
|
242
|
+
"id = 1 UNION SELECT pw FROM users", { contextMode: "fragment" },
|
|
243
|
+
"setop-in-fragment");
|
|
244
|
+
// String-literal smuggling: a keyword INSIDE a literal is masked and
|
|
245
|
+
// never fires a detector (operator-sql so no fragment embedded-literal).
|
|
246
|
+
passesClean("mask: keyword inside a literal does not fire a detector",
|
|
247
|
+
"SELECT 'pg_read_file'", { contextMode: "operator-sql", profile: "strict" });
|
|
248
|
+
// Intra-keyword comment collapse: LOAD/**/_FILE fuses to LOAD_FILE.
|
|
249
|
+
refusesWith("mask: comment-split LOAD/**/_FILE collapses and fires",
|
|
250
|
+
"SELECT LOAD/**/_FILE('/x')", { contextMode: "operator-sql", profile: "permissive" },
|
|
251
|
+
"mysql-load-file");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---- operator-sql mode ----
|
|
255
|
+
|
|
256
|
+
function testOperatorSqlMode() {
|
|
257
|
+
// No resolvable leading verb — surfaced as an audited no-verb (ok:true).
|
|
258
|
+
auditsWith("operator-sql: no resolvable verb audited",
|
|
259
|
+
"()", { contextMode: "operator-sql" }, "no-verb");
|
|
260
|
+
// Set operation: strict refuses, permissive audits.
|
|
261
|
+
refusesWith("operator-sql: UNION refused under strict",
|
|
262
|
+
"SELECT a FROM t UNION SELECT b FROM u",
|
|
263
|
+
{ contextMode: "operator-sql", profile: "strict" }, "setop");
|
|
264
|
+
auditsWith("operator-sql: UNION audited under permissive",
|
|
265
|
+
"SELECT a FROM t UNION SELECT b FROM u",
|
|
266
|
+
{ contextMode: "operator-sql", profile: "permissive" }, "setop");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---- migration mode ----
|
|
270
|
+
|
|
271
|
+
function testMigrationMode() {
|
|
272
|
+
// Multiple DDL statements + a read are permitted (no stacked refusal).
|
|
273
|
+
passesClean("migration: multi-statement DDL permitted",
|
|
274
|
+
"CREATE TABLE a(id int); ALTER TABLE a ADD c int",
|
|
275
|
+
{ contextMode: "migration" });
|
|
276
|
+
// A verb outside the DDL / read allowlist refuses.
|
|
277
|
+
refusesWith("migration: GRANT not in the DDL allowlist",
|
|
278
|
+
"GRANT ALL ON t TO u", { contextMode: "migration" }, "migration-verb");
|
|
279
|
+
// The OS-reach floor still applies across the whole migration script.
|
|
280
|
+
refusesWith("migration: COPY ... PROGRAM floor still refuses",
|
|
281
|
+
"CREATE TABLE a(id int); COPY a TO PROGRAM 'sh'",
|
|
282
|
+
{ contextMode: "migration" }, "copy-program");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---- OS-reach floor — refuses at EVERY profile (incl. permissive) ----
|
|
286
|
+
|
|
287
|
+
function testOsReachFloorEverywhere() {
|
|
288
|
+
var opFloor = { contextMode: "operator-sql", profile: "permissive" };
|
|
289
|
+
refusesWith("floor: pg_read_file",
|
|
290
|
+
"SELECT pg_read_file('/etc/passwd')", opFloor, "pg-read-file");
|
|
291
|
+
refusesWith("floor: SQLite load_extension",
|
|
292
|
+
"SELECT load_extension('x')", opFloor, "sqlite-load-extension");
|
|
293
|
+
refusesWith("floor: MySQL LOAD_FILE",
|
|
294
|
+
"SELECT LOAD_FILE('/etc/passwd')", opFloor, "mysql-load-file");
|
|
295
|
+
refusesWith("floor: MySQL INTO OUTFILE",
|
|
296
|
+
"SELECT a INTO OUTFILE '/x' FROM t", opFloor, "into-outfile");
|
|
297
|
+
refusesWith("floor: SQLite ATTACH DATABASE",
|
|
298
|
+
"ATTACH DATABASE 'x' AS y", opFloor, "attach-db");
|
|
299
|
+
refusesWith("floor: Postgres dblink()",
|
|
300
|
+
"SELECT dblink('x', 'y')", opFloor, "dblink");
|
|
301
|
+
refusesWith("floor: CREATE EXTENSION",
|
|
302
|
+
"CREATE EXTENSION plpythonu", opFloor, "create-extension");
|
|
303
|
+
refusesWith("floor: ALTER SYSTEM",
|
|
304
|
+
"ALTER SYSTEM SET x = 1", opFloor, "alter-system");
|
|
305
|
+
refusesWith("floor: MySQL sys_exec()",
|
|
306
|
+
"SELECT sys_exec('id')", opFloor, "mysql-sys-exec");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ---- recon / timing families soften per profile (NOT floor) ----
|
|
310
|
+
|
|
311
|
+
function testReconTimingSoftenByProfile() {
|
|
312
|
+
refusesWith("recon: information_schema refused under strict",
|
|
313
|
+
"SELECT * FROM information_schema.tables",
|
|
314
|
+
{ contextMode: "operator-sql", profile: "strict" }, "schema-recon");
|
|
315
|
+
auditsWith("recon: information_schema audited under balanced",
|
|
316
|
+
"SELECT * FROM information_schema.tables",
|
|
317
|
+
{ contextMode: "operator-sql", profile: "balanced" }, "schema-recon");
|
|
318
|
+
refusesWith("timing: WAITFOR DELAY refused under strict",
|
|
319
|
+
"SELECT 1 WAITFOR DELAY '0:0:5'",
|
|
320
|
+
{ contextMode: "operator-sql", profile: "strict" }, "time-waitfor");
|
|
321
|
+
auditsWith("timing: WAITFOR DELAY audited under permissive",
|
|
322
|
+
"SELECT 1 WAITFOR DELAY '0:0:5'",
|
|
323
|
+
{ contextMode: "operator-sql", profile: "permissive" }, "time-waitfor");
|
|
324
|
+
refusesWith("timing: SLEEP() refused under strict",
|
|
325
|
+
"SELECT SLEEP(5)", { contextMode: "operator-sql", profile: "strict" }, "time-sleep");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ---- sanitize() — normalized output vs refuse-throw ----
|
|
329
|
+
|
|
330
|
+
function testSanitize() {
|
|
331
|
+
// permissive permits a comment; sanitize returns the comment-stripped
|
|
332
|
+
// normalized form (NOT a made-safe query).
|
|
333
|
+
var normalized = b.guardSql.sanitize("id = ? -- note" + LF + " AND active = ?",
|
|
334
|
+
{ profile: "permissive" });
|
|
335
|
+
check("sanitize: returns a string", typeof normalized === "string");
|
|
336
|
+
check("sanitize: comment stripped from normalized form",
|
|
337
|
+
normalized.indexOf("note") === -1 && normalized.indexOf("active") !== -1);
|
|
338
|
+
// An OS-reach floor construct has no safe transform — sanitize throws.
|
|
339
|
+
throwsCode("sanitize: throws on the OS-reach floor",
|
|
340
|
+
function () { return b.guardSql.sanitize("SELECT pg_read_file('/etc/passwd')"); },
|
|
341
|
+
"sql.file-access");
|
|
342
|
+
throwsCode("sanitize: throws on a stacked statement",
|
|
343
|
+
function () { return b.guardSql.sanitize("1; DROP TABLE users"); },
|
|
344
|
+
"sql.stacked");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ---- gate() decision chain: serve / audit-only / refuse / fail-closed ----
|
|
348
|
+
|
|
349
|
+
async function testGateDispositions() {
|
|
350
|
+
var strict = b.guardSql.gate({ profile: "strict" });
|
|
351
|
+
var serveEmpty = await strict.check({});
|
|
352
|
+
check("gate: no sql -> serve", serveEmpty.action === "serve" && serveEmpty.ok === true);
|
|
353
|
+
var serveNonStr = await strict.check({ sql: 123 });
|
|
354
|
+
check("gate: non-string sql -> serve", serveNonStr.action === "serve");
|
|
355
|
+
var serveClean = await strict.check({ sql: "id = ?", mode: "fragment" });
|
|
356
|
+
check("gate: clean fragment -> serve", serveClean.action === "serve");
|
|
357
|
+
var refuseStacked = await strict.check({ sql: "1; DROP TABLE users" });
|
|
358
|
+
check("gate: stacked -> refuse", refuseStacked.action === "refuse" && refuseStacked.ok === false);
|
|
359
|
+
|
|
360
|
+
// audit-only — a warn-level (recon) finding under balanced.
|
|
361
|
+
var balanced = b.guardSql.gate({ profile: "balanced" });
|
|
362
|
+
var auditOnly = await balanced.check({
|
|
363
|
+
sql: "SELECT a FROM information_schema.tables", mode: "operator-sql" });
|
|
364
|
+
check("gate: recon under balanced -> audit-only",
|
|
365
|
+
auditOnly.action === "audit-only" && auditOnly.ok === true);
|
|
366
|
+
|
|
367
|
+
// ctx.mode overrides the opts default per call.
|
|
368
|
+
var refuseVerbFrag = await strict.check({ sql: "SELECT 1", mode: "fragment" });
|
|
369
|
+
check("gate: ctx.mode=fragment refuses a statement verb",
|
|
370
|
+
refuseVerbFrag.action === "refuse");
|
|
371
|
+
var serveVerbOp = await strict.check({ sql: "SELECT 1", mode: "operator-sql" });
|
|
372
|
+
check("gate: ctx.mode=operator-sql serves the same verb",
|
|
373
|
+
serveVerbOp.action === "serve");
|
|
374
|
+
|
|
375
|
+
// A bad ctx.mode makes the inner check throw -> the gate FAILS CLOSED
|
|
376
|
+
// (refuse with a check-threw issue), never fails open.
|
|
377
|
+
var failClosed = await strict.check({ sql: "x = 1", mode: "bogus" });
|
|
378
|
+
check("gate: bad ctx.mode fails closed -> refuse",
|
|
379
|
+
failClosed.action === "refuse" && failClosed.ok === false);
|
|
380
|
+
check("gate: bad ctx.mode surfaces check-threw",
|
|
381
|
+
failClosed.issues.some(function (i) { return i.kind === "check-threw"; }));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ---- surface / postures / fixtures ----
|
|
385
|
+
|
|
386
|
+
function testSurfaceAndPostures() {
|
|
387
|
+
check("surface: CONTEXT_MODES lists the three modes",
|
|
388
|
+
b.guardSql.CONTEXT_MODES.length === 3 &&
|
|
389
|
+
b.guardSql.CONTEXT_MODES.indexOf("fragment") !== -1 &&
|
|
390
|
+
b.guardSql.CONTEXT_MODES.indexOf("operator-sql") !== -1 &&
|
|
391
|
+
b.guardSql.CONTEXT_MODES.indexOf("migration") !== -1);
|
|
392
|
+
check("surface: PROFILES has strict/balanced/permissive",
|
|
393
|
+
!!b.guardSql.PROFILES.strict && !!b.guardSql.PROFILES.balanced &&
|
|
394
|
+
!!b.guardSql.PROFILES.permissive);
|
|
395
|
+
check("surface: gdpr posture sets gdprRedact",
|
|
396
|
+
b.guardSql.compliancePosture("gdpr").gdprRedact === true);
|
|
397
|
+
check("surface: hipaa posture maps to the strict floor",
|
|
398
|
+
b.guardSql.compliancePosture("hipaa").floor === "refuse");
|
|
399
|
+
// The adaptive integration fixtures round-trip through validate.
|
|
400
|
+
check("fixtures: benignSql serves",
|
|
401
|
+
b.guardSql.validate(b.guardSql.INTEGRATION_FIXTURES.benignSql).ok === true);
|
|
402
|
+
check("fixtures: hostileSql refuses",
|
|
403
|
+
b.guardSql.validate(b.guardSql.INTEGRATION_FIXTURES.hostileSql,
|
|
404
|
+
{ profile: "strict" }).ok === false);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function run() {
|
|
408
|
+
testBadOptsThrow();
|
|
409
|
+
testBadInputResult();
|
|
410
|
+
testEncodingGate();
|
|
411
|
+
testOversizeCap();
|
|
412
|
+
testIdentifierHygiene();
|
|
413
|
+
testLeadingVerbFloor();
|
|
414
|
+
testSmugglingFloor();
|
|
415
|
+
testFragmentMode();
|
|
416
|
+
testOperatorSqlMode();
|
|
417
|
+
testMigrationMode();
|
|
418
|
+
testOsReachFloorEverywhere();
|
|
419
|
+
testReconTimingSoftenByProfile();
|
|
420
|
+
testSanitize();
|
|
421
|
+
await testGateDispositions();
|
|
422
|
+
testSurfaceAndPostures();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
module.exports = { run: run };
|
|
426
|
+
|
|
427
|
+
if (require.main === module) {
|
|
428
|
+
run().then(
|
|
429
|
+
function () { console.log("[guard-sql-coverage] OK — " + helpers.getChecks() + " checks passed"); process.exit(0); },
|
|
430
|
+
function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }
|
|
431
|
+
);
|
|
432
|
+
}
|