@blamejs/core 0.7.52 → 0.7.61
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 +18 -0
- package/index.js +14 -0
- package/lib/crypto.js +125 -0
- package/lib/framework-error.js +53 -0
- package/lib/guard-all.js +7 -0
- package/lib/guard-auth.js +278 -0
- package/lib/guard-image.js +371 -0
- package/lib/guard-jsonpath.js +300 -0
- package/lib/guard-pdf.js +345 -0
- package/lib/guard-regex.js +287 -0
- package/lib/guard-shell.js +319 -0
- package/lib/guard-template.js +279 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* guard-shell — Shell metacharacter identifier-safety primitive
|
|
4
|
+
* (b.guardShell).
|
|
5
|
+
*
|
|
6
|
+
* Validates user-input strings BEFORE they're handed to a
|
|
7
|
+
* child-process spawn (regardless of operator's `shell:` opt). The
|
|
8
|
+
* canonical defense is "use array args + shell:false", but operators
|
|
9
|
+
* still receive operator-untrusted strings that flow through path-
|
|
10
|
+
* arg or arg-list shapes — guardShell refuses obvious shell-injection
|
|
11
|
+
* shapes before the spawn call. KIND="identifier" — consumes
|
|
12
|
+
* ctx.identifier (or ctx.arg).
|
|
13
|
+
*
|
|
14
|
+
* Threat catalog:
|
|
15
|
+
* - POSIX shell metacharacters — `;`, `&`, `|`, `<`, `>`, `(`, `)`,
|
|
16
|
+
* `{`, `}`, `[`, `]`, `*`, `?`, `~`, `!`, `#`, `\`, single + double
|
|
17
|
+
* quotes.
|
|
18
|
+
* - Backtick command substitution.
|
|
19
|
+
* - `$(...)` command substitution and `${VAR}` parameter expansion.
|
|
20
|
+
* - Process substitution `<(...)` / `>(...)`.
|
|
21
|
+
* - cmd.exe metacharacters — `&`, `|`, `<`, `>`, `^`, `%`, `"`, `'`,
|
|
22
|
+
* `(`, `)`, `,`, `;`, `=`, ` `, tabs, newlines.
|
|
23
|
+
* - Newline / NUL injection (line splitting in scripts).
|
|
24
|
+
* - Variable expansion `$VAR`.
|
|
25
|
+
* - Operator may opt-in to `argHyphenPolicy` to refuse leading `-`
|
|
26
|
+
* arguments (defense against `-rf` / `--exec` / etc.).
|
|
27
|
+
* - BIDI / zero-width / control / null-byte universal refuse.
|
|
28
|
+
*
|
|
29
|
+
* var rv = b.guardShell.validate("file with spaces.txt",
|
|
30
|
+
* { profile: "strict" });
|
|
31
|
+
* var g = b.guardShell.gate({ profile: "strict" });
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
var codepointClass = require("./codepoint-class");
|
|
35
|
+
var lazyRequire = require("./lazy-require");
|
|
36
|
+
var gateContract = require("./gate-contract");
|
|
37
|
+
var C = require("./constants");
|
|
38
|
+
var numericBounds = require("./numeric-bounds");
|
|
39
|
+
var { GuardShellError } = require("./framework-error");
|
|
40
|
+
|
|
41
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
42
|
+
void observability;
|
|
43
|
+
|
|
44
|
+
var _err = GuardShellError.factory;
|
|
45
|
+
|
|
46
|
+
// POSIX shell metachars (excluding whitespace which is per-policy).
|
|
47
|
+
var POSIX_META_RE = /[;&|<>$`\\()[\]{}*?~!#'"]/;
|
|
48
|
+
|
|
49
|
+
// cmd.exe metachars.
|
|
50
|
+
var CMD_META_RE = /[&|<>^%"',;=]/;
|
|
51
|
+
|
|
52
|
+
// $(...) and ${...} command/param substitution.
|
|
53
|
+
var DOLLAR_PAREN_RE = /\$\(/;
|
|
54
|
+
var DOLLAR_BRACE_RE = /\$\{/;
|
|
55
|
+
|
|
56
|
+
// $VAR parameter expansion (bare).
|
|
57
|
+
var DOLLAR_VAR_RE = /\$[A-Za-z_][A-Za-z0-9_]*/;
|
|
58
|
+
|
|
59
|
+
// Process substitution.
|
|
60
|
+
var PROCESS_SUBST_RE = /[<>]\(/;
|
|
61
|
+
|
|
62
|
+
// Newline (any).
|
|
63
|
+
var NEWLINE_RE = /[\r\n]/;
|
|
64
|
+
|
|
65
|
+
// ---- Profile presets ----
|
|
66
|
+
|
|
67
|
+
var PROFILES = Object.freeze({
|
|
68
|
+
"strict": {
|
|
69
|
+
bidiPolicy: "reject",
|
|
70
|
+
controlPolicy: "reject",
|
|
71
|
+
nullBytePolicy: "reject",
|
|
72
|
+
zeroWidthPolicy: "reject",
|
|
73
|
+
posixMetaPolicy: "reject",
|
|
74
|
+
cmdMetaPolicy: "reject",
|
|
75
|
+
dollarSubstPolicy: "reject",
|
|
76
|
+
processSubstPolicy: "reject",
|
|
77
|
+
backtickPolicy: "reject",
|
|
78
|
+
newlinePolicy: "reject",
|
|
79
|
+
argHyphenPolicy: "reject",
|
|
80
|
+
maxBytes: C.BYTES.kib(2),
|
|
81
|
+
maxRuntimeMs: C.TIME.seconds(2),
|
|
82
|
+
},
|
|
83
|
+
"balanced": {
|
|
84
|
+
bidiPolicy: "reject",
|
|
85
|
+
controlPolicy: "reject",
|
|
86
|
+
nullBytePolicy: "reject",
|
|
87
|
+
zeroWidthPolicy: "reject",
|
|
88
|
+
posixMetaPolicy: "audit",
|
|
89
|
+
cmdMetaPolicy: "audit",
|
|
90
|
+
dollarSubstPolicy: "reject",
|
|
91
|
+
processSubstPolicy: "reject",
|
|
92
|
+
backtickPolicy: "reject",
|
|
93
|
+
newlinePolicy: "reject",
|
|
94
|
+
argHyphenPolicy: "audit",
|
|
95
|
+
maxBytes: C.BYTES.kib(2),
|
|
96
|
+
maxRuntimeMs: C.TIME.seconds(2),
|
|
97
|
+
},
|
|
98
|
+
"permissive": {
|
|
99
|
+
bidiPolicy: "reject", // BIDI refused at every profile
|
|
100
|
+
controlPolicy: "reject", // controls refused at every profile
|
|
101
|
+
nullBytePolicy: "reject", // null refused at every profile
|
|
102
|
+
zeroWidthPolicy: "reject", // zero-width refused at every profile
|
|
103
|
+
posixMetaPolicy: "audit",
|
|
104
|
+
cmdMetaPolicy: "audit",
|
|
105
|
+
dollarSubstPolicy: "reject", // command substitution refused at every profile
|
|
106
|
+
processSubstPolicy: "reject", // process substitution refused at every profile
|
|
107
|
+
backtickPolicy: "reject", // backtick substitution refused at every profile
|
|
108
|
+
newlinePolicy: "reject", // newline refused at every profile
|
|
109
|
+
argHyphenPolicy: "allow",
|
|
110
|
+
maxBytes: C.BYTES.kib(8),
|
|
111
|
+
maxRuntimeMs: C.TIME.seconds(2),
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
var DEFAULTS = Object.freeze(Object.assign({}, PROFILES["strict"], {
|
|
116
|
+
mode: "enforce",
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
var COMPLIANCE_POSTURES = Object.freeze({
|
|
120
|
+
"hipaa": Object.assign({}, PROFILES["strict"], {
|
|
121
|
+
forensicSnippetBytes: C.BYTES.bytes(256),
|
|
122
|
+
}),
|
|
123
|
+
"pci-dss": Object.assign({}, PROFILES["strict"], {
|
|
124
|
+
forensicSnippetBytes: C.BYTES.bytes(256),
|
|
125
|
+
}),
|
|
126
|
+
"gdpr": Object.assign({}, PROFILES["balanced"], {
|
|
127
|
+
forensicSnippetBytes: C.BYTES.bytes(128),
|
|
128
|
+
}),
|
|
129
|
+
"soc2": Object.assign({}, PROFILES["strict"], {
|
|
130
|
+
forensicSnippetBytes: C.BYTES.bytes(512),
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
function _resolveOpts(opts) {
|
|
135
|
+
return gateContract.resolveProfileAndPosture(opts, {
|
|
136
|
+
profiles: PROFILES,
|
|
137
|
+
compliancePostures: COMPLIANCE_POSTURES,
|
|
138
|
+
defaults: DEFAULTS,
|
|
139
|
+
errorClass: GuardShellError,
|
|
140
|
+
errCodePrefix: "shell",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _detectIssues(input, opts) {
|
|
145
|
+
var issues = [];
|
|
146
|
+
if (typeof input !== "string") {
|
|
147
|
+
return [{ kind: "bad-input", severity: "high",
|
|
148
|
+
ruleId: "shell.bad-input",
|
|
149
|
+
snippet: "shell arg is not a string" }];
|
|
150
|
+
}
|
|
151
|
+
if (input.length === 0) {
|
|
152
|
+
// Empty arg is not necessarily a threat (legit blank args exist).
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
if (Buffer.byteLength(input, "utf8") > opts.maxBytes) {
|
|
156
|
+
return [{ kind: "shell-cap", severity: "high",
|
|
157
|
+
ruleId: "shell.shell-cap",
|
|
158
|
+
snippet: "shell arg exceeds maxBytes " + opts.maxBytes }];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
var charThreats = codepointClass.detectCharThreats(input, opts, "shell");
|
|
162
|
+
for (var ci = 0; ci < charThreats.length; ci += 1) issues.push(charThreats[ci]);
|
|
163
|
+
|
|
164
|
+
// $(...) / ${...} / backtick — universal refuse.
|
|
165
|
+
if (opts.dollarSubstPolicy !== "allow" &&
|
|
166
|
+
(DOLLAR_PAREN_RE.test(input) || DOLLAR_BRACE_RE.test(input))) { // allow:regex-no-length-cap — input bounded by maxBytes
|
|
167
|
+
issues.push({
|
|
168
|
+
kind: "dollar-substitution", severity: "critical",
|
|
169
|
+
ruleId: "shell.dollar-substitution",
|
|
170
|
+
snippet: "argument contains `$(` or `${` — POSIX shell command / " +
|
|
171
|
+
"parameter substitution",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (opts.backtickPolicy !== "allow" && input.indexOf("`") !== -1) {
|
|
175
|
+
issues.push({
|
|
176
|
+
kind: "backtick", severity: "critical",
|
|
177
|
+
ruleId: "shell.backtick",
|
|
178
|
+
snippet: "argument contains backtick — POSIX shell command " +
|
|
179
|
+
"substitution",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (opts.processSubstPolicy !== "allow" && PROCESS_SUBST_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
|
|
183
|
+
issues.push({
|
|
184
|
+
kind: "process-substitution", severity: "critical",
|
|
185
|
+
ruleId: "shell.process-substitution",
|
|
186
|
+
snippet: "argument contains `<(` or `>(` — Bash process " +
|
|
187
|
+
"substitution",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (opts.dollarSubstPolicy !== "allow" && DOLLAR_VAR_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
|
|
191
|
+
issues.push({
|
|
192
|
+
kind: "dollar-var",
|
|
193
|
+
severity: opts.dollarSubstPolicy === "reject" ? "high" : "warn",
|
|
194
|
+
ruleId: "shell.dollar-var",
|
|
195
|
+
snippet: "argument contains `$VAR` parameter expansion",
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (opts.newlinePolicy !== "allow" && NEWLINE_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
|
|
199
|
+
issues.push({
|
|
200
|
+
kind: "newline", severity: "high",
|
|
201
|
+
ruleId: "shell.newline",
|
|
202
|
+
snippet: "argument contains CR / LF — line-splitting in shell " +
|
|
203
|
+
"scripts",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (opts.posixMetaPolicy !== "allow" && POSIX_META_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
|
|
207
|
+
issues.push({
|
|
208
|
+
kind: "posix-metachar",
|
|
209
|
+
severity: opts.posixMetaPolicy === "reject" ? "high" : "warn",
|
|
210
|
+
ruleId: "shell.posix-metachar",
|
|
211
|
+
snippet: "argument contains POSIX shell metacharacter " +
|
|
212
|
+
"(`;|&<>()[]{}*?~!#`'\"\\`)",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (opts.cmdMetaPolicy !== "allow" && CMD_META_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
|
|
216
|
+
issues.push({
|
|
217
|
+
kind: "cmd-metachar",
|
|
218
|
+
severity: opts.cmdMetaPolicy === "reject" ? "high" : "warn",
|
|
219
|
+
ruleId: "shell.cmd-metachar",
|
|
220
|
+
snippet: "argument contains cmd.exe metacharacter " +
|
|
221
|
+
"(`&|<>^%\"',;=`)",
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (opts.argHyphenPolicy !== "allow" && input.charAt(0) === "-") {
|
|
225
|
+
issues.push({
|
|
226
|
+
kind: "arg-hyphen-leading",
|
|
227
|
+
severity: opts.argHyphenPolicy === "reject" ? "high" : "warn",
|
|
228
|
+
ruleId: "shell.arg-hyphen-leading",
|
|
229
|
+
snippet: "argument begins with `-` — would be parsed as an " +
|
|
230
|
+
"option flag by the target binary (`-rf` / `--exec` " +
|
|
231
|
+
"class)",
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return issues;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function validate(input, opts) {
|
|
239
|
+
opts = _resolveOpts(opts);
|
|
240
|
+
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
241
|
+
["maxBytes"],
|
|
242
|
+
"guardShell.validate", GuardShellError, "shell.bad-opt");
|
|
243
|
+
return gateContract.aggregateIssues(_detectIssues(input, opts));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function sanitize(input, opts) {
|
|
247
|
+
opts = _resolveOpts(opts);
|
|
248
|
+
if (typeof input !== "string") {
|
|
249
|
+
throw _err("shell.bad-input", "sanitize requires string input");
|
|
250
|
+
}
|
|
251
|
+
// Shell args can't be repaired — sanitize either passes through
|
|
252
|
+
// valid input or throws.
|
|
253
|
+
var issues = _detectIssues(input, opts);
|
|
254
|
+
for (var i = 0; i < issues.length; i += 1) {
|
|
255
|
+
if (issues[i].severity === "critical" || issues[i].severity === "high") {
|
|
256
|
+
throw _err(issues[i].ruleId || "shell.refused",
|
|
257
|
+
"guardShell.sanitize: " + issues[i].snippet);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return input;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function gate(opts) {
|
|
264
|
+
opts = _resolveOpts(opts);
|
|
265
|
+
return gateContract.buildGuardGate(
|
|
266
|
+
opts.name || "guardShell:" + (opts.profile || "default"),
|
|
267
|
+
opts,
|
|
268
|
+
async function (ctx) {
|
|
269
|
+
var arg = ctx && (ctx.identifier || ctx.arg);
|
|
270
|
+
if (arg === undefined || arg === null) return { ok: true, action: "serve" };
|
|
271
|
+
var rv = validate(arg, opts);
|
|
272
|
+
if (rv.issues.length === 0) return { ok: true, action: "serve" };
|
|
273
|
+
var hasCritical = rv.issues.some(function (i) {
|
|
274
|
+
return i.severity === "critical";
|
|
275
|
+
});
|
|
276
|
+
var hasHigh = rv.issues.some(function (i) {
|
|
277
|
+
return i.severity === "high";
|
|
278
|
+
});
|
|
279
|
+
if (!hasCritical && !hasHigh) {
|
|
280
|
+
return { ok: true, action: "audit-only", issues: rv.issues };
|
|
281
|
+
}
|
|
282
|
+
return { ok: false, action: "refuse", issues: rv.issues };
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
var buildProfile = gateContract.makeProfileBuilder(PROFILES);
|
|
287
|
+
|
|
288
|
+
function compliancePosture(name) {
|
|
289
|
+
return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES,
|
|
290
|
+
_err, "shell");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
var _shellRulePacks = gateContract.makeRulePackLoader(GuardShellError, "shell");
|
|
294
|
+
var loadRulePack = _shellRulePacks.load;
|
|
295
|
+
|
|
296
|
+
module.exports = {
|
|
297
|
+
// ---- guard-* family registry exports ----
|
|
298
|
+
NAME: "shell",
|
|
299
|
+
KIND: "identifier",
|
|
300
|
+
INTEGRATION_FIXTURES: Object.freeze({
|
|
301
|
+
kind: "identifier",
|
|
302
|
+
benignBytes: Buffer.from("safe-arg-value", "utf8"),
|
|
303
|
+
hostileBytes: Buffer.from("safe; rm -rf /", "utf8"),
|
|
304
|
+
benignIdentifier: "safe-arg-value",
|
|
305
|
+
// Hostile: command-injection via metacharacter chain.
|
|
306
|
+
hostileIdentifier: "safe; rm -rf /",
|
|
307
|
+
}),
|
|
308
|
+
// ---- primitive surface ----
|
|
309
|
+
validate: validate,
|
|
310
|
+
sanitize: sanitize,
|
|
311
|
+
gate: gate,
|
|
312
|
+
buildProfile: buildProfile,
|
|
313
|
+
compliancePosture: compliancePosture,
|
|
314
|
+
loadRulePack: loadRulePack,
|
|
315
|
+
PROFILES: PROFILES,
|
|
316
|
+
DEFAULTS: DEFAULTS,
|
|
317
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
318
|
+
GuardShellError: GuardShellError,
|
|
319
|
+
};
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* guard-template — Template-injection identifier-safety primitive
|
|
4
|
+
* (b.guardTemplate).
|
|
5
|
+
*
|
|
6
|
+
* Detects Server-Side Template Injection (SSTI) shapes in user-input
|
|
7
|
+
* strings before they're rendered through any template engine. Refused
|
|
8
|
+
* by default at every profile — operator-untrusted input rarely
|
|
9
|
+
* legitimately contains template-engine syntax. KIND="identifier" —
|
|
10
|
+
* consumes ctx.identifier (or ctx.text).
|
|
11
|
+
*
|
|
12
|
+
* Threat catalog (engine-shape detection):
|
|
13
|
+
* - Jinja2 / Django / Twig / Liquid — `{{...}}` and `{%...%}`.
|
|
14
|
+
* Recent CVEs: CVE-2024-22195 (Jinja xml_attr filter),
|
|
15
|
+
* CVE-2024-26139 (Bottle), CVE-2024-23348 (Pyrogram).
|
|
16
|
+
* - Handlebars — `{{...}}` (same shape as Jinja; flagged together).
|
|
17
|
+
* - ERB / Tornado — `<%...%>` and `<%=...%>`.
|
|
18
|
+
* - Pug — `#{...}` interpolation, `!{...}` raw-HTML interpolation.
|
|
19
|
+
* - Mako / Velocity / Tornado — `${...}` interpolation.
|
|
20
|
+
* - Velocity directive — `#set(...)`, `#if(...)`, `#foreach(...)`.
|
|
21
|
+
* - AngularJS — `{{...}}` (covered by Jinja shape; legacy).
|
|
22
|
+
* - BIDI / null / control / zero-width universal refuse.
|
|
23
|
+
*
|
|
24
|
+
* var rv = b.guardTemplate.validate("Hello {{ name }}",
|
|
25
|
+
* { profile: "strict" });
|
|
26
|
+
* var g = b.guardTemplate.gate({ profile: "strict" });
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
var codepointClass = require("./codepoint-class");
|
|
30
|
+
var lazyRequire = require("./lazy-require");
|
|
31
|
+
var gateContract = require("./gate-contract");
|
|
32
|
+
var C = require("./constants");
|
|
33
|
+
var numericBounds = require("./numeric-bounds");
|
|
34
|
+
var { GuardTemplateError } = require("./framework-error");
|
|
35
|
+
|
|
36
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
37
|
+
void observability;
|
|
38
|
+
|
|
39
|
+
var _err = GuardTemplateError.factory;
|
|
40
|
+
|
|
41
|
+
// Engine-shape detectors.
|
|
42
|
+
var JINJA_EXPR_RE = /\{\{[\s\S]*?\}\}/;
|
|
43
|
+
var JINJA_STMT_RE = /\{%[\s\S]*?%\}/;
|
|
44
|
+
var ERB_EXPR_RE = /<%[\s\S]*?%>/;
|
|
45
|
+
var PUG_INTERP_RE = /[#!]\{[\s\S]*?\}/;
|
|
46
|
+
var DOLLAR_BRACE_RE = /\$\{[\s\S]*?\}/;
|
|
47
|
+
var VELOCITY_DIR_RE = /#(?:set|if|else|elseif|end|foreach|parse|include|stop)\b/i;
|
|
48
|
+
|
|
49
|
+
// ---- Profile presets ----
|
|
50
|
+
|
|
51
|
+
var PROFILES = Object.freeze({
|
|
52
|
+
"strict": {
|
|
53
|
+
bidiPolicy: "reject",
|
|
54
|
+
controlPolicy: "reject",
|
|
55
|
+
nullBytePolicy: "reject",
|
|
56
|
+
zeroWidthPolicy: "reject",
|
|
57
|
+
jinjaPolicy: "reject",
|
|
58
|
+
erbPolicy: "reject",
|
|
59
|
+
pugPolicy: "reject",
|
|
60
|
+
dollarBracePolicy: "reject",
|
|
61
|
+
velocityDirectivePolicy: "reject",
|
|
62
|
+
maxBytes: C.BYTES.kib(64),
|
|
63
|
+
maxRuntimeMs: C.TIME.seconds(2),
|
|
64
|
+
},
|
|
65
|
+
"balanced": {
|
|
66
|
+
bidiPolicy: "reject",
|
|
67
|
+
controlPolicy: "reject",
|
|
68
|
+
nullBytePolicy: "reject",
|
|
69
|
+
zeroWidthPolicy: "reject",
|
|
70
|
+
jinjaPolicy: "reject", // SSTI class — refused at every profile
|
|
71
|
+
erbPolicy: "reject",
|
|
72
|
+
pugPolicy: "reject",
|
|
73
|
+
dollarBracePolicy: "audit", // ${...} can also be JS template literal — audit
|
|
74
|
+
velocityDirectivePolicy: "reject",
|
|
75
|
+
maxBytes: C.BYTES.kib(128),
|
|
76
|
+
maxRuntimeMs: C.TIME.seconds(2),
|
|
77
|
+
},
|
|
78
|
+
"permissive": {
|
|
79
|
+
bidiPolicy: "reject", // BIDI refused at every profile
|
|
80
|
+
controlPolicy: "reject", // controls refused at every profile
|
|
81
|
+
nullBytePolicy: "reject", // null refused at every profile
|
|
82
|
+
zeroWidthPolicy: "reject", // zero-width refused at every profile
|
|
83
|
+
jinjaPolicy: "reject", // SSTI class refused at every profile
|
|
84
|
+
erbPolicy: "reject", // SSTI class refused at every profile
|
|
85
|
+
pugPolicy: "reject", // SSTI class refused at every profile
|
|
86
|
+
dollarBracePolicy: "audit",
|
|
87
|
+
velocityDirectivePolicy: "audit",
|
|
88
|
+
maxBytes: C.BYTES.kib(512),
|
|
89
|
+
maxRuntimeMs: C.TIME.seconds(2),
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
var DEFAULTS = Object.freeze(Object.assign({}, PROFILES["strict"], {
|
|
94
|
+
mode: "enforce",
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
var COMPLIANCE_POSTURES = Object.freeze({
|
|
98
|
+
"hipaa": Object.assign({}, PROFILES["strict"], {
|
|
99
|
+
forensicSnippetBytes: C.BYTES.bytes(512),
|
|
100
|
+
}),
|
|
101
|
+
"pci-dss": Object.assign({}, PROFILES["strict"], {
|
|
102
|
+
forensicSnippetBytes: C.BYTES.bytes(512),
|
|
103
|
+
}),
|
|
104
|
+
"gdpr": Object.assign({}, PROFILES["balanced"], {
|
|
105
|
+
forensicSnippetBytes: C.BYTES.bytes(256),
|
|
106
|
+
}),
|
|
107
|
+
"soc2": Object.assign({}, PROFILES["strict"], {
|
|
108
|
+
forensicSnippetBytes: C.BYTES.bytes(1024),
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
function _resolveOpts(opts) {
|
|
113
|
+
return gateContract.resolveProfileAndPosture(opts, {
|
|
114
|
+
profiles: PROFILES,
|
|
115
|
+
compliancePostures: COMPLIANCE_POSTURES,
|
|
116
|
+
defaults: DEFAULTS,
|
|
117
|
+
errorClass: GuardTemplateError,
|
|
118
|
+
errCodePrefix: "template",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _detectIssues(input, opts) {
|
|
123
|
+
var issues = [];
|
|
124
|
+
if (typeof input !== "string") {
|
|
125
|
+
return [{ kind: "bad-input", severity: "high",
|
|
126
|
+
ruleId: "template.bad-input",
|
|
127
|
+
snippet: "template input is not a string" }];
|
|
128
|
+
}
|
|
129
|
+
if (input.length === 0) return [];
|
|
130
|
+
if (Buffer.byteLength(input, "utf8") > opts.maxBytes) {
|
|
131
|
+
return [{ kind: "input-cap", severity: "high",
|
|
132
|
+
ruleId: "template.input-cap",
|
|
133
|
+
snippet: "template input exceeds maxBytes " + opts.maxBytes }];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
var charThreats = codepointClass.detectCharThreats(input, opts, "template");
|
|
137
|
+
for (var ci = 0; ci < charThreats.length; ci += 1) issues.push(charThreats[ci]);
|
|
138
|
+
|
|
139
|
+
if (opts.jinjaPolicy !== "allow") {
|
|
140
|
+
if (JINJA_EXPR_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
|
|
141
|
+
issues.push({
|
|
142
|
+
kind: "jinja-expression", severity: "high",
|
|
143
|
+
ruleId: "template.jinja-expression",
|
|
144
|
+
snippet: "input contains `{{...}}` template-engine expression " +
|
|
145
|
+
"syntax — Jinja / Django / Twig / Liquid / Handlebars / " +
|
|
146
|
+
"AngularJS SSTI shape (CVE-2024-22195 / 26139 / 23348 " +
|
|
147
|
+
"class)",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (JINJA_STMT_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
|
|
151
|
+
issues.push({
|
|
152
|
+
kind: "jinja-statement", severity: "high",
|
|
153
|
+
ruleId: "template.jinja-statement",
|
|
154
|
+
snippet: "input contains `{%...%}` template-engine statement " +
|
|
155
|
+
"syntax — SSTI shape",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (opts.erbPolicy !== "allow" && ERB_EXPR_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
|
|
160
|
+
issues.push({
|
|
161
|
+
kind: "erb-expression", severity: "high",
|
|
162
|
+
ruleId: "template.erb-expression",
|
|
163
|
+
snippet: "input contains `<%...%>` template-engine expression " +
|
|
164
|
+
"syntax — ERB / Tornado SSTI shape",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (opts.pugPolicy !== "allow" && PUG_INTERP_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
|
|
168
|
+
issues.push({
|
|
169
|
+
kind: "pug-interpolation", severity: "high",
|
|
170
|
+
ruleId: "template.pug-interpolation",
|
|
171
|
+
snippet: "input contains `#{...}` or `!{...}` template-engine " +
|
|
172
|
+
"interpolation — Pug SSTI shape",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (opts.dollarBracePolicy !== "allow" && DOLLAR_BRACE_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
|
|
176
|
+
issues.push({
|
|
177
|
+
kind: "dollar-brace",
|
|
178
|
+
severity: opts.dollarBracePolicy === "reject" ? "high" : "warn",
|
|
179
|
+
ruleId: "template.dollar-brace",
|
|
180
|
+
snippet: "input contains `${...}` interpolation — Mako / " +
|
|
181
|
+
"Velocity / Tornado / JS template-literal SSTI shape",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (opts.velocityDirectivePolicy !== "allow" &&
|
|
185
|
+
VELOCITY_DIR_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
|
|
186
|
+
issues.push({
|
|
187
|
+
kind: "velocity-directive",
|
|
188
|
+
severity: opts.velocityDirectivePolicy === "reject" ? "high" : "warn",
|
|
189
|
+
ruleId: "template.velocity-directive",
|
|
190
|
+
snippet: "input contains Velocity directive (`#set` / `#if` / " +
|
|
191
|
+
"`#foreach` / etc.) — SSTI shape",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return issues;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function validate(input, opts) {
|
|
199
|
+
opts = _resolveOpts(opts);
|
|
200
|
+
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
201
|
+
["maxBytes"],
|
|
202
|
+
"guardTemplate.validate", GuardTemplateError, "template.bad-opt");
|
|
203
|
+
return gateContract.aggregateIssues(_detectIssues(input, opts));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function sanitize(input, opts) {
|
|
207
|
+
opts = _resolveOpts(opts);
|
|
208
|
+
if (typeof input !== "string") {
|
|
209
|
+
throw _err("template.bad-input", "sanitize requires string input");
|
|
210
|
+
}
|
|
211
|
+
var issues = _detectIssues(input, opts);
|
|
212
|
+
for (var i = 0; i < issues.length; i += 1) {
|
|
213
|
+
if (issues[i].severity === "critical" || issues[i].severity === "high") {
|
|
214
|
+
throw _err(issues[i].ruleId || "template.refused",
|
|
215
|
+
"guardTemplate.sanitize: " + issues[i].snippet);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return input;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function gate(opts) {
|
|
222
|
+
opts = _resolveOpts(opts);
|
|
223
|
+
return gateContract.buildGuardGate(
|
|
224
|
+
opts.name || "guardTemplate:" + (opts.profile || "default"),
|
|
225
|
+
opts,
|
|
226
|
+
async function (ctx) {
|
|
227
|
+
var text = ctx && (ctx.identifier || ctx.text);
|
|
228
|
+
if (text === undefined || text === null) {
|
|
229
|
+
return { ok: true, action: "serve" };
|
|
230
|
+
}
|
|
231
|
+
var rv = validate(text, opts);
|
|
232
|
+
if (rv.issues.length === 0) return { ok: true, action: "serve" };
|
|
233
|
+
var hasCritical = rv.issues.some(function (i) {
|
|
234
|
+
return i.severity === "critical";
|
|
235
|
+
});
|
|
236
|
+
var hasHigh = rv.issues.some(function (i) {
|
|
237
|
+
return i.severity === "high";
|
|
238
|
+
});
|
|
239
|
+
if (!hasCritical && !hasHigh) {
|
|
240
|
+
return { ok: true, action: "audit-only", issues: rv.issues };
|
|
241
|
+
}
|
|
242
|
+
return { ok: false, action: "refuse", issues: rv.issues };
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
var buildProfile = gateContract.makeProfileBuilder(PROFILES);
|
|
247
|
+
|
|
248
|
+
function compliancePosture(name) {
|
|
249
|
+
return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES,
|
|
250
|
+
_err, "template");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
var _tplRulePacks = gateContract.makeRulePackLoader(GuardTemplateError, "template");
|
|
254
|
+
var loadRulePack = _tplRulePacks.load;
|
|
255
|
+
|
|
256
|
+
module.exports = {
|
|
257
|
+
// ---- guard-* family registry exports ----
|
|
258
|
+
NAME: "template",
|
|
259
|
+
KIND: "identifier",
|
|
260
|
+
INTEGRATION_FIXTURES: Object.freeze({
|
|
261
|
+
kind: "identifier",
|
|
262
|
+
benignBytes: Buffer.from("Hello world", "utf8"),
|
|
263
|
+
hostileBytes: Buffer.from("Hello {{7*7}}", "utf8"),
|
|
264
|
+
benignIdentifier: "Hello world",
|
|
265
|
+
// Hostile: Jinja-shape SSTI probe.
|
|
266
|
+
hostileIdentifier: "Hello {{7*7}}",
|
|
267
|
+
}),
|
|
268
|
+
// ---- primitive surface ----
|
|
269
|
+
validate: validate,
|
|
270
|
+
sanitize: sanitize,
|
|
271
|
+
gate: gate,
|
|
272
|
+
buildProfile: buildProfile,
|
|
273
|
+
compliancePosture: compliancePosture,
|
|
274
|
+
loadRulePack: loadRulePack,
|
|
275
|
+
PROFILES: PROFILES,
|
|
276
|
+
DEFAULTS: DEFAULTS,
|
|
277
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
278
|
+
GuardTemplateError: GuardTemplateError,
|
|
279
|
+
};
|
package/package.json
CHANGED
package/sbom.cyclonedx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:a193a2a6-fe98-4457-b5e9-e5ffa6a1d0ab",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-06T00:18:55.633Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.7.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.7.61",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.7.
|
|
25
|
+
"version": "0.7.61",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.7.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.7.61",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.7.
|
|
57
|
+
"ref": "@blamejs/core@0.7.61",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|