@blamejs/core 0.7.46 → 0.7.48

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 CHANGED
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.7.x
10
10
 
11
+ - **0.7.48** (2026-05-05) — `b.cookies.parseSafe(header, opts)` + `b.middleware.cookies(opts)` — inbound cookie-header threat detection. The existing `b.cookies.parse` is lenient (last-write-wins, silent skip on malformed pairs); `parseSafe` returns `{ jar, issues }` and surfaces every detected anomaly: header-cap (oversized Cookie header), header-control-byte (CR / LF / NUL injected through proxy — header-injection prelude class), pair-malformed (missing `=`), pair-empty-name, name-cap (oversized name), value-cap (oversized value), duplicate-name (cookie-tossing class — same name appearing more than once in one Cookie header indicates an attacker-set parent-domain cookie shadowing the legitimate one). The middleware shape (`b.middleware.cookies({ mode, audit, refuseOnHigh })`) wires `parseSafe` into the request lifecycle: populates `req.cookieJar`, emits one audit row per detected issue, and refuses with HTTP 400 on any high-severity issue when `mode: "enforce"` (default). Existing `b.cookies` invariants (RFC 6265bis token grammar enforcement, `__Host-` / `__Secure-` prefix invariants, SameSite=None requires Secure, `Partitioned` / CHIPS attribute support, length caps on serialize-side) remain unchanged — this slice closes the inbound-detection gap.
12
+
13
+ - **0.7.47** (2026-05-05) — `b.guardMime` — RFC 6838 media-type identifier-safety primitive (KIND="identifier"). Validates user-supplied media type strings destined for Accept-shape comparison, content-type allowlists, and dispatch routing. Threat catalog: shape malformation (missing `/`, bad type/subtype tokens against RFC 6838 §4.2 restricted-name grammar); parameter validation against the RFC 7231 §3.1.1.1 tchar token grammar (token-only or quoted-string per RFC 7230 §3.2.6); wildcard (`type/subtype` with `*`) outside Accept context refuse; vendor tree (`vnd.*`), personal tree (`prs.*`), and unregistered (`x.*` / `x-*`) namespace audit so operators audit those slots; risky-type refuse list covering executable + script-host content types (`application/x-msdownload`, `application/x-bat`, `application/x-msdos-program`, `application/x-sh`, `application/x-csh`, `application/x-perl`, `application/x-python`, `application/javascript`, `application/x-javascript`, `text/javascript`, `text/x-javascript`, `application/x-shockwave-flash`, `application/x-msi`); BIDI / zero-width / control / null-byte universal refuse. `sanitize` lowercases type/subtype while preserving parameter case (multipart boundary tokens etc. are case-significant). Profiles: `strict` (refuse wildcard + risky-type, audit trees + parameters), `balanced` (audit most things, allow vendor tree), `permissive` (universal-refuse class still refused). Postures: `hipaa` / `pci-dss` / `soc2` strict overlay; `gdpr` balanced overlay. Auto-registers into `b.guardAll` as a STANDALONE_GUARD.
14
+
11
15
  - **0.7.46** (2026-05-05) — `b.guardTime` — RFC 3339 / ISO 8601 datetime identifier-safety primitive (KIND="identifier"). Validates user-supplied datetime strings destined for audit timestamps, scheduling, retention windows, query ranges, and cross-system event correlation. Threat catalog: shape malformation against the RFC 3339 §5.6 grammar; year-window overflow (default `[1970, 9999]`); naive datetime (no offset) refuse; non-UTC offset policy (strict requires `Z` / `+00:00`); leap-second `60` field policy (RFC 3339 §5.6 valid but parser-panic prone); excessive fractional precision cap (default 9 digits / nanoseconds); date-only and time-only refuse for full-datetime contexts; structural range violations (month / day-in-month / hour / minute / second); BIDI / zero-width / control / null-byte universal refuse. `sanitize` normalizes a space date/time separator to `T` and uppercases the trailing `z` UTC marker. Profiles: `strict` (refuse all of the above), `balanced` (refuse naive; audit non-UTC + leap + fractional + date/time-only), `permissive` (universal-refuse class still refused). Postures: `hipaa` / `pci-dss` / `soc2` strict overlay; `gdpr` balanced overlay. Auto-registers into `b.guardAll` as a STANDALONE_GUARD.
12
16
 
13
17
  - **0.7.45** (2026-05-05) — `b.guardCidr` — CIDR identifier-safety primitive (KIND="identifier"). Validates user-supplied CIDR notation strings (IPv4 + IPv6) destined for network allowlists, ACLs, security-group rules, and tenant-boundary configuration. Threat catalog: shape malformation, IPv4 octet overflow + leading-zero refuse, IPv6 zero-group ambiguity, mask out-of-range (IPv4 32 / IPv6 128 ceiling), network-address misalignment (host bits set on a non-/32 / non-/128 prefix — common typo class), reserved-range membership covering all the IPv4 RFC 1918 private blocks (`10/8`, `172.16/12`, `192.168/16`) plus loopback `127/8`, link-local `169.254/16`, multicast `224/4`, class-E `240/4`, RFC 5737 documentation `192.0.2/24` + `198.51.100/24` + `203.0.113/24`, benchmarking `198.18/15`, CGNAT `100.64/10`, this-network `0/8`, plus IPv6 loopback `::1`, unspecified `::/128`, ULA `fc00::/7`, link-local `fe80::/10`, multicast `ff00::/8`, documentation `2001:db8::/32`, teredo, deprecated 6to4 `2002::/16`. Includes IPv4-mapped IPv6 dual-stack confusion detection (`::ffff:0:0/96` — CVE-2021-22931 IPv6 variant). Bare-IP-without-mask policy (strict refuses; balanced audits; permissive allows). BIDI / zero-width / control / null-byte universal refuse. Profiles: `strict` / `balanced` / `permissive`. Postures: `hipaa` / `pci-dss` / `soc2` strict overlay; `gdpr` balanced overlay. Auto-registers into `b.guardAll` as a STANDALONE_GUARD.
package/index.js CHANGED
@@ -114,6 +114,7 @@ var guardDomain = require("./lib/guard-domain");
114
114
  var guardUuid = require("./lib/guard-uuid");
115
115
  var guardCidr = require("./lib/guard-cidr");
116
116
  var guardTime = require("./lib/guard-time");
117
+ var guardMime = require("./lib/guard-mime");
117
118
  var guardAll = require("./lib/guard-all");
118
119
  var ssrfGuard = require("./lib/ssrf-guard");
119
120
  var authHeader = require("./lib/auth-header");
@@ -259,6 +260,7 @@ module.exports = {
259
260
  guardUuid: guardUuid,
260
261
  guardCidr: guardCidr,
261
262
  guardTime: guardTime,
263
+ guardMime: guardMime,
262
264
  guardAll: guardAll,
263
265
  ssrfGuard: ssrfGuard,
264
266
  authHeader: authHeader,
package/lib/cookies.js CHANGED
@@ -342,9 +342,126 @@ function create(opts) {
342
342
  };
343
343
  }
344
344
 
345
+ // parseSafe — threat-detecting inbound-cookie parser. Returns
346
+ // { jar, issues } where every detected anomaly surfaces as an issue
347
+ // instead of being silently dropped (as the lenient parse() does).
348
+ //
349
+ // Threat catalog applied to the inbound Cookie header:
350
+ // - Oversized header — total bytes exceed maxHeaderBytes (default 8 KiB).
351
+ // - Oversized pair — name + value exceeds NAME_LENGTH + VALUE_LENGTH cap.
352
+ // - Duplicate cookie name — RFC 6265 last-write-wins is the browser
353
+ // behavior, but two pairs with the same name in one Cookie header
354
+ // usually indicates cookie-tossing (attacker-set parent-domain
355
+ // cookie shadowing the legitimate one).
356
+ // - Malformed pair — missing `=` or empty name.
357
+ // - Forbidden chars in raw header — CR / LF / NUL injected through
358
+ // a downstream proxy.
359
+ // - Empty / non-string input — operator-misuse signal.
360
+ //
361
+ // Issue shape: { kind, severity: "high"|"warn", snippet, name? }.
362
+ //
363
+ // Operators wire it through `b.middleware.cookies` (the convenience
364
+ // middleware below) or call directly when they want the issues list
365
+ // without imposing a request lifecycle.
366
+ function parseSafe(cookieHeader, opts) {
367
+ opts = opts || {};
368
+ var maxHeaderBytes = opts.maxHeaderBytes || C.BYTES.kib(8);
369
+ var maxNameBytes = opts.maxNameBytes || MAX_NAME_LENGTH;
370
+ var maxValueBytes = opts.maxValueBytes || MAX_VALUE_LENGTH;
371
+
372
+ if (typeof cookieHeader !== "string") {
373
+ return {
374
+ jar: {},
375
+ issues: [{ kind: "bad-input", severity: "high",
376
+ snippet: "cookie header is not a string" }],
377
+ };
378
+ }
379
+ if (cookieHeader.length === 0) return { jar: {}, issues: [] };
380
+
381
+ var issues = [];
382
+ var jar = Object.create(null);
383
+ var seen = Object.create(null);
384
+
385
+ if (Buffer.byteLength(cookieHeader, "utf8") > maxHeaderBytes) {
386
+ issues.push({
387
+ kind: "header-cap", severity: "high",
388
+ snippet: "Cookie header " + cookieHeader.length + " bytes exceeds " +
389
+ "maxHeaderBytes " + maxHeaderBytes,
390
+ });
391
+ return { jar: jar, issues: issues };
392
+ }
393
+ for (var hi = 0; hi < cookieHeader.length; hi += 1) {
394
+ var ch = cookieHeader.charCodeAt(hi);
395
+ if (ch === 0x0D || ch === 0x0A || ch === 0x00) { // allow:raw-byte-literal — CR / LF / NUL forbidden in cookie header
396
+ issues.push({
397
+ kind: "header-control-byte", severity: "high",
398
+ snippet: "Cookie header contains CR / LF / NUL — proxy-side " +
399
+ "header injection vector",
400
+ });
401
+ return { jar: jar, issues: issues };
402
+ }
403
+ }
404
+
405
+ var pairs = cookieHeader.split(/;\s*/);
406
+ for (var i = 0; i < pairs.length; i += 1) {
407
+ var pair = pairs[i];
408
+ if (!pair) continue;
409
+ var eq = pair.indexOf("=");
410
+ if (eq < 0) {
411
+ issues.push({
412
+ kind: "pair-malformed", severity: "warn",
413
+ snippet: "cookie pair " + JSON.stringify(pair) + " missing `=`",
414
+ });
415
+ continue;
416
+ }
417
+ var k = pair.slice(0, eq).trim();
418
+ if (!k) {
419
+ issues.push({
420
+ kind: "pair-empty-name", severity: "warn",
421
+ snippet: "cookie pair has empty name",
422
+ });
423
+ continue;
424
+ }
425
+ var v = pair.slice(eq + 1).trim();
426
+ if (v.length >= 2 && v.charAt(0) === '"' && v.charAt(v.length - 1) === '"') {
427
+ v = v.slice(1, -1);
428
+ }
429
+ try { v = decodeURIComponent(v); }
430
+ catch (_e) { /* malformed encoding — keep raw */ }
431
+
432
+ if (Buffer.byteLength(k, "utf8") > maxNameBytes) {
433
+ issues.push({
434
+ kind: "name-cap", severity: "high", name: k,
435
+ snippet: "cookie name exceeds maxNameBytes " + maxNameBytes,
436
+ });
437
+ continue;
438
+ }
439
+ if (Buffer.byteLength(v, "utf8") > maxValueBytes) {
440
+ issues.push({
441
+ kind: "value-cap", severity: "high", name: k,
442
+ snippet: "cookie `" + k + "` value exceeds maxValueBytes " +
443
+ maxValueBytes,
444
+ });
445
+ continue;
446
+ }
447
+ if (seen[k]) {
448
+ issues.push({
449
+ kind: "duplicate-name", severity: "high", name: k,
450
+ snippet: "cookie name `" + k + "` appears more than once — " +
451
+ "browser last-write-wins; cookie-tossing class " +
452
+ "(parent-domain cookie shadowing the legitimate one)",
453
+ });
454
+ }
455
+ seen[k] = true;
456
+ jar[k] = v;
457
+ }
458
+ return { jar: jar, issues: issues };
459
+ }
460
+
345
461
  module.exports = {
346
462
  create: create,
347
463
  parse: parse,
464
+ parseSafe: parseSafe,
348
465
  serialize: serialize,
349
466
  CookieError: CookieError,
350
467
  };
@@ -280,6 +280,14 @@ var GuardCidrError = defineClass("GuardCidrError", { alwaysPermane
280
280
  // refuse, structural range violations (month / day-in-month / hour /
281
281
  // minute / second). alwaysPermanent.
282
282
  var GuardTimeError = defineClass("GuardTimeError", { alwaysPermanent: true });
283
+ // GuardMimeError covers RFC 6838 media-type identifier violations:
284
+ // shape malformation (missing `/`, bad type/subtype tokens), parameter
285
+ // injection (multiple params, bad name/value tokens, malformed quoted-
286
+ // string), wildcard `*/*` outside Accept context, vendor / personal /
287
+ // unregistered tree namespaces, risky-type refuse list (executable +
288
+ // script-host content types), BIDI / zero-width / control / null-byte
289
+ // universal refuse. alwaysPermanent.
290
+ var GuardMimeError = defineClass("GuardMimeError", { alwaysPermanent: true });
283
291
  // DoraError covers DORA Article 17 incident-reporting workflow errors
284
292
  // (classification refusal, report-shape validation, ESA-template
285
293
  // generation, audit-chain integration). Permanent — these are
@@ -343,6 +351,7 @@ module.exports = {
343
351
  GuardUuidError: GuardUuidError,
344
352
  GuardCidrError: GuardCidrError,
345
353
  GuardTimeError: GuardTimeError,
354
+ GuardMimeError: GuardMimeError,
346
355
  DoraError: DoraError,
347
356
  ComplianceError: ComplianceError,
348
357
  SmtpPolicyError: SmtpPolicyError,
package/lib/guard-all.js CHANGED
@@ -93,6 +93,7 @@ var STANDALONE_GUARDS = [
93
93
  require("./guard-uuid"),
94
94
  require("./guard-cidr"),
95
95
  require("./guard-time"),
96
+ require("./guard-mime"),
96
97
  ];
97
98
 
98
99
  // Framework-wide profile + posture vocabulary that every guard MUST
@@ -0,0 +1,444 @@
1
+ "use strict";
2
+ /**
3
+ * guard-mime — Media-type identifier-safety primitive (b.guardMime).
4
+ *
5
+ * Validates user-supplied RFC 6838 media type strings destined for
6
+ * Accept-shape comparison, content-type allowlists, and dispatch
7
+ * routing. KIND="identifier" — consumes ctx.identifier (or ctx.mime).
8
+ *
9
+ * Threat catalog:
10
+ * - Shape malformation — not RFC 6838 type/subtype grammar.
11
+ * - Bad token characters — RFC 6838 §4.2 restricts type and subtype
12
+ * to ALPHA / DIGIT / `!#$&-^_.+`. Spaces / quotes / Unicode reject.
13
+ * - Parameter injection — operators sometimes pass through user-
14
+ * supplied parameters (`text/plain; charset=...`); the grammar is
15
+ * permissive enough to smuggle multiple parameters or bare values.
16
+ * - Wildcard `* / *` and `type / *` — only valid in Accept-header
17
+ * context; refused as a content-type at strict.
18
+ * - Vendor tree without operator opt-in (`application/vnd.<vendor>`)
19
+ * — flag at strict so operators audit the vendor namespace.
20
+ * - Personal tree (`application/prs.*`) and unregistered (`x.*`) —
21
+ * same flag class.
22
+ * - Risky types refuse list — `application/x-msdownload`, `.x-bat`,
23
+ * `.x-msdos-program`, `.x-sh`, `.x-csh`, `.javascript`,
24
+ * `.x-javascript` (when handed off to a script-host).
25
+ * - BIDI / zero-width / control / null-byte universal refuse.
26
+ *
27
+ * var rv = b.guardMime.validate("application/json",
28
+ * { profile: "strict" });
29
+ * var safe = b.guardMime.sanitize("Application/JSON; charset=UTF-8",
30
+ * { profile: "balanced" });
31
+ * var g = b.guardMime.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 { GuardMimeError } = require("./framework-error");
40
+
41
+ var observability = lazyRequire(function () { return require("./observability"); });
42
+ void observability;
43
+
44
+ var _err = GuardMimeError.factory;
45
+
46
+ // RFC 6838 type / subtype grammar. The `restricted-name` allows
47
+ // ALPHA / DIGIT first, then ALPHA / DIGIT / `!#$&-^_.+`. Length cap
48
+ // 127 octets per token.
49
+ var TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9!#$&\-^_.+]{0,126}$/;
50
+
51
+ // Parameter token (RFC 7231 §3.1.1.1): tchar set.
52
+ var PARAM_TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
53
+
54
+ // Quoted-string body (between double quotes) per RFC 7230 §3.2.6.
55
+ var QUOTED_STRING_BODY_RE = /^[\t\x20-\x7e]*$/; // allow:raw-byte-literal — printable ASCII range
56
+
57
+ // Risky-type refuse list (operator-supplied scripts handed to a host).
58
+ var RISKY_TYPES = Object.freeze([
59
+ "application/x-msdownload",
60
+ "application/x-bat",
61
+ "application/x-msdos-program",
62
+ "application/x-sh",
63
+ "application/x-csh",
64
+ "application/x-perl",
65
+ "application/x-python",
66
+ "application/javascript",
67
+ "application/x-javascript",
68
+ "text/javascript",
69
+ "text/x-javascript",
70
+ "application/x-shockwave-flash",
71
+ "application/x-msi",
72
+ ]);
73
+
74
+ // ---- Profile presets ----
75
+
76
+ var PROFILES = Object.freeze({
77
+ "strict": {
78
+ bidiPolicy: "reject",
79
+ controlPolicy: "reject",
80
+ nullBytePolicy: "reject",
81
+ zeroWidthPolicy: "reject",
82
+ wildcardPolicy: "reject",
83
+ vendorTreePolicy: "audit",
84
+ personalTreePolicy: "audit",
85
+ unregisteredTreePolicy: "audit",
86
+ riskyTypePolicy: "reject",
87
+ parameterPolicy: "audit",
88
+ maxBytes: C.BYTES.bytes(255),
89
+ maxRuntimeMs: C.TIME.seconds(2),
90
+ },
91
+ "balanced": {
92
+ bidiPolicy: "reject",
93
+ controlPolicy: "reject",
94
+ nullBytePolicy: "reject",
95
+ zeroWidthPolicy: "reject",
96
+ wildcardPolicy: "audit",
97
+ vendorTreePolicy: "allow",
98
+ personalTreePolicy: "audit",
99
+ unregisteredTreePolicy: "audit",
100
+ riskyTypePolicy: "audit",
101
+ parameterPolicy: "audit",
102
+ maxBytes: C.BYTES.bytes(255),
103
+ maxRuntimeMs: C.TIME.seconds(2),
104
+ },
105
+ "permissive": {
106
+ bidiPolicy: "reject", // BIDI refused at every profile
107
+ controlPolicy: "reject", // controls refused at every profile
108
+ nullBytePolicy: "reject", // null refused at every profile
109
+ zeroWidthPolicy: "reject", // zero-width refused at every profile
110
+ wildcardPolicy: "allow",
111
+ vendorTreePolicy: "allow",
112
+ personalTreePolicy: "allow",
113
+ unregisteredTreePolicy: "allow",
114
+ riskyTypePolicy: "audit",
115
+ parameterPolicy: "allow",
116
+ maxBytes: C.BYTES.bytes(255),
117
+ maxRuntimeMs: C.TIME.seconds(2),
118
+ },
119
+ });
120
+
121
+ var DEFAULTS = Object.freeze(Object.assign({}, PROFILES["strict"], {
122
+ mode: "enforce",
123
+ }));
124
+
125
+ var COMPLIANCE_POSTURES = Object.freeze({
126
+ "hipaa": Object.assign({}, PROFILES["strict"], {
127
+ forensicSnippetBytes: C.BYTES.bytes(128),
128
+ }),
129
+ "pci-dss": Object.assign({}, PROFILES["strict"], {
130
+ forensicSnippetBytes: C.BYTES.bytes(128),
131
+ }),
132
+ "gdpr": Object.assign({}, PROFILES["balanced"], {
133
+ forensicSnippetBytes: C.BYTES.bytes(64),
134
+ }),
135
+ "soc2": Object.assign({}, PROFILES["strict"], {
136
+ forensicSnippetBytes: C.BYTES.bytes(256),
137
+ }),
138
+ });
139
+
140
+ function _resolveOpts(opts) {
141
+ return gateContract.resolveProfileAndPosture(opts, {
142
+ profiles: PROFILES,
143
+ compliancePostures: COMPLIANCE_POSTURES,
144
+ defaults: DEFAULTS,
145
+ errorClass: GuardMimeError,
146
+ errCodePrefix: "mime",
147
+ });
148
+ }
149
+
150
+ // ---- Parser ----
151
+
152
+ function _splitTopLevel(input) {
153
+ // Returns { typeSubtype, params: [{name, value}], errors: [string] }.
154
+ // Splits on `;` outside quoted-strings.
155
+ var parts = [];
156
+ var inQuote = false;
157
+ var start = 0;
158
+ for (var i = 0; i < input.length; i += 1) {
159
+ var c = input.charAt(i);
160
+ if (c === '"' && (i === 0 || input.charAt(i - 1) !== "\\")) inQuote = !inQuote;
161
+ else if (!inQuote && c === ";") {
162
+ parts.push(input.slice(start, i));
163
+ start = i + 1;
164
+ }
165
+ }
166
+ parts.push(input.slice(start));
167
+ parts = parts.map(function (p) { return p.trim(); });
168
+ return parts;
169
+ }
170
+
171
+ function _detectIssues(input, opts) {
172
+ var issues = [];
173
+ if (typeof input !== "string") {
174
+ return [{ kind: "bad-input", severity: "high",
175
+ ruleId: "mime.bad-input",
176
+ snippet: "mime is not a string" }];
177
+ }
178
+ if (input.length === 0) {
179
+ return [{ kind: "empty", severity: "high",
180
+ ruleId: "mime.empty",
181
+ snippet: "mime is empty" }];
182
+ }
183
+ if (Buffer.byteLength(input, "utf8") > opts.maxBytes) {
184
+ return [{ kind: "mime-cap", severity: "high",
185
+ ruleId: "mime.mime-cap",
186
+ snippet: "mime input exceeds maxBytes " + opts.maxBytes }];
187
+ }
188
+
189
+ var charThreats = codepointClass.detectCharThreats(input, opts, "mime");
190
+ for (var ci = 0; ci < charThreats.length; ci += 1) issues.push(charThreats[ci]);
191
+
192
+ var parts = _splitTopLevel(input);
193
+ var typeSubtype = parts[0];
194
+ var paramParts = parts.slice(1).filter(function (p) { return p.length > 0; });
195
+
196
+ // Type/subtype shape.
197
+ var slashAt = typeSubtype.indexOf("/");
198
+ if (slashAt === -1) {
199
+ issues.push({
200
+ kind: "mime-shape", severity: "high",
201
+ ruleId: "mime.mime-shape",
202
+ snippet: "missing `/` between type and subtype",
203
+ });
204
+ return issues;
205
+ }
206
+ var type = typeSubtype.slice(0, slashAt);
207
+ var subtype = typeSubtype.slice(slashAt + 1);
208
+
209
+ // Wildcard policy.
210
+ if ((type === "*" || subtype === "*") &&
211
+ opts.wildcardPolicy !== "allow") {
212
+ issues.push({
213
+ kind: "wildcard",
214
+ severity: opts.wildcardPolicy === "reject" ? "high" : "warn",
215
+ ruleId: "mime.wildcard",
216
+ snippet: "wildcard `" + typeSubtype + "` only valid in Accept " +
217
+ "headers; refused as content-type at strict",
218
+ });
219
+ }
220
+
221
+ // Token validation per type / subtype.
222
+ if (type !== "*") {
223
+ if (!TOKEN_RE.test(type)) { // allow:regex-no-length-cap — input bounded by maxBytes
224
+ issues.push({
225
+ kind: "type-shape", severity: "high",
226
+ ruleId: "mime.type-shape",
227
+ snippet: "type `" + type + "` is not a valid RFC 6838 " +
228
+ "restricted-name token",
229
+ });
230
+ }
231
+ }
232
+ if (subtype !== "*") {
233
+ if (!TOKEN_RE.test(subtype)) { // allow:regex-no-length-cap — input bounded by maxBytes
234
+ issues.push({
235
+ kind: "subtype-shape", severity: "high",
236
+ ruleId: "mime.subtype-shape",
237
+ snippet: "subtype `" + subtype + "` is not a valid RFC 6838 " +
238
+ "restricted-name token",
239
+ });
240
+ }
241
+ }
242
+
243
+ // Tree-prefix detection.
244
+ var subtypeLower = subtype.toLowerCase();
245
+ if (subtypeLower.indexOf("vnd.") === 0 &&
246
+ opts.vendorTreePolicy !== "allow") {
247
+ issues.push({
248
+ kind: "vendor-tree",
249
+ severity: opts.vendorTreePolicy === "reject" ? "high" : "warn",
250
+ ruleId: "mime.vendor-tree",
251
+ snippet: "subtype `" + subtype + "` is in the vendor tree " +
252
+ "(`vnd.*`); audit the vendor namespace",
253
+ });
254
+ }
255
+ if (subtypeLower.indexOf("prs.") === 0 &&
256
+ opts.personalTreePolicy !== "allow") {
257
+ issues.push({
258
+ kind: "personal-tree",
259
+ severity: opts.personalTreePolicy === "reject" ? "high" : "warn",
260
+ ruleId: "mime.personal-tree",
261
+ snippet: "subtype `" + subtype + "` is in the personal tree " +
262
+ "(`prs.*`)",
263
+ });
264
+ }
265
+ if ((subtypeLower.indexOf("x.") === 0 || subtypeLower.indexOf("x-") === 0) &&
266
+ opts.unregisteredTreePolicy !== "allow") {
267
+ issues.push({
268
+ kind: "unregistered-tree",
269
+ severity: opts.unregisteredTreePolicy === "reject" ? "high" : "warn",
270
+ ruleId: "mime.unregistered-tree",
271
+ snippet: "subtype `" + subtype + "` is in the unregistered tree " +
272
+ "(`x.*` / `x-*`)",
273
+ });
274
+ }
275
+
276
+ // Risky-type refuse list (use lowercased canonical compare).
277
+ var canonical = (type + "/" + subtype).toLowerCase();
278
+ if (RISKY_TYPES.indexOf(canonical) !== -1 &&
279
+ opts.riskyTypePolicy !== "allow") {
280
+ issues.push({
281
+ kind: "risky-type",
282
+ severity: opts.riskyTypePolicy === "reject" ? "high" : "warn",
283
+ ruleId: "mime.risky-type",
284
+ snippet: "media type `" + canonical + "` is on the risky-type " +
285
+ "refuse list (executable / script-host class)",
286
+ });
287
+ }
288
+
289
+ // Parameter validation (all params at once — covers injection class).
290
+ if (paramParts.length > 0 && opts.parameterPolicy !== "allow") {
291
+ for (var pi = 0; pi < paramParts.length; pi += 1) {
292
+ var pp = paramParts[pi];
293
+ var eqAt = pp.indexOf("=");
294
+ if (eqAt === -1) {
295
+ issues.push({
296
+ kind: "param-shape",
297
+ severity: opts.parameterPolicy === "reject" ? "high" : "warn",
298
+ ruleId: "mime.param-shape",
299
+ snippet: "parameter `" + pp + "` missing `=` value separator",
300
+ });
301
+ continue;
302
+ }
303
+ var pname = pp.slice(0, eqAt).trim();
304
+ var pvalue = pp.slice(eqAt + 1).trim();
305
+ if (!PARAM_TOKEN_RE.test(pname)) { // allow:regex-no-length-cap — name bounded by parameter length within maxBytes
306
+ issues.push({
307
+ kind: "param-name",
308
+ severity: opts.parameterPolicy === "reject" ? "high" : "warn",
309
+ ruleId: "mime.param-name",
310
+ snippet: "parameter name `" + pname + "` is not a valid " +
311
+ "RFC 7231 §3.1.1.1 tchar token",
312
+ });
313
+ }
314
+ // Value: either a token or a quoted-string.
315
+ if (pvalue.length === 0) {
316
+ issues.push({
317
+ kind: "param-value-empty",
318
+ severity: opts.parameterPolicy === "reject" ? "high" : "warn",
319
+ ruleId: "mime.param-value-empty",
320
+ snippet: "parameter `" + pname + "` has empty value",
321
+ });
322
+ } else if (pvalue.charAt(0) === '"' &&
323
+ pvalue.charAt(pvalue.length - 1) === '"') {
324
+ var inner = pvalue.slice(1, -1);
325
+ if (!QUOTED_STRING_BODY_RE.test(inner)) { // allow:regex-no-length-cap — value bounded within maxBytes
326
+ issues.push({
327
+ kind: "param-value-shape",
328
+ severity: opts.parameterPolicy === "reject" ? "high" : "warn",
329
+ ruleId: "mime.param-value-shape",
330
+ snippet: "parameter `" + pname + "` quoted-string contains " +
331
+ "non-printable bytes (RFC 7230 §3.2.6)",
332
+ });
333
+ }
334
+ } else if (!PARAM_TOKEN_RE.test(pvalue)) { // allow:regex-no-length-cap — value bounded within maxBytes
335
+ issues.push({
336
+ kind: "param-value-shape",
337
+ severity: opts.parameterPolicy === "reject" ? "high" : "warn",
338
+ ruleId: "mime.param-value-shape",
339
+ snippet: "parameter `" + pname + "` value `" + pvalue + "` " +
340
+ "is not a valid token or quoted-string",
341
+ });
342
+ }
343
+ }
344
+ }
345
+
346
+ return issues;
347
+ }
348
+
349
+ function validate(input, opts) {
350
+ opts = _resolveOpts(opts);
351
+ numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
352
+ ["maxBytes"],
353
+ "guardMime.validate", GuardMimeError, "mime.bad-opt");
354
+ if (typeof input !== "string") {
355
+ return {
356
+ ok: false,
357
+ issues: [{ kind: "bad-input", severity: "high",
358
+ ruleId: "mime.bad-input",
359
+ snippet: "mime is not a string" }],
360
+ };
361
+ }
362
+ return gateContract.aggregateIssues(_detectIssues(input, opts));
363
+ }
364
+
365
+ function sanitize(input, opts) {
366
+ opts = _resolveOpts(opts);
367
+ if (typeof input !== "string") {
368
+ throw _err("mime.bad-input", "sanitize requires string input");
369
+ }
370
+ var issues = _detectIssues(input, opts);
371
+ for (var i = 0; i < issues.length; i += 1) {
372
+ if (issues[i].severity === "critical" || issues[i].severity === "high") {
373
+ throw _err(issues[i].ruleId || "mime.refused",
374
+ "guardMime.sanitize: " + issues[i].snippet);
375
+ }
376
+ }
377
+ // Normalize: lowercase the type/subtype; preserve parameter case
378
+ // because some parameter values are case-significant (e.g. boundary
379
+ // tokens in multipart/form-data).
380
+ var parts = _splitTopLevel(input);
381
+ var canonical = parts[0].toLowerCase();
382
+ return parts.slice(1).reduce(function (acc, p) {
383
+ return acc + "; " + p;
384
+ }, canonical);
385
+ }
386
+
387
+ function gate(opts) {
388
+ opts = _resolveOpts(opts);
389
+ return gateContract.buildGuardGate(
390
+ opts.name || "guardMime:" + (opts.profile || "default"),
391
+ opts,
392
+ async function (ctx) {
393
+ var identifier = ctx && (ctx.identifier || ctx.mime || "");
394
+ if (!identifier) return { ok: true, action: "serve" };
395
+ var rv = validate(identifier, opts);
396
+ if (rv.issues.length === 0) return { ok: true, action: "serve" };
397
+ var hasCritical = rv.issues.some(function (i) {
398
+ return i.severity === "critical";
399
+ });
400
+ var hasHigh = rv.issues.some(function (i) {
401
+ return i.severity === "high";
402
+ });
403
+ if (!hasCritical && !hasHigh) {
404
+ return { ok: true, action: "audit-only", issues: rv.issues };
405
+ }
406
+ return { ok: false, action: "refuse", issues: rv.issues };
407
+ });
408
+ }
409
+
410
+ var buildProfile = gateContract.makeProfileBuilder(PROFILES);
411
+
412
+ function compliancePosture(name) {
413
+ return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES,
414
+ _err, "mime");
415
+ }
416
+
417
+ var _mimeRulePacks = gateContract.makeRulePackLoader(GuardMimeError, "mime");
418
+ var loadRulePack = _mimeRulePacks.load;
419
+
420
+ module.exports = {
421
+ // ---- guard-* family registry exports ----
422
+ NAME: "mime",
423
+ KIND: "identifier",
424
+ INTEGRATION_FIXTURES: Object.freeze({
425
+ kind: "identifier",
426
+ benignBytes: Buffer.from("application/json", "utf8"),
427
+ hostileBytes: Buffer.from("application/x-msdownload", "utf8"),
428
+ benignIdentifier: "application/json",
429
+ // Hostile: risky-type — refused at strict (executable script-host
430
+ // class).
431
+ hostileIdentifier: "application/x-msdownload",
432
+ }),
433
+ // ---- primitive surface ----
434
+ validate: validate,
435
+ sanitize: sanitize,
436
+ gate: gate,
437
+ buildProfile: buildProfile,
438
+ compliancePosture: compliancePosture,
439
+ loadRulePack: loadRulePack,
440
+ PROFILES: PROFILES,
441
+ DEFAULTS: DEFAULTS,
442
+ COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
443
+ GuardMimeError: GuardMimeError,
444
+ };
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ /**
3
+ * b.middleware.cookies — inbound cookie-header threat detection.
4
+ *
5
+ * Sits in the request lifecycle and runs `b.cookies.parseSafe` against
6
+ * the inbound `Cookie` header. Threat detection always-on; the gate
7
+ * either refuses, audits, or logs the detected anomalies based on the
8
+ * operator's `mode`.
9
+ *
10
+ * Threats surfaced (see lib/cookies.js parseSafe):
11
+ * - header-cap — Cookie header exceeds maxHeaderBytes
12
+ * - header-control-byte — CR / LF / NUL injected through proxy
13
+ * - pair-malformed — pair missing `=`
14
+ * - pair-empty-name — empty name
15
+ * - name-cap — cookie name exceeds maxNameBytes
16
+ * - value-cap — cookie value exceeds maxValueBytes
17
+ * - duplicate-name — name appears >1 time (cookie-tossing class)
18
+ *
19
+ * Side effects:
20
+ * - req.cookieJar — populated with the parsed jar (overwriteable)
21
+ * - audit emission — one row per detected high-severity issue
22
+ * - response — refused requests get HTTP 400 + JSON body
23
+ *
24
+ * var middleware = b.middleware.cookies({
25
+ * mode: "enforce", // "enforce" | "audit-only" | "log-only"
26
+ * audit: b.audit,
27
+ * maxHeaderBytes: 8 * 1024,
28
+ * refuseOnHigh: true, // 400 if any high-severity issue
29
+ * });
30
+ */
31
+
32
+ var cookies = require("../cookies");
33
+ var lazyRequire = require("../lazy-require");
34
+
35
+ var observability = lazyRequire(function () { return require("../observability"); });
36
+ void observability;
37
+
38
+ function _emitAudit(audit, action, outcome, metadata) {
39
+ if (!audit || typeof audit.safeEmit !== "function") return;
40
+ try {
41
+ audit.safeEmit({
42
+ action: action,
43
+ actor: metadata.actor || { kind: "framework", id: "middleware/cookies" },
44
+ outcome: outcome,
45
+ metadata: metadata,
46
+ });
47
+ } catch (_e) { /* drop-silent — observability sink */ }
48
+ }
49
+
50
+ function create(opts) {
51
+ opts = opts || {};
52
+ var mode = opts.mode || "enforce";
53
+ var refuseOnHigh = opts.refuseOnHigh !== false && mode === "enforce";
54
+ var maxHeaderBytes = opts.maxHeaderBytes;
55
+ var maxNameBytes = opts.maxNameBytes;
56
+ var maxValueBytes = opts.maxValueBytes;
57
+ var audit = opts.audit || null;
58
+
59
+ return function cookiesMiddleware(req, res, next) {
60
+ var header = req && req.headers ? req.headers.cookie : "";
61
+ var rv = cookies.parseSafe(header || "", {
62
+ maxHeaderBytes: maxHeaderBytes,
63
+ maxNameBytes: maxNameBytes,
64
+ maxValueBytes: maxValueBytes,
65
+ });
66
+ req.cookieJar = rv.jar;
67
+
68
+ if (rv.issues.length === 0) return next();
69
+
70
+ var hasHigh = false;
71
+ for (var i = 0; i < rv.issues.length; i += 1) {
72
+ var iss = rv.issues[i];
73
+ if (iss.severity === "high") hasHigh = true;
74
+ _emitAudit(audit, "middleware.cookies.threat-detected",
75
+ iss.severity === "high" ? "blocked" : "audit", {
76
+ kind: iss.kind,
77
+ name: iss.name || null,
78
+ snippet: iss.snippet,
79
+ mode: mode,
80
+ });
81
+ }
82
+
83
+ if (hasHigh && refuseOnHigh) {
84
+ res.statusCode = 400;
85
+ res.setHeader("Content-Type", "application/json");
86
+ res.end(JSON.stringify({
87
+ error: "cookie-threat-detected",
88
+ issues: rv.issues.map(function (i) {
89
+ return { kind: i.kind, severity: i.severity };
90
+ }),
91
+ }));
92
+ return;
93
+ }
94
+ return next();
95
+ };
96
+ }
97
+
98
+ module.exports = { create: create };
@@ -22,6 +22,7 @@ var bearerAuth = require("./bearer-auth");
22
22
  var bodyParser = require("./body-parser");
23
23
  var botGuard = require("./bot-guard");
24
24
  var compression = require("./compression");
25
+ var cookies = require("./cookies");
25
26
  var cors = require("./cors");
26
27
  var cspNonce = require("./csp-nonce");
27
28
  var csrfProtect = require("./csrf-protect");
@@ -52,6 +53,7 @@ module.exports = {
52
53
  bodyParser: bodyParser.create,
53
54
  health: health.create,
54
55
  compression: compression.create,
56
+ cookies: cookies.create,
55
57
  cspNonce: cspNonce.create,
56
58
  sse: sse.create,
57
59
  requestLog: requestLog.create,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.7.46",
3
+ "version": "0.7.48",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -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:2e0e50c0-9f28-4f7f-9a4e-4be5cb7f3707",
5
+ "serialNumber": "urn:uuid:3da7281a-182d-463b-a5eb-166309f6fa76",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-05T21:40:55.433Z",
8
+ "timestamp": "2026-05-05T21:55:22.735Z",
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.46",
22
+ "bom-ref": "@blamejs/core@0.7.48",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.7.46",
25
+ "version": "0.7.48",
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.46",
29
+ "purl": "pkg:npm/%40blamejs/core@0.7.48",
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.46",
57
+ "ref": "@blamejs/core@0.7.48",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]