@blamejs/core 0.7.63 → 0.7.73

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.
@@ -27,14 +27,17 @@ var cors = require("./cors");
27
27
  var cspNonce = require("./csp-nonce");
28
28
  var csrfProtect = require("./csrf-protect");
29
29
  var dbRoleFor = require("./db-role-for");
30
+ var dpop = require("./dpop");
30
31
  var errorHandler = require("./error-handler");
31
32
  var fetchMetadata = require("./fetch-metadata");
33
+ var gpc = require("./gpc");
32
34
  var headers = require("./headers");
33
35
  var health = require("./health");
34
36
  var networkAllowlist = require("./network-allowlist");
35
37
  var rateLimit = require("./rate-limit");
36
38
  var requestId = require("./request-id");
37
39
  var requestLog = require("./request-log");
40
+ var requireAal = require("./require-aal");
38
41
  var requireAuth = require("./require-auth");
39
42
  var securityHeaders = require("./security-headers");
40
43
  var sse = require("./sse");
@@ -48,9 +51,11 @@ module.exports = {
48
51
  rateLimit: rateLimit.create,
49
52
  attachUser: attachUser.create,
50
53
  bearerAuth: bearerAuth.create,
54
+ requireAal: requireAal.create,
51
55
  requireAuth: requireAuth.create,
52
56
  csrfProtect: csrfProtect.create,
53
57
  fetchMetadata: fetchMetadata.create,
58
+ gpc: gpc.create,
54
59
  headers: headers.create,
55
60
  bodyParser: bodyParser.create,
56
61
  health: health.create,
@@ -61,6 +66,7 @@ module.exports = {
61
66
  requestLog: requestLog.create,
62
67
  apiEncrypt: apiEncrypt,
63
68
  dbRoleFor: dbRoleFor.create,
69
+ dpop: dpop.create,
64
70
  networkAllowlist: networkAllowlist.create,
65
71
 
66
72
  // Module exports for advanced use (constants, raw factory access)
@@ -73,6 +79,7 @@ module.exports = {
73
79
  rateLimit: rateLimit,
74
80
  attachUser: attachUser,
75
81
  bearerAuth: bearerAuth,
82
+ requireAal: requireAal,
76
83
  requireAuth: requireAuth,
77
84
  csrfProtect: csrfProtect,
78
85
  fetchMetadata: fetchMetadata,
@@ -84,6 +91,7 @@ module.exports = {
84
91
  requestLog: requestLog,
85
92
  apiEncrypt: apiEncrypt,
86
93
  dbRoleFor: dbRoleFor,
94
+ dpop: dpop,
87
95
  networkAllowlist: networkAllowlist,
88
96
  },
89
97
  };
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ /**
3
+ * require-aal middleware — gate routes by NIST SP 800-63-4 AAL band.
4
+ *
5
+ * var stepUp = b.middleware.requireAal({ minimum: "AAL2" });
6
+ * router.use("/admin", stepUp);
7
+ *
8
+ * Reads the AAL band from `req.user.aal` by default. Operators with a
9
+ * different shape pass `getAal(req)` returning the band string.
10
+ *
11
+ * On failure the middleware writes 401 with
12
+ * `WWW-Authenticate: AAL-StepUp realm="<X>", required="<minimum>"`
13
+ * — the bespoke scheme name signals to the operator's frontend that a
14
+ * step-up flow should be triggered (re-prompt for TOTP / passkey).
15
+ *
16
+ * Audit:
17
+ * auth.aal.granted — request passed (carries the actual band)
18
+ * auth.aal.denied — request below the required minimum
19
+ */
20
+
21
+ var lazyRequire = require("../lazy-require");
22
+ var requestHelpers = require("../request-helpers");
23
+ var validateOpts = require("../validate-opts");
24
+ var { AuthError } = require("../framework-error");
25
+
26
+ var aal = lazyRequire(function () { return require("../auth/aal"); });
27
+ var audit = lazyRequire(function () { return require("../audit"); });
28
+
29
+ function _writeUnauthorized(res, requiredBand, actualBand, realm) {
30
+ if (res.headersSent) return;
31
+ var body = JSON.stringify({
32
+ error: "step_up_required",
33
+ error_description: "AAL " + requiredBand + " is required for this resource",
34
+ required_aal: requiredBand,
35
+ actual_aal: actualBand || null,
36
+ });
37
+ var realmStr = realm ? ' realm="' + realm + '"' : "";
38
+ var challenge = "AAL-StepUp" + realmStr + ', required="' + requiredBand + '"';
39
+ res.writeHead(401, { // allow:raw-byte-literal — HTTP 401 status
40
+ "Content-Type": "application/json; charset=utf-8",
41
+ "Content-Length": Buffer.byteLength(body),
42
+ "WWW-Authenticate": challenge,
43
+ });
44
+ res.end(body);
45
+ }
46
+
47
+ function create(opts) {
48
+ opts = opts || {};
49
+ validateOpts(opts, [
50
+ "minimum", "getAal", "audit", "realm",
51
+ ], "middleware.requireAal");
52
+
53
+ var minimum = opts.minimum;
54
+ if (!aal().isValidBand(minimum)) {
55
+ throw new AuthError("auth-aal/bad-minimum",
56
+ "middleware.requireAal: opts.minimum must be one of " +
57
+ aal().BANDS.join(", ") + " (got " + JSON.stringify(minimum) + ")");
58
+ }
59
+ validateOpts.optionalFunction(opts.getAal,
60
+ "middleware.requireAal: getAal", AuthError, "auth-aal/bad-opt");
61
+
62
+ var auditOn = opts.audit !== false;
63
+ var realm = (typeof opts.realm === "string" && opts.realm.length > 0) ? opts.realm : null;
64
+
65
+ return function requireAalMiddleware(req, res, next) {
66
+ var actual = null;
67
+ if (typeof opts.getAal === "function") {
68
+ try { actual = opts.getAal(req); } catch (_e) { actual = null; }
69
+ } else if (req.user && typeof req.user.aal === "string") {
70
+ actual = req.user.aal;
71
+ }
72
+
73
+ if (!aal().meets(actual, minimum)) {
74
+ if (auditOn) {
75
+ try {
76
+ audit().safeEmit({
77
+ action: "auth.aal.denied",
78
+ actor: { clientIp: requestHelpers.clientIp(req), userId: req.user && req.user.id },
79
+ outcome: "fail",
80
+ metadata: {
81
+ required: minimum,
82
+ actual: actual || null,
83
+ route: req.url,
84
+ },
85
+ });
86
+ } catch (_ignored) { /* drop-silent */ }
87
+ }
88
+ return _writeUnauthorized(res, minimum, actual, realm);
89
+ }
90
+
91
+ if (auditOn) {
92
+ try {
93
+ audit().safeEmit({
94
+ action: "auth.aal.granted",
95
+ actor: { clientIp: requestHelpers.clientIp(req), userId: req.user && req.user.id },
96
+ outcome: "ok",
97
+ metadata: { aal: actual, required: minimum, route: req.url },
98
+ });
99
+ } catch (_ignored) { /* drop-silent */ }
100
+ }
101
+ return next();
102
+ };
103
+ }
104
+
105
+ module.exports = {
106
+ create: create,
107
+ };
@@ -75,6 +75,7 @@ function create(opts) {
75
75
  "hsts", "contentTypeOptions", "frameOptions", "referrerPolicy",
76
76
  "permissionsPolicy", "coop", "coep", "corp",
77
77
  "originAgentCluster", "dnsPrefetchControl", "csp", "trustProxy",
78
+ "reportingEndpoints",
78
79
  ], "middleware.securityHeaders");
79
80
  var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
80
81
  ? opts.trustProxy : false;
@@ -89,6 +90,32 @@ function create(opts) {
89
90
  var oac = opts.originAgentCluster === undefined ? "?1" : opts.originAgentCluster;
90
91
  var dpc = opts.dnsPrefetchControl === undefined ? "off" : opts.dnsPrefetchControl;
91
92
  var csp = opts.csp === undefined ? DEFAULT_CSP : opts.csp;
93
+ // Reporting-Endpoints (W3C Reporting API) — when operator passes a
94
+ // map of endpoint-name → URL, we emit `Reporting-Endpoints: name="url",
95
+ // name2="url2", ...` and (when default CSP is in force) append
96
+ // `report-to default` to the CSP so violations route to the named
97
+ // endpoint. Operators using a custom CSP add `report-to` to it
98
+ // themselves.
99
+ var reportingEndpoints = null;
100
+ if (opts.reportingEndpoints && typeof opts.reportingEndpoints === "object") {
101
+ var pairs = [];
102
+ var keys = Object.keys(opts.reportingEndpoints);
103
+ for (var i = 0; i < keys.length; i += 1) {
104
+ var k = keys[i];
105
+ var v = opts.reportingEndpoints[k];
106
+ if (typeof v !== "string" || v.length === 0) continue;
107
+ // Defensive — refuse CR/LF/NUL in either side (header injection).
108
+ if (/[\r\n\0]/.test(k) || /[\r\n\0]/.test(v)) continue; // allow:duplicate-regex — CR/LF/NUL header-injection rejection appears in cookies / mail / security-headers; each is the boundary primitive — extracting forces a shared module that hides the boundary check from each domain
109
+ pairs.push(k + '="' + v + '"');
110
+ }
111
+ if (pairs.length > 0) reportingEndpoints = pairs.join(", ");
112
+ }
113
+ // Auto-append `report-to default` to the default CSP when operator
114
+ // wires a `default` reporting endpoint and didn't override `csp`.
115
+ if (csp === DEFAULT_CSP && reportingEndpoints &&
116
+ opts.reportingEndpoints && opts.reportingEndpoints["default"]) {
117
+ csp = csp.replace(/;\s*$/, "") + "; report-to default;";
118
+ }
92
119
 
93
120
  return function securityHeaders(req, res, next) {
94
121
  if (typeof res.setHeader !== "function") return next();
@@ -109,7 +136,8 @@ function create(opts) {
109
136
  if (corp) res.setHeader("Cross-Origin-Resource-Policy", corp);
110
137
  if (oac) res.setHeader("Origin-Agent-Cluster", oac);
111
138
  if (dpc) res.setHeader("X-DNS-Prefetch-Control", dpc);
112
- if (csp) res.setHeader("Content-Security-Policy", csp);
139
+ if (csp) res.setHeader("Content-Security-Policy", csp);
140
+ if (reportingEndpoints) res.setHeader("Reporting-Endpoints", reportingEndpoints);
113
141
  next();
114
142
  };
115
143
  }
@@ -291,28 +291,44 @@ function _encodeDnsQuery(host, qtype) {
291
291
  return { buf: buf, id: id };
292
292
  }
293
293
 
294
+ // Walk a DNS-message name in-place and advance `state.off`. RFC 1035
295
+ // §3.1 names terminate either with a single 0x00 byte OR with a
296
+ // 2-byte compression pointer (high two bits 11). The pre-0.7.68
297
+ // parser unconditionally executed `if (buf[off] === 0) off++`
298
+ // after the loop, which consumed the high byte of the next field
299
+ // when the loop had exited via the compression pointer — silently
300
+ // breaking every DNS response that used name compression in the
301
+ // answer section (which is most of them).
302
+ function _skipDnsName(buf, state) {
303
+ var endedViaPointer = false;
304
+ while (state.off < buf.length && buf[state.off] !== 0) {
305
+ if ((buf[state.off] & 0xc0) === 0xc0) { // allow:raw-byte-literal — RFC 1035 name-compression pointer mask
306
+ state.off += 2;
307
+ endedViaPointer = true;
308
+ break;
309
+ }
310
+ state.off += buf[state.off] + 1;
311
+ }
312
+ if (!endedViaPointer && state.off < buf.length && buf[state.off] === 0) {
313
+ state.off += 1;
314
+ }
315
+ }
316
+
294
317
  function _decodeDnsAnswer(buf, qtype) {
295
318
  if (!Buffer.isBuffer(buf) || buf.length < 12) throw new DnsError("dns/bad-reply", "dns reply truncated");
296
319
  var rcode = buf.readUInt8(3) & 0x0f;
297
320
  if (rcode !== 0) throw new DnsError("dns/no-result", "dns reply rcode " + rcode);
298
321
  var qdcount = buf.readUInt16BE(4);
299
322
  var ancount = buf.readUInt16BE(6);
300
- var off = 12;
323
+ var state = { off: 12 };
301
324
  for (var q = 0; q < qdcount; q++) {
302
- while (off < buf.length && buf[off] !== 0) {
303
- if ((buf[off] & 0xc0) === 0xc0) { off += 2; break; }
304
- off += buf[off] + 1;
305
- }
306
- if (buf[off] === 0) off++;
307
- off += 4;
325
+ _skipDnsName(buf, state);
326
+ state.off += 4;
308
327
  }
309
328
  var addrs = [];
310
329
  for (var a = 0; a < ancount; a++) {
311
- while (off < buf.length && buf[off] !== 0) {
312
- if ((buf[off] & 0xc0) === 0xc0) { off += 2; break; }
313
- off += buf[off] + 1;
314
- }
315
- if (buf[off] === 0) off++;
330
+ _skipDnsName(buf, state);
331
+ var off = state.off;
316
332
  var rtype = buf.readUInt16BE(off); off += 2;
317
333
  off += 2;
318
334
  off += 4;
@@ -327,10 +343,20 @@ function _decodeDnsAnswer(buf, qtype) {
327
343
  addrs.push(groups.join(":"));
328
344
  }
329
345
  off += rdlen;
346
+ state.off = off;
330
347
  }
331
348
  return addrs;
332
349
  }
333
350
 
351
+ // Read the AD bit (Authenticated Data, RFC 4035) from a DNS reply
352
+ // header. Byte 3 holds RA, Z, AD, CD, and rcode bits; AD is bit 5
353
+ // (mask 0x20). Set when the upstream recursive resolver has validated
354
+ // the chain.
355
+ function _readAdBit(buf) {
356
+ if (!Buffer.isBuffer(buf) || buf.length < 12) return false;
357
+ return (buf.readUInt8(3) & 0x20) !== 0; // allow:raw-byte-literal — RFC 4035 AD-bit mask
358
+ }
359
+
334
360
  // DoH GET URL length cap. RFC 8484 §4.1 says clients MAY use POST when
335
361
  // the GET URL would exceed implementation limits. We pick 2048 bytes
336
362
  // (a conservative ceiling well below RFC 7230's recommended 8 KB) so
@@ -404,6 +430,98 @@ async function _dohLookup(host, family) {
404
430
  });
405
431
  }
406
432
 
433
+ // _dohLookupSecure — DNSSEC-aware DoH lookup. Returns `{ rrs, ad }`
434
+ // where `ad` is the AD bit (RFC 4035) set by the upstream resolver
435
+ // after chain validation. Internal — operators reach for
436
+ // `resolveSecure` instead.
437
+ async function _dohLookupSecure(host, family) {
438
+ var qtype = family === 6 ? 28 : 1; // allow:raw-byte-literal — DNS QTYPE values for A / AAAA
439
+ var enc = _encodeDnsQuery(host, qtype);
440
+ var b64 = enc.buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
441
+ var getUrl = STATE.doh.url + (STATE.doh.url.indexOf("?") === -1 ? "?" : "&") + "dns=" + b64;
442
+ var forcedMethod = STATE.doh.method;
443
+ var usePost = forcedMethod === "POST" || (!forcedMethod && getUrl.length > DOH_GET_URL_MAX_BYTES);
444
+ var u = safeUrl.parse(STATE.doh.url, { allowedProtocols: safeUrl.ALLOW_HTTP_TLS });
445
+ return new Promise(function (resolve, reject) {
446
+ var reqOpts = {
447
+ hostname: u.hostname,
448
+ port: u.port || 443, // allow:raw-byte-literal — HTTPS default port
449
+ path: u.pathname + u.search,
450
+ method: usePost ? "POST" : "GET",
451
+ headers: { "accept": "application/dns-message" },
452
+ minVersion: "TLSv1.3",
453
+ ecdhCurve: C.TLS_GROUP_CURVE_STR,
454
+ };
455
+ if (STATE.doh.ca) reqOpts.ca = STATE.doh.ca;
456
+ if (usePost) {
457
+ reqOpts.headers["content-type"] = "application/dns-message";
458
+ reqOpts.headers["content-length"] = enc.buf.length;
459
+ } else {
460
+ var parsedGet = safeUrl.parse(getUrl, { allowedProtocols: safeUrl.ALLOW_HTTP_TLS });
461
+ reqOpts.path = parsedGet.pathname + parsedGet.search;
462
+ }
463
+ var req = https.request(reqOpts, function (res) {
464
+ var collector = safeBuffer.boundedChunkCollector({
465
+ maxBytes: C.BYTES.kib(256),
466
+ errorClass: DnsError,
467
+ sizeCode: "dns/doh-too-large",
468
+ sizeMessage: "DoH response exceeds 256 KiB",
469
+ });
470
+ var pushFailed = null;
471
+ res.on("data", function (c) {
472
+ if (pushFailed) return;
473
+ try { collector.push(c); }
474
+ catch (e) { pushFailed = e; }
475
+ });
476
+ res.on("end", function () {
477
+ try {
478
+ if (pushFailed) { reject(pushFailed); return; }
479
+ var body = collector.result();
480
+ if (res.statusCode !== 200) { // allow:raw-byte-literal — HTTP 200 OK
481
+ reject(new DnsError("dns/doh-http", "DoH HTTP " + res.statusCode + " for " + host));
482
+ return;
483
+ }
484
+ resolve({ rrs: _decodeDnsAnswer(body, qtype), ad: _readAdBit(body) });
485
+ } catch (e) { reject(e); }
486
+ });
487
+ });
488
+ req.on("error", function (e) { reject(new DnsError("dns/doh-failed", "DoH request failed: " + e.message)); });
489
+ if (usePost) req.write(enc.buf);
490
+ req.end();
491
+ });
492
+ }
493
+
494
+ // resolveSecure — DNSSEC-aware resolution. Returns `{ rrs, ad }`
495
+ // where `ad` is the AD bit (RFC 4035) set by the upstream DoH
496
+ // resolver after chain validation, and `rrs` is the answer-record
497
+ // list (IPv4 / IPv6 string addresses for A / AAAA queries).
498
+ //
499
+ // Operators wiring DANE / TLSA validation (RFC 7672 SMTP DANE)
500
+ // require `ad === true` to honor the DANE security claim per RFC
501
+ // 7672 §1.3 — without DNSSEC validation the TLSA records can't be
502
+ // authenticated and the chain check is meaningless.
503
+ //
504
+ // Only available over DoH transport. The system resolver and DoT
505
+ // transports don't surface the AD bit through Node's API today.
506
+ async function resolveSecure(host, type) {
507
+ type = type || "A";
508
+ if (!STATE.doh) {
509
+ throw new DnsError("dns/secure-requires-doh",
510
+ "resolveSecure requires DoH transport (call useDnsOverHttps " +
511
+ "or rely on the default-on DoH posture)");
512
+ }
513
+ if (typeof host !== "string" || host.length === 0 || host.length > 253) { // allow:raw-byte-literal — RFC 1035 hostname octet ceiling
514
+ throw new DnsError("dns/bad-host",
515
+ "resolveSecure host is malformed");
516
+ }
517
+ var family;
518
+ if (type === "A") family = 4;
519
+ else if (type === "AAAA") family = 6;
520
+ else throw new DnsError("dns/secure-unsupported-type",
521
+ "resolveSecure currently supports A and AAAA; got " + type);
522
+ return _dohLookupSecure(host, family);
523
+ }
524
+
407
525
  // DoT connection pool. Per-(host:port) cached TLS socket so successive
408
526
  // lookups amortize the handshake. Sockets idle past the timeout are
409
527
  // closed and removed; first lookup after expiry rebuilds. Each socket
@@ -695,6 +813,7 @@ module.exports = {
695
813
  resolve4: resolve4,
696
814
  resolve6: resolve6,
697
815
  resolveAaaa: resolveAaaa,
816
+ resolveSecure: resolveSecure,
698
817
  nodeLookup: nodeLookup,
699
818
  clearCache: _clearCache,
700
819
  DnsError: DnsError,
@@ -42,9 +42,13 @@
42
42
  * - Full DANE certificate-chain verification per RFC 6698 (needs
43
43
  * ASN.1 cert parsing). Operators today verify policy presence +
44
44
  * match the leaf SHA-256 themselves.
45
- * - DNSSEC-validated DANE lookups (node:dns doesn't expose
46
- * DNSSEC ad-bit; operators pin to a DNSSEC-validating resolver
47
- * externally).
45
+ * - DNSSEC-validated DANE lookups: the framework now exposes the
46
+ * AD bit via `b.network.dns.resolveSecure(name, type) { rrs,
47
+ * ad }`. Operators wiring strict RFC 7672 §1.3 compliance pass
48
+ * a DNSSEC-aware resolver via opts and refuse the chain when
49
+ * ad === false. The default `tlsa()` lookup path stays on
50
+ * `node:dns` for compatibility with operators on system
51
+ * resolvers.
48
52
  */
49
53
 
50
54
  var dns = require("node:dns");
package/lib/router.js CHANGED
@@ -626,10 +626,32 @@ class Router {
626
626
  // accept both. enableConnectProtocol: true is what enables h2
627
627
  // WebSocket (RFC 8441) — clients refuse to issue Extended CONNECT
628
628
  // until they see this in the server's SETTINGS frame.
629
+ // Framework-default HTTP/2 hardening — operator-supplied
630
+ // tlsOptions can override any of these.
631
+ //
632
+ // maxConcurrentStreams: cap concurrent streams per session (Node
633
+ // default is 4294967295 — way too high; CVE-2023-44487 Rapid
634
+ // Reset relies on the unbounded default).
635
+ // maxSessionMemory: 10 MB cap per session (Node default; explicit).
636
+ // maxHeaderListPairs: 100 header pairs max (Node default 128;
637
+ // tightened — CVE-2024-27983 / CVE-2024-28182 CONTINUATION
638
+ // flood relies on header-pair amplification).
639
+ // maxSettings: cap SETTINGS-frame entries.
640
+ // peerMaxConcurrentStreams: cap how many streams the peer is
641
+ // willing to accept (limits server-initiated push, which the
642
+ // framework doesn't use).
643
+ // unknownProtocolTimeout: 10s — drop sessions stuck in protocol-
644
+ // detection (Slowloris-h2 variant).
629
645
  server = http2.createSecureServer(Object.assign({
630
- allowHTTP1: true,
631
- ALPNProtocols: ["h2", "http/1.1"],
632
- settings: { enableConnectProtocol: true },
646
+ allowHTTP1: true,
647
+ ALPNProtocols: ["h2", "http/1.1"],
648
+ settings: { enableConnectProtocol: true },
649
+ maxConcurrentStreams: 100, // allow:raw-byte-literal — CVE-2023-44487 Rapid Reset cap
650
+ maxSessionMemory: 10, // allow:raw-byte-literal — MB cap (Node default explicit)
651
+ maxHeaderListPairs: 100, // allow:raw-byte-literal — CVE-2024-27983 CONTINUATION-flood cap
652
+ maxSettings: 32, // allow:raw-byte-literal — SETTINGS-frame entry ceiling
653
+ peerMaxConcurrentStreams: 100, // allow:raw-byte-literal — peer-side stream cap
654
+ unknownProtocolTimeout: C.TIME.seconds(10),
633
655
  }, tlsOptions), requestHandler);
634
656
  } else {
635
657
  // Cleartext path is h1-only. Operators wanting h2c on cleartext
@@ -710,7 +732,23 @@ class Router {
710
732
 
711
733
  if (host) server.listen(port, host, cb);
712
734
  else server.listen(port, cb);
713
- server.timeout = C.TIME.minutes(5);
735
+ // Slowloris / slow-read defenses. Node defaults shifted across
736
+ // versions; the framework pins them explicitly so operators on
737
+ // older Node releases get the modern bar.
738
+ //
739
+ // headersTimeout: 60s — time allotted for the entire request-line
740
+ // + header section. Slowloris's classic posture is a connection
741
+ // that trickles headers indefinitely.
742
+ // requestTimeout: 5min — total wall-clock for a request including
743
+ // body. Body-streaming uploads through fileUpload can take
744
+ // minutes; this is the operator-overridable ceiling.
745
+ // keepAliveTimeout: 5s — idle timeout between requests on a
746
+ // keep-alive connection.
747
+ // server.timeout: 5min — hardware/network timeout (legacy).
748
+ server.headersTimeout = C.TIME.seconds(60);
749
+ server.requestTimeout = C.TIME.minutes(5);
750
+ server.keepAliveTimeout = C.TIME.seconds(5);
751
+ server.timeout = C.TIME.minutes(5);
714
752
  return server;
715
753
  }
716
754
  }
@@ -13,7 +13,10 @@
13
13
  "server": "lib/vendor/noble-ciphers.cjs"
14
14
  },
15
15
  "bundler": "esbuild --format=cjs --minify --platform=node",
16
- "bundledAt": "2026-04-25"
16
+ "bundledAt": "2026-04-25",
17
+ "hashes": {
18
+ "server": "sha256:5d539dfc9ef47121d4c09bd7256d76448a1f5ac47ee09ac44c78ff6a062af9ab"
19
+ }
17
20
  },
18
21
  "argon2": {
19
22
  "version": "0.44.0",
@@ -39,7 +42,11 @@
39
42
  "win32-x64"
40
43
  ],
41
44
  "bundler": "esbuild --format=cjs --platform=node (inlines @phc/format + node-gyp-build)",
42
- "bundledAt": "2026-04-25"
45
+ "bundledAt": "2026-04-25",
46
+ "hashes": {
47
+ "server": "sha256:93b8d2fb7f24dc1b3304dc9420844d5e1afc199c41ab1f9a90c8de48cc7c2359",
48
+ "prebuilds": "sha256-tree:65921b7cf331e0a9430a1b52440da8f26cdf9d215a4cd490edbc4804dd713df3"
49
+ }
43
50
  },
44
51
  "@simplewebauthn/server": {
45
52
  "version": "13.3.0",
@@ -57,7 +64,10 @@
57
64
  "server": "lib/vendor/simplewebauthn-server.cjs"
58
65
  },
59
66
  "bundler": "esbuild --format=cjs --minify --platform=node --external:crypto --external:node:crypto",
60
- "bundledAt": "2026-04-26"
67
+ "bundledAt": "2026-04-26",
68
+ "hashes": {
69
+ "server": "sha256:a9777dca582095d67f17ca24e19a0791de29928555b6b779c2233429175eb3f0"
70
+ }
61
71
  },
62
72
  "SecLists-common-passwords-top-10000": {
63
73
  "version": "10k-most-common (master)",
@@ -69,7 +79,10 @@
69
79
  "server": "lib/vendor/common-passwords-top-10000.txt"
70
80
  },
71
81
  "bundler": "curl https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/Common-Credentials/10k-most-common.txt",
72
- "bundledAt": "2026-05-02"
82
+ "bundledAt": "2026-05-02",
83
+ "hashes": {
84
+ "server": "sha256:4adb3f0afb4a10cf19ebe48d8c69a46f934bbc8d77c694c210564f9583e7f4ba"
85
+ }
73
86
  },
74
87
  "peculiar-pki": {
75
88
  "version": "2.0.0+pkijs-3.4.0",
@@ -90,7 +103,10 @@
90
103
  "server": "lib/vendor/pki.cjs"
91
104
  },
92
105
  "bundler": "esbuild --format=cjs --minify --platform=node --external:crypto --external:node:crypto",
93
- "bundledAt": "2026-04-29"
106
+ "bundledAt": "2026-04-29",
107
+ "hashes": {
108
+ "server": "sha256:9bbc191afaaa2b1e5757f00480457c08134cdc2c55d541df18d9155bba9cbf77"
109
+ }
94
110
  }
95
111
  }
96
112
  }
package/lib/websocket.js CHANGED
@@ -221,12 +221,17 @@ function negotiateSubprotocol(req, supported) {
221
221
  // origins shapes:
222
222
  // array — strict allowlist, enforced
223
223
  // "*" — explicit "accept all" (operator opt-in to no checking)
224
- // null/undefined — same as "*" but caller (router) is expected to
225
- // have logged a startup warning. Origin policy is a
226
- // framework-level decision; this primitive doesn't
227
- // re-warn here.
224
+ // null/undefined — DEFAULT: same-origin (Origin host matches Host
225
+ // header). The pre-0.7.64 default was "accept all"
226
+ // flipped here because cross-site WebSocket
227
+ // hijacking (CSWSH) is a real attacker capability
228
+ // against any browser-targeted WebSocket route, and
229
+ // same-origin is the safe default. Operators
230
+ // needing cross-origin opt in explicitly via
231
+ // `origins: "*"` (with audited reason) or
232
+ // `origins: [...allowlist]`.
228
233
  function isOriginAllowed(req, origins) {
229
- if (!origins || origins === "*") return true;
234
+ if (origins === "*") return true;
230
235
  var origin = (req.headers || {}).origin;
231
236
  // Non-browser clients (curl, server-to-server, native apps) don't
232
237
  // send Origin. Origin enforcement only meaningfully applies to
@@ -234,6 +239,17 @@ function isOriginAllowed(req, origins) {
234
239
  // the operator's network ACL / auth middleware, not Origin.
235
240
  if (!origin) return true;
236
241
  if (Array.isArray(origins)) return origins.indexOf(origin) !== -1;
242
+ // Default: same-origin. Compare the Origin header's hostname against
243
+ // the Host header. Operators behind a TLS-terminating LB pass the
244
+ // canonical Host through (or set `origins: [...]` explicitly).
245
+ if (!origins) {
246
+ var host = (req.headers || {}).host;
247
+ if (!host) return false;
248
+ var originHost;
249
+ try { originHost = new URL(origin).host; } // allow:raw-new-url — comparing browser-supplied Origin header against Host; safeUrl.parse adds policy filtering that isn't appropriate for exact host comparison
250
+ catch (_e) { return false; }
251
+ return originHost === host;
252
+ }
237
253
  return false;
238
254
  }
239
255
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.7.63",
3
+ "version": "0.7.73",
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:e7800257-940b-492f-8159-1b9d7b8b5f90",
5
+ "serialNumber": "urn:uuid:55a0114a-f249-481f-b122-6997247485a1",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-06T00:32:55.128Z",
8
+ "timestamp": "2026-05-06T03:31:31.641Z",
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.63",
22
+ "bom-ref": "@blamejs/core@0.7.73",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.7.63",
25
+ "version": "0.7.73",
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.63",
29
+ "purl": "pkg:npm/%40blamejs/core@0.7.73",
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.63",
57
+ "ref": "@blamejs/core@0.7.73",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]