@blamejs/blamejs-shop 0.4.84 → 0.4.86

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/inventory-locations.js +86 -0
  4. package/lib/order.js +4 -2
  5. package/lib/security-middleware.js +1 -0
  6. package/lib/stock-transfers.js +21 -14
  7. package/lib/vendor/MANIFEST.json +71 -39
  8. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  9. package/lib/vendor/blamejs/api-snapshot.json +11 -3
  10. package/lib/vendor/blamejs/lib/atomic-file.js +34 -0
  11. package/lib/vendor/blamejs/lib/auth/fido-mds3.js +10 -0
  12. package/lib/vendor/blamejs/lib/auth/password.js +1 -0
  13. package/lib/vendor/blamejs/lib/auth/saml.js +11 -3
  14. package/lib/vendor/blamejs/lib/daemon.js +4 -1
  15. package/lib/vendor/blamejs/lib/external-db.js +131 -0
  16. package/lib/vendor/blamejs/lib/graphql-federation.js +25 -15
  17. package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +1 -0
  18. package/lib/vendor/blamejs/lib/log-stream-local.js +14 -1
  19. package/lib/vendor/blamejs/lib/log-stream-otlp.js +1 -0
  20. package/lib/vendor/blamejs/lib/log-stream-webhook.js +1 -0
  21. package/lib/vendor/blamejs/lib/mail-auth.js +69 -14
  22. package/lib/vendor/blamejs/lib/mail-bimi.js +6 -0
  23. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +10 -0
  24. package/lib/vendor/blamejs/lib/mail-dkim.js +68 -15
  25. package/lib/vendor/blamejs/lib/mail.js +39 -0
  26. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +6 -2
  27. package/lib/vendor/blamejs/lib/network-dns-resolver.js +61 -11
  28. package/lib/vendor/blamejs/lib/network-dns.js +47 -2
  29. package/lib/vendor/blamejs/lib/network-nts.js +16 -0
  30. package/lib/vendor/blamejs/lib/network-proxy.js +55 -2
  31. package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +1 -0
  32. package/lib/vendor/blamejs/lib/object-store/gcs-bucket-ops.js +1 -0
  33. package/lib/vendor/blamejs/lib/object-store/http-request.js +4 -0
  34. package/lib/vendor/blamejs/lib/outbox.js +29 -0
  35. package/lib/vendor/blamejs/lib/queue-sqs.js +1 -0
  36. package/lib/vendor/blamejs/lib/request-helpers.js +25 -6
  37. package/lib/vendor/blamejs/lib/session-device-binding.js +46 -24
  38. package/lib/vendor/blamejs/lib/session.js +85 -28
  39. package/lib/vendor/blamejs/package.json +1 -1
  40. package/lib/vendor/blamejs/release-notes/v0.15.16.json +94 -0
  41. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +51 -0
  42. package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-open-append-nofollow.test.js +87 -0
  43. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +48 -0
  44. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-non-atomic-backend.test.js +200 -0
  45. package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3-cert-bad-validity.test.js +159 -0
  46. package/lib/vendor/blamejs/test/layer-0-primitives/graphql-federation.test.js +49 -2
  47. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth-dmarc-policy-failclosed.test.js +139 -0
  48. package/lib/vendor/blamejs/test/layer-0-primitives/mail-bimi-cert-validity-unparseable.test.js +137 -0
  49. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime-bad-validity.test.js +134 -0
  50. package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim-numericdate-failclosed.test.js +155 -0
  51. package/lib/vendor/blamejs/test/layer-0-primitives/mail-proxy-framing-bounds.test.js +263 -0
  52. package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-lookup-timeout-default.test.js +116 -0
  53. package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-resolver-timeout.test.js +126 -0
  54. package/lib/vendor/blamejs/test/layer-0-primitives/network-nts-handshake-byte-cap.test.js +127 -0
  55. package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notbefore.test.js +238 -0
  56. package/lib/vendor/blamejs/test/layer-0-primitives/session-destroy-all-store-backed.test.js +128 -0
  57. package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding-ipv6-canonical-and-no-store.test.js +202 -0
  58. package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding.test.js +18 -9
  59. package/lib/vendor/blamejs/test/layer-0-primitives/webhook-verify-nonce-atomic.test.js +169 -0
  60. package/lib/webhooks.js +38 -9
  61. package/package.json +1 -1
@@ -482,6 +482,36 @@ function _parseDkimTagList(value) {
482
482
  return tags;
483
483
  }
484
484
 
485
+ // RFC 6376 §3.5 — x= / t= / l= are unsigned decimal integers ("1*DIGIT").
486
+ // parseInt() is too lenient here ("12abc" → 12, "0x10" → 0, " 12" → 12),
487
+ // so a present-but-malformed value must not be coerced into a finite
488
+ // number that then passes the downstream check. Returns the parsed integer,
489
+ // or null when the value is not a well-formed unsigned integer — the caller
490
+ // fails CLOSED on null. `maxDigits` bounds the digit count (a ReDoS-free
491
+ // length cap; x=/t= are NumericDate, bounded to 12 digits).
492
+ function _parseDkimUnsignedInt(raw, maxDigits) {
493
+ if (typeof raw !== "string") return null;
494
+ var len = raw.length;
495
+ // "1*DIGIT": non-empty, ASCII 0-9 only, length-capped. A char-code scan
496
+ // (no regex) keeps this linear and avoids a dynamic RegExp / a shared
497
+ // digit-pattern literal — maxDigits bounds x=/t=/l= to their NumericDate
498
+ // / octet-count headroom.
499
+ if (len === 0 || (maxDigits && len > maxDigits)) return null;
500
+ for (var i = 0; i < len; i++) {
501
+ var c = raw.charCodeAt(i);
502
+ if (c < 0x30 || c > 0x39) return null; // not an ASCII digit
503
+ }
504
+ var n = parseInt(raw, 10);
505
+ if (!isFinite(n)) return null;
506
+ return n;
507
+ }
508
+
509
+ // x= / t= are NumericDate (seconds-since-epoch); 12 digits is ample headroom
510
+ // past any realistic epoch and keeps the parse bounded.
511
+ function _parseDkimNumericDate(raw) {
512
+ return _parseDkimUnsignedInt(raw, 12);
513
+ }
514
+
485
515
  function _selectorTxtToKeyTags(txtRecords) {
486
516
  // DKIM key record is a TXT record at <selector>._domainkey.<domain>.
487
517
  // Format: "v=DKIM1; k=rsa; p=<base64>" (chunks may be split across
@@ -694,8 +724,16 @@ function _verifySingleSignature(rfc822, parsedHeaders, sigHeader, keyTags, sigTa
694
724
  // the signature still validates against legitimate senders that
695
725
  // use l=, but flag in the result. The cap is octets of the
696
726
  // CANONICALIZED body (RFC 6376 §3.4.5), applied inside _bodyHashB64.
697
- var parsedL = parseInt(sigTags.l, 10);
698
- if (isFinite(parsedL) && parsedL >= 0) lcap = parsedL;
727
+ // l= is an unsigned decimal integer ("1*DIGIT" per §3.5); a
728
+ // present-but-unparseable value must FAIL CLOSED rather than fall
729
+ // through to lcap=undefined (hashing the whole body silently masks the
730
+ // malformed tag instead of refusing it).
731
+ var parsedL = _parseDkimUnsignedInt(sigTags.l, 18);
732
+ if (parsedL === null) {
733
+ return { result: "permerror",
734
+ errors: ["DKIM-Signature l= present but unparseable (RFC 6376 §3.5 — unsigned integer required)"] };
735
+ }
736
+ lcap = parsedL;
699
737
  }
700
738
 
701
739
  // 1. Body-hash check.
@@ -922,34 +960,49 @@ async function verify(rfc822, opts) {
922
960
  // attack). Both are in seconds-since-epoch per ABNF.
923
961
  var nowSec = Math.floor(Date.now() / C.TIME.seconds(1));
924
962
  var clockSkewSec = Math.floor(clockSkewMs / C.TIME.seconds(1));
963
+ // x= / t= are NumericDate (digits-only seconds-since-epoch per the
964
+ // §3.5 ABNF). A present-but-unparseable value must FAIL CLOSED — a NaN
965
+ // result silently skipping the expiry / future-date / ordering checks
966
+ // would let verification proceed to result:"pass" with NO expiry
967
+ // enforced (mirrors the SAML Conditions present-but-unparseable fix).
968
+ var xSec = null;
925
969
  if (sigTags.x !== undefined) {
926
- var expSec = parseInt(sigTags.x, 10);
927
- if (isFinite(expSec) && expSec + clockSkewSec < nowSec) {
970
+ xSec = _parseDkimNumericDate(sigTags.x);
971
+ if (xSec === null) {
972
+ results.push({ d: d || null, s: s || null, alg: alg || null,
973
+ result: "permerror",
974
+ errors: ["DKIM-Signature x= present but unparseable (RFC 6376 §3.5 — NumericDate required)"] });
975
+ continue;
976
+ }
977
+ if (xSec + clockSkewSec < nowSec) {
928
978
  results.push({ d: d || null, s: s || null, alg: alg || null,
929
979
  result: "permerror",
930
- errors: ["DKIM-Signature x=" + expSec + " has expired (RFC 6376 §3.5)"] });
980
+ errors: ["DKIM-Signature x=" + xSec + " has expired (RFC 6376 §3.5)"] });
931
981
  continue;
932
982
  }
933
983
  }
934
984
  if (sigTags.t !== undefined) {
935
- var tSec = parseInt(sigTags.t, 10);
985
+ var tSec = _parseDkimNumericDate(sigTags.t);
986
+ if (tSec === null) {
987
+ results.push({ d: d || null, s: s || null, alg: alg || null,
988
+ result: "permerror",
989
+ errors: ["DKIM-Signature t= present but unparseable (RFC 6376 §3.5 — NumericDate required)"] });
990
+ continue;
991
+ }
936
992
  // Allow up to 24h future-skew; beyond that, refuse — neither
937
993
  // operator clock drift nor delivery latency explains a future-
938
994
  // dated signing time of more than a day.
939
- if (isFinite(tSec) && tSec - (24 * 60 * 60) > nowSec) { // allow:raw-time-literal — 24h future-date sanity ceiling
995
+ if (tSec - (24 * 60 * 60) > nowSec) { // allow:raw-time-literal — 24h future-date sanity ceiling
940
996
  results.push({ d: d || null, s: s || null, alg: alg || null,
941
997
  result: "permerror",
942
998
  errors: ["DKIM-Signature t=" + tSec + " is more than 24h in the future (RFC 6376 §3.5 sanity)"] });
943
999
  continue;
944
1000
  }
945
- if (sigTags.x !== undefined) {
946
- var xSec = parseInt(sigTags.x, 10);
947
- if (isFinite(xSec) && isFinite(tSec) && xSec < tSec) {
948
- results.push({ d: d || null, s: s || null, alg: alg || null,
949
- result: "permerror",
950
- errors: ["DKIM-Signature x= must be after t= (RFC 6376 §3.5)"] });
951
- continue;
952
- }
1001
+ if (xSec !== null && xSec < tSec) {
1002
+ results.push({ d: d || null, s: s || null, alg: alg || null,
1003
+ result: "permerror",
1004
+ errors: ["DKIM-Signature x= must be after t= (RFC 6376 §3.5)"] });
1005
+ continue;
953
1006
  }
954
1007
  }
955
1008
  if (!d || !s) {
@@ -825,6 +825,16 @@ function smtpTransport(opts) {
825
825
  _refuseCtlBytes("host", opts.host);
826
826
  _refuseCtlBytes("servername", opts.servername);
827
827
  var timeoutMs = opts.timeoutMs || C.TIME.seconds(15);
828
+ // Absolute transaction deadline — distinct from the per-socket IDLE
829
+ // timeout above. A slow-trickle MX that emits one byte just inside
830
+ // every idle window resets socket.setTimeout forever and never
831
+ // completes; this wall-clock bound fails the whole send regardless of
832
+ // trickle. Defaults well above timeoutMs so a normal multi-round-trip
833
+ // SMTP conversation (greeting → EHLO → STARTTLS → AUTH → MAIL/RCPT →
834
+ // DATA/BDAT) never trips it.
835
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.maxTransactionMs,
836
+ "smtp transport: opts.maxTransactionMs", MailError, "mail/smtp-misconfigured");
837
+ var maxTransactionMs = opts.maxTransactionMs || C.TIME.minutes(5);
828
838
  var tlsOpts = {
829
839
  rejectUnauthorized: rejectUnauthorized,
830
840
  minVersion: opts.minTlsVersion || "TLSv1.3",
@@ -875,6 +885,7 @@ function smtpTransport(opts) {
875
885
  useImplicitTLS: useImplicitTLS,
876
886
  ehloName: ehloName,
877
887
  timeoutMs: timeoutMs,
888
+ maxTransactionMs: maxTransactionMs,
878
889
  tlsOpts: tlsOpts,
879
890
  servername: servername,
880
891
  dkimSigner: opts.dkimSigner || null,
@@ -1043,6 +1054,7 @@ function _smtpSend(message, cfg) {
1043
1054
  var dataWireBytes = null; // Buffer view of dataMessage for BDAT slicing
1044
1055
  var useBdat = false; // Decided post-EHLO based on peerSupportsChunking + cfg.chunkingEnabled
1045
1056
  var bodyMode = "7BIT"; // "7BIT" / "8BITMIME" / "BINARYMIME"
1057
+ var txTimer = null; // Absolute transaction-deadline timer (cfg.maxTransactionMs)
1046
1058
 
1047
1059
  var fromAddr = _extractAddr(message.from);
1048
1060
  var toList = _toArray(message.to).map(_extractAddr);
@@ -1081,9 +1093,13 @@ function _smtpSend(message, cfg) {
1081
1093
  }
1082
1094
  }
1083
1095
 
1096
+ function clearTxTimer() {
1097
+ if (txTimer) { try { clearTimeout(txTimer); } catch (_e) { /* best-effort */ } txTimer = null; }
1098
+ }
1084
1099
  function fail(reason) {
1085
1100
  if (settled) return;
1086
1101
  settled = true;
1102
+ clearTxTimer();
1087
1103
  try { if (socket) socket.destroy(); } catch (_e) { /* socket may already be torn down */ }
1088
1104
  reject(new MailError("mail/smtp-failed",
1089
1105
  "SMTP send failed: " + reason, false));
@@ -1091,6 +1107,7 @@ function _smtpSend(message, cfg) {
1091
1107
  function done(ok, code) {
1092
1108
  if (settled) return;
1093
1109
  settled = true;
1110
+ clearTxTimer();
1094
1111
  try { socket.end(); } catch (_e) { /* socket may already be torn down */ }
1095
1112
  if (ok) resolve({ transport: "smtp", deliveredAt: Date.now(), code: code });
1096
1113
  else reject(new MailError("mail/smtp-rejected",
@@ -1149,6 +1166,16 @@ function _smtpSend(message, cfg) {
1149
1166
 
1150
1167
  function onData(data) {
1151
1168
  buffer += data;
1169
+ // Bound the framing accumulator. A hostile / broken MX that
1170
+ // streams bytes without ever sending CRLF would otherwise grow
1171
+ // `buffer` without limit and OOM the process. SMTP responses are
1172
+ // tiny per spec; the 256 KiB cap is generous headroom. Measure
1173
+ // bytes (not UTF-16 code units) so multibyte trickle can't slip a
1174
+ // larger payload past a char-length check.
1175
+ if (safeBuffer.byteLengthOf(buffer) > MAIL_RESPONSE_MAX_BYTES) {
1176
+ fail("response-too-large");
1177
+ return;
1178
+ }
1152
1179
  var lines = buffer.split("\r\n");
1153
1180
  buffer = lines.pop();
1154
1181
  for (var i = 0; i < lines.length; i++) {
@@ -1233,6 +1260,7 @@ function _smtpSend(message, cfg) {
1233
1260
  // would corrupt NUL-bearing octets in transit.
1234
1261
  if (requiresBinaryMime && !peerSupportsBinaryMime) {
1235
1262
  settled = true;
1263
+ clearTxTimer();
1236
1264
  try { socket.destroy(); } catch (_e) { /* socket may already be torn down */ }
1237
1265
  reject(new MailError("mail/binarymime-not-advertised",
1238
1266
  "message has 8-bit binary content but peer does not advertise BINARYMIME (RFC 3030 §3)",
@@ -1245,6 +1273,7 @@ function _smtpSend(message, cfg) {
1245
1273
  // cap"; -1 means "SIZE not advertised" (no precheck).
1246
1274
  if (cfg.respectPeerSize && peerSizeCap > 0 && messageWireSize > peerSizeCap) {
1247
1275
  settled = true;
1276
+ clearTxTimer();
1248
1277
  try { socket.destroy(); } catch (_e) { /* socket may already be torn down */ }
1249
1278
  reject(new MailError("mail/peer-size-exceeded",
1250
1279
  "message wire size " + messageWireSize + " bytes exceeds peer SIZE cap " +
@@ -1341,6 +1370,7 @@ function _smtpSend(message, cfg) {
1341
1370
  // the chunked-body failure mode.
1342
1371
  if (code !== 250) {
1343
1372
  settled = true;
1373
+ clearTxTimer();
1344
1374
  try { socket.destroy(); } catch (_e) { /* socket may already be torn down */ }
1345
1375
  reject(new MailError("mail/bdat-chunk-rejected",
1346
1376
  "BDAT chunk rejected (code " + code + ", offset " + bdatOffset + "/" +
@@ -1360,6 +1390,15 @@ function _smtpSend(message, cfg) {
1360
1390
  }
1361
1391
  }
1362
1392
 
1393
+ // Arm the absolute transaction deadline before opening the socket so
1394
+ // a peer that connects but then trickles (or never responds) is
1395
+ // bounded regardless of how it games the per-socket idle timer.
1396
+ // unref so a pending deadline never keeps the process alive on its own.
1397
+ txTimer = setTimeout(function () {
1398
+ fail("transaction-timeout");
1399
+ }, cfg.maxTransactionMs);
1400
+ if (txTimer && typeof txTimer.unref === "function") txTimer.unref();
1401
+
1363
1402
  try { connect(); }
1364
1403
  catch (e) { fail(e.message || String(e)); }
1365
1404
  });
@@ -1086,8 +1086,12 @@ function httpClientEncrypted(opts) {
1086
1086
  headers["Content-Type"] = "application/json";
1087
1087
 
1088
1088
  var passThrough = {};
1089
- var passable = ["allowedProtocols", "allowInternal", "idleTimeoutMs", "maxResponseBytes",
1090
- "agent", "errorClass"];
1089
+ // timeoutMs (overall wall-clock cap) and signal (caller cancellation) are
1090
+ // forwarded alongside idleTimeoutMs: without the wall-clock cap a peer that
1091
+ // trickles bytes within the idle window holds the encrypted call open
1092
+ // indefinitely (the plain request path already accepts both).
1093
+ var passable = ["allowedProtocols", "allowInternal", "timeoutMs", "idleTimeoutMs",
1094
+ "maxResponseBytes", "signal", "agent", "errorClass"];
1091
1095
  for (var i = 0; i < passable.length; i++) {
1092
1096
  if (reqOpts[passable[i]] !== undefined) passThrough[passable[i]] = reqOpts[passable[i]];
1093
1097
  }
@@ -112,6 +112,7 @@ var networkDns = require("./network-dns");
112
112
  var safeDns = require("./safe-dns");
113
113
  var safeUrl = require("./safe-url");
114
114
  var safeBuffer = require("./safe-buffer");
115
+ var safeAsync = require("./safe-async");
115
116
  var lazyRequire = require("./lazy-require");
116
117
 
117
118
  var audit = lazyRequire(function () { return require("./audit"); });
@@ -126,6 +127,14 @@ var DEFAULT_MAX_TTL_MS = C.TIME.hours(24);
126
127
  var DEFAULT_MIN_TTL_MS = C.TIME.seconds(60);
127
128
  var DEFAULT_STALE_WINDOW = C.TIME.hours(6);
128
129
  var DEFAULT_PROFILE = "strict";
130
+ // CWE-400. Per-query wall-clock deadline on the upstream transport
131
+ // lookup. A non-responsive / slow / stalling DoH endpoint (or a
132
+ // transport that sends headers then never ends the body) must not hold
133
+ // the await pending forever — every query() and every followCnames hop
134
+ // is bounded by this, and the default _wireLookup additionally tears
135
+ // the socket down (req.setTimeout) so the fd is released, not just the
136
+ // promise rejected. 10s matches a generous DoH round-trip ceiling.
137
+ var DEFAULT_TIMEOUT_MS = C.TIME.seconds(10);
129
138
  // CWE-400/770. Bound the cache so a hostile peer
130
139
  // that can drive query-name selection (e.g. inbound SMTP forwarding
131
140
  // DKIM `s=` / `d=` tag-controlled lookups) cannot inflate the Map to
@@ -168,6 +177,7 @@ var QTYPE_BY_NAME = Object.freeze({
168
177
  * maxTtlMs: number, // cap any TTL from upstream; default 24h
169
178
  * minTtlMs: number, // floor short-TTL records; default 60s
170
179
  * serveStale: number | false, // ms to retain expired entries; default 6h
180
+ * timeoutMs: number, // per-query upstream deadline; default 10s
171
181
  * transport: { lookup(name, qtype) → Promise<Buffer> }, // operator override
172
182
  * audit: b.audit namespace,
173
183
  *
@@ -187,7 +197,12 @@ function create(opts) {
187
197
  var minTtlMs = typeof opts.minTtlMs === "number" ? opts.minTtlMs : DEFAULT_MIN_TTL_MS;
188
198
  var serveStale = opts.serveStale === false ? 0 :
189
199
  typeof opts.serveStale === "number" ? opts.serveStale : DEFAULT_STALE_WINDOW;
190
- var transport = opts.transport || _defaultTransport();
200
+ var timeoutMs = typeof opts.timeoutMs === "number" ? opts.timeoutMs : DEFAULT_TIMEOUT_MS;
201
+ if (!isFinite(timeoutMs) || timeoutMs <= 0) {
202
+ throw new ResolverError("resolver/bad-input",
203
+ "create: timeoutMs must be a positive finite number");
204
+ }
205
+ var transport = opts.transport || _defaultTransport(timeoutMs);
191
206
  // Audit goes to the operator-supplied sink (opts.audit) when present, else the
192
207
  // framework chain — b.audit.namespaced no-ops if the sink has no safeEmit.
193
208
  var _baseAudit = audit().namespaced("network.dns.resolver", { sink: opts.audit });
@@ -276,10 +291,18 @@ function create(opts) {
276
291
  return _result(hit.parsed, hit.ttl, true, false, hit.validated);
277
292
  }
278
293
 
279
- // Cache miss / expired — fetch from upstream.
294
+ // Cache miss / expired — fetch from upstream, bounded by the
295
+ // per-query wall-clock deadline. withTimeout rejects the await so a
296
+ // non-responsive / stalling endpoint can't hold the request pending
297
+ // forever; the default _wireLookup also tears the socket down on its
298
+ // own req.setTimeout so the underlying fd is released.
280
299
  var wireResponse;
281
300
  try {
282
- wireResponse = await transport.lookup(name, qtype);
301
+ wireResponse = await safeAsync.withTimeout(
302
+ Promise.resolve(transport.lookup(name, qtype)),
303
+ timeoutMs,
304
+ { name: "dns-resolver:" + name + "/" + qtype }
305
+ );
283
306
  } catch (e) {
284
307
  // Upstream failure — serve stale if we have it within window.
285
308
  if (hit && serveStale > 0 && hit.staleUntil > now) {
@@ -425,13 +448,13 @@ function create(opts) {
425
448
  };
426
449
  }
427
450
 
428
- function _defaultTransport() {
451
+ function _defaultTransport(timeoutMs) {
429
452
  // Default transport — compose b.network.dns.useDnsOverHttps()'s
430
453
  // existing DoH path. We use the wire-format DoH endpoint directly
431
454
  // so the response arrives as raw bytes for safeDns parsing.
432
455
  return {
433
456
  lookup: function (name, qtype) {
434
- return _wireLookup(name, qtype);
457
+ return _wireLookup(name, qtype, timeoutMs);
435
458
  },
436
459
  };
437
460
  }
@@ -440,7 +463,9 @@ function _defaultTransport() {
440
463
  // existing DoH path. Returns the raw response bytes for safeDns to
441
464
  // parse. Distinct from the existing network-dns DoH path which returns
442
465
  // already-decoded address strings — we need the raw bytes here.
443
- async function _wireLookup(name, qtype) {
466
+ async function _wireLookup(name, qtype, timeoutMs) {
467
+ var ms = typeof timeoutMs === "number" && isFinite(timeoutMs) && timeoutMs > 0
468
+ ? timeoutMs : DEFAULT_TIMEOUT_MS;
444
469
  var url = networkDns._getDohUrlForTest ? networkDns._getDohUrlForTest() : "https://cloudflare-dns.com/dns-query";
445
470
  // Encode a wire-format query for the target qtype.
446
471
  var qbuf = _encodeWireQuery(name, qtype);
@@ -448,6 +473,18 @@ async function _wireLookup(name, qtype) {
448
473
  var getUrl = url + (url.indexOf("?") === -1 ? "?" : "&") + "dns=" + b64;
449
474
  var u = safeUrl.parse(getUrl, { allowedProtocols: safeUrl.ALLOW_HTTP_TLS });
450
475
  return new Promise(function (resolve, reject) {
476
+ var settled = false;
477
+ function _fail(err) {
478
+ if (settled) return;
479
+ settled = true;
480
+ try { req.destroy(); } catch (_e) { /* best-effort socket teardown */ }
481
+ reject(err);
482
+ }
483
+ function _done(buf) {
484
+ if (settled) return;
485
+ settled = true;
486
+ resolve(buf);
487
+ }
451
488
  // Raw DoH wire-format request — bypasses b.httpClient envelope
452
489
  // because we need the raw binary response bytes for safeDns to
453
490
  // parse (httpClient assumes JSON/text shapes).
@@ -470,18 +507,31 @@ async function _wireLookup(name, qtype) {
470
507
  res.on("data", function (c) { if (!pushFailed) { try { collector.push(c); } catch (e) { pushFailed = e; } } });
471
508
  res.on("end", function () {
472
509
  try {
473
- if (pushFailed) { reject(pushFailed); return; }
510
+ if (pushFailed) { _fail(pushFailed); return; }
474
511
  if (res.statusCode !== 200) { // HTTP 200 OK
475
- reject(new ResolverError("resolver/upstream-http",
512
+ _fail(new ResolverError("resolver/upstream-http",
476
513
  "DoH HTTP " + res.statusCode + " for " + name));
477
514
  return;
478
515
  }
479
- resolve(collector.result());
480
- } catch (e) { reject(e); }
516
+ _done(collector.result());
517
+ } catch (e) { _fail(e); }
481
518
  });
482
519
  });
520
+ // Wall-clock / idle deadline — a DoH endpoint that accepts the
521
+ // connection, sends 200 headers, then never ends the body would
522
+ // otherwise hold the socket (and its fd) open indefinitely. setTimeout
523
+ // arms on socket inactivity; on fire we tear the socket down AND reject
524
+ // so neither the promise nor the fd leaks (CWE-400 slowloris-style).
525
+ req.setTimeout(ms, function () {
526
+ _fail(new ResolverError("resolver/upstream-timeout",
527
+ "DoH request to " + u.hostname + " for " + name + " exceeded " + ms + "ms"));
528
+ });
529
+ req.on("timeout", function () {
530
+ _fail(new ResolverError("resolver/upstream-timeout",
531
+ "DoH request to " + u.hostname + " for " + name + " exceeded " + ms + "ms"));
532
+ });
483
533
  req.on("error", function (e) {
484
- reject(new ResolverError("resolver/upstream-failed",
534
+ _fail(new ResolverError("resolver/upstream-failed",
485
535
  "DoH request failed: " + e.message));
486
536
  });
487
537
  req.end();
@@ -28,11 +28,18 @@ var HEX_RADIX = C.BYTES.bytes(16); // parseInt / toString radix-16
28
28
  var observability = lazyRequire(function () { return require("./observability"); });
29
29
  var safeEnv = require("./parsers/safe-env");
30
30
 
31
+ // Default wall-clock deadline for every lookup (resolve / DoH / DoT /
32
+ // system). Without a non-zero default a header-then-stall or
33
+ // slow-trickle upstream hangs the request forever (CWE-400). 10s
34
+ // matches the resolver's default. Operators override via
35
+ // setLookupTimeoutMs(); 0 disables the deadline (operator opt-out).
36
+ var DEFAULT_LOOKUP_TIMEOUT_MS = C.TIME.seconds(10);
37
+
31
38
  var STATE = {
32
39
  servers: null,
33
40
  resultOrder: null,
34
41
  family: 0,
35
- lookupTimeoutMs: 0,
42
+ lookupTimeoutMs: DEFAULT_LOOKUP_TIMEOUT_MS,
36
43
  cacheTtlMs: 0,
37
44
  cacheNegativeTtlMs: 0,
38
45
  doh: null,
@@ -285,6 +292,19 @@ function _withTimeout(promise, ms, host) {
285
292
  });
286
293
  }
287
294
 
295
+ // Arm a wall-clock deadline on a raw https.request so a stalled upstream
296
+ // (headers-then-stall, slow-trickle body) tears the socket down instead
297
+ // of leaking the fd until the process exits. The promise-level
298
+ // _withTimeout rejects the caller, but only this destroys the underlying
299
+ // connection. No-op when the operator has disabled the deadline (ms<=0).
300
+ function _armRequestTimeout(req, ms, host, reject) {
301
+ if (ms <= 0) return;
302
+ req.setTimeout(ms, function () {
303
+ try { req.destroy(); } catch (_e) { /* best-effort socket teardown */ }
304
+ reject(new DnsError("dns/lookup-timeout", "dns lookup of '" + host + "' exceeded " + ms + "ms"));
305
+ });
306
+ }
307
+
288
308
  function _encodeDnsQuery(host, qtype) {
289
309
  var parts = host.split(".").filter(Boolean);
290
310
  var nameLen = 1;
@@ -450,6 +470,7 @@ async function _dohLookup(host, family) {
450
470
  });
451
471
  });
452
472
  req.on("error", function (e) { reject(new DnsError("dns/doh-failed", "DoH request failed: " + e.message)); });
473
+ _armRequestTimeout(req, STATE.lookupTimeoutMs, host, reject);
453
474
  if (usePost) req.write(enc.buf);
454
475
  req.end();
455
476
  });
@@ -511,6 +532,7 @@ async function _dohLookupSecure(host, family) {
511
532
  });
512
533
  });
513
534
  req.on("error", function (e) { reject(new DnsError("dns/doh-failed", "DoH request failed: " + e.message)); });
535
+ _armRequestTimeout(req, STATE.lookupTimeoutMs, host, reject);
514
536
  if (usePost) req.write(enc.buf);
515
537
  req.end();
516
538
  });
@@ -591,6 +613,17 @@ function _dotConnect() {
591
613
  // when idle — _dotLookup toggles this around its query. Calling
592
614
  // unref() unconditionally here let node exit during a normal lookup
593
615
  // when no other I/O kept the event loop alive.
616
+ //
617
+ // Wall-clock teardown: a stalled handshake or trickle-response upstream
618
+ // would otherwise leak the socket until the process exits (CWE-400).
619
+ // On inactivity past the deadline destroy the socket — the pool's
620
+ // error/close handlers evict it and the next lookup rebuilds. No-op
621
+ // when the operator has disabled the deadline (lookupTimeoutMs<=0).
622
+ if (STATE.lookupTimeoutMs > 0) {
623
+ sock.setTimeout(STATE.lookupTimeoutMs, function () {
624
+ try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
625
+ });
626
+ }
594
627
  return sock;
595
628
  }
596
629
 
@@ -865,6 +898,7 @@ async function _dohRawQuery(host, qtype) {
865
898
  });
866
899
  });
867
900
  req.on("error", function (e) { reject(new DnsError("dns/doh-failed", "DoH request failed: " + e.message)); });
901
+ _armRequestTimeout(req, STATE.lookupTimeoutMs, host, reject);
868
902
  if (usePost) req.write(enc.buf);
869
903
  req.end();
870
904
  });
@@ -979,6 +1013,17 @@ async function _systemRawQuery(host, qtype) {
979
1013
  try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
980
1014
  if (err) reject(err); else resolve(val);
981
1015
  }
1016
+ // Wall-clock teardown: a server that accepts the TCP connection but
1017
+ // never replies (or trickles the length-prefix) would otherwise hang
1018
+ // the query forever (CWE-400). On inactivity past the deadline
1019
+ // settle()'s destroy() tears the socket down. No-op when the operator
1020
+ // has disabled the deadline (lookupTimeoutMs<=0).
1021
+ if (STATE.lookupTimeoutMs > 0) {
1022
+ sock.setTimeout(STATE.lookupTimeoutMs, function () {
1023
+ settle(new DnsError("dns/lookup-timeout",
1024
+ "system DNS TCP query of '" + host + "' exceeded " + STATE.lookupTimeoutMs + "ms"));
1025
+ });
1026
+ }
982
1027
  sock.on("connect", function () {
983
1028
  var lenBuf = Buffer.alloc(2);
984
1029
  lenBuf.writeUInt16BE(enc.buf.length, 0);
@@ -1684,7 +1729,7 @@ function nodeLookup(host, options, callback) {
1684
1729
  function _stateForTest() { return STATE; }
1685
1730
  function _resetForTest() {
1686
1731
  STATE.servers = null; STATE.resultOrder = null; STATE.family = 0;
1687
- STATE.lookupTimeoutMs = 0; STATE.cacheTtlMs = 0; STATE.cacheNegativeTtlMs = 0;
1732
+ STATE.lookupTimeoutMs = DEFAULT_LOOKUP_TIMEOUT_MS; STATE.cacheTtlMs = 0; STATE.cacheNegativeTtlMs = 0;
1688
1733
  STATE.doh = null; STATE.dot = null; STATE.systemResolver = false;
1689
1734
  _designatedResolvers = null;
1690
1735
  _clearCache();
@@ -7,12 +7,20 @@ var nodeCrypto = require("node:crypto");
7
7
  var C = require("./constants");
8
8
  var { timingSafeEqual } = require("./crypto");
9
9
  var validateOpts = require("./validate-opts");
10
+ var safeBuffer = require("./safe-buffer");
10
11
  var { defineClass } = require("./framework-error");
11
12
 
12
13
  var NtsError = defineClass("NtsError", { alwaysPermanent: false });
13
14
 
14
15
  var NTS_KE_DEFAULT_PORT = 4460;
15
16
  var NTPV4_DEFAULT_PORT = 123;
17
+ // Upper bound on accumulated NTS-KE handshake bytes before a REC_END
18
+ // record terminates the exchange. A conformant NTS-KE response is a few
19
+ // hundred bytes (RFC 8915 §4); 64 KiB is generous. Without this ceiling
20
+ // a malicious or buggy server that streams non-END records fast enough
21
+ // OOMs the process before the wall-clock timer fires (it bounds time,
22
+ // not memory). Mirrors the ws-client handshake header cap.
23
+ var NTS_KE_HANDSHAKE_MAX_BYTES = C.BYTES.kib(64);
16
24
  // RFC 5905 §6 — seconds between 1900-01-01T00:00Z (NTP epoch) and
17
25
  // 1970-01-01T00:00Z (Unix epoch). Protocol-fixed (not a tunable).
18
26
  var NTP_TO_UNIX_OFFSET_SECONDS = 2208988800;
@@ -284,6 +292,14 @@ function performKeHandshake(opts) {
284
292
  var warnings = [];
285
293
  sock.on("data", function (chunk) {
286
294
  got = Buffer.concat([got, chunk]);
295
+ if (safeBuffer.byteLengthOf(got) > NTS_KE_HANDSHAKE_MAX_BYTES) {
296
+ clearTimeout(timer);
297
+ try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
298
+ done(new NtsError("nts/ke-too-large",
299
+ "NTS-KE handshake exceeded " + NTS_KE_HANDSHAKE_MAX_BYTES +
300
+ " bytes before a REC_END record"));
301
+ return;
302
+ }
287
303
  try {
288
304
  var records = _decodeRecords(got);
289
305
  var endRec = records.find(function (r) { return r.type === REC_END; });
@@ -7,6 +7,7 @@ var nodeTls = require("node:tls");
7
7
 
8
8
  var C = require("./constants");
9
9
  var lazyRequire = require("./lazy-require");
10
+ var safeBuffer = require("./safe-buffer");
10
11
  var safeUrl = require("./safe-url");
11
12
  var validateOpts = require("./validate-opts");
12
13
  var { defineClass } = require("./framework-error");
@@ -19,6 +20,23 @@ var IPV4_PREFIX_MAX_BITS = C.BYTES.bytes(32); // RFC 791 §3.1 IPv4 address bi
19
20
  var DEFAULT_HTTPS_PORT = 443; // RFC 9110 §4.2.2
20
21
  var DEFAULT_HTTP_PORT = C.BYTES.bytes(80); // RFC 9110 §4.2.1
21
22
 
23
+ // Bound the CONNECT-reply framing buffer (mirrors ws-client's handshake
24
+ // cap). A proxy that streams bytes without ever sending the CRLFCRLF
25
+ // header terminator would otherwise grow `buf` without limit and OOM the
26
+ // process; CONNECT replies are tiny, 64 KiB is generous headroom.
27
+ var TUNNEL_HEADER_MAX_BYTES = C.BYTES.kib(64);
28
+ // Wall-clock bound on the proxy connect + CONNECT handshake. Without it a
29
+ // proxy that accepts the socket but never replies (or trickles forever)
30
+ // hangs the tunnel indefinitely.
31
+ var TUNNEL_CONNECT_TIMEOUT_MS = C.TIME.seconds(30);
32
+ // Test-only override of the connect deadline so a unit test can prove the
33
+ // absolute-timeout behavior without a 30s wall-clock wait. null = use the
34
+ // real default. Cleared by _resetForTest.
35
+ var _testConnectTimeoutMs = null;
36
+ function _connectTimeoutMs() {
37
+ return typeof _testConnectTimeoutMs === "number" ? _testConnectTimeoutMs : TUNNEL_CONNECT_TIMEOUT_MS;
38
+ }
39
+
22
40
  var observability = lazyRequire(function () { return require("./observability"); });
23
41
  // Lazy so pqc-agent's TLS/audit graph isn't pulled into every process that
24
42
  // imports network-proxy but never proxies an https upstream. Used only to audit
@@ -169,7 +187,27 @@ function _connectThroughTunnel(proxyUrl, targetHost, targetPort, callback) {
169
187
  })
170
188
  : net.connect({ host: proxyUrl.hostname, port: proxyPort });
171
189
  var settled = false;
172
- function done(err, sock) { if (settled) return; settled = true; callback(err, sock); }
190
+ var connectDeadline = null;
191
+ function done(err, sock) {
192
+ if (settled) return;
193
+ settled = true;
194
+ if (connectDeadline) { try { clearTimeout(connectDeadline); } catch (_e) { /* best-effort */ } connectDeadline = null; }
195
+ callback(err, sock);
196
+ }
197
+ // ABSOLUTE wall-clock deadline on the proxy connect + CONNECT handshake. A
198
+ // proxy that accepts the socket but never sends the CRLFCRLF terminator —
199
+ // or trickles partial bytes just inside every idle window — must not hang
200
+ // the tunnel forever. socket.setTimeout is an IDLE timer that such a trickle
201
+ // resets indefinitely, so a fixed setTimeout (cleared in done()) enforces a
202
+ // real total-time bound regardless of how the bytes arrive.
203
+ var connectTimeoutMs = _connectTimeoutMs();
204
+ connectDeadline = setTimeout(function () {
205
+ done(new ProxyError("proxy/connect-timeout",
206
+ "proxy CONNECT to " + targetHost + ":" + targetPort + " timed out after " +
207
+ connectTimeoutMs + "ms"));
208
+ try { proxySocket.destroy(); } catch (_e) { /* best-effort socket teardown */ }
209
+ }, connectTimeoutMs);
210
+ if (connectDeadline && typeof connectDeadline.unref === "function") connectDeadline.unref();
173
211
  proxySocket.on("error", function (e) { done(e); });
174
212
  proxySocket.on(proxyUrl.protocol === "https:" ? "secureConnect" : "connect", function () {
175
213
  if (proxyUrl.protocol === "https:") {
@@ -191,7 +229,18 @@ function _connectThroughTunnel(proxyUrl, targetHost, targetPort, callback) {
191
229
  function onData(chunk) {
192
230
  buf = Buffer.concat([buf, chunk]);
193
231
  var idx = buf.indexOf("\r\n\r\n");
194
- if (idx === -1) return;
232
+ if (idx === -1) {
233
+ // No header terminator yet — bound the accumulator so a proxy
234
+ // that trickles non-CRLFCRLF bytes forever can't OOM the process.
235
+ if (safeBuffer.byteLengthOf(buf) > TUNNEL_HEADER_MAX_BYTES) {
236
+ proxySocket.removeListener("data", onData);
237
+ done(new ProxyError("proxy/connect-headers-too-large",
238
+ "proxy CONNECT reply exceeded " + TUNNEL_HEADER_MAX_BYTES +
239
+ " bytes before CRLFCRLF"));
240
+ try { proxySocket.destroy(); } catch (_e) { /* best-effort socket teardown */ }
241
+ }
242
+ return;
243
+ }
195
244
  proxySocket.removeListener("data", onData);
196
245
  var head = buf.slice(0, idx).toString("ascii");
197
246
  var status = head.split("\r\n")[0] || "";
@@ -271,8 +320,11 @@ function snapshot() {
271
320
  function _resetForTest() {
272
321
  STATE.http = null; STATE.https = null; STATE.noProxy = []; STATE.auth = null;
273
322
  STATE.agentCache.clear();
323
+ _testConnectTimeoutMs = null;
274
324
  }
275
325
 
326
+ function _setConnectTimeoutForTest(ms) { _testConnectTimeoutMs = ms; }
327
+
276
328
  module.exports = {
277
329
  set: set,
278
330
  fromEnv: fromEnv,
@@ -281,4 +333,5 @@ module.exports = {
281
333
  snapshot: snapshot,
282
334
  ProxyError: ProxyError,
283
335
  _resetForTest: _resetForTest,
336
+ _setConnectTimeoutForTest: _setConnectTimeoutForTest,
284
337
  };
@@ -233,6 +233,7 @@ function create(config) {
233
233
  url: url,
234
234
  headers: headers,
235
235
  body: body,
236
+ timeoutMs: timeoutMs,
236
237
  idleTimeoutMs: timeoutMs,
237
238
  allowedProtocols: allowedProtocols,
238
239
  errorClass: ObjectStoreError,
@@ -182,6 +182,7 @@ function create(config) {
182
182
  url: url,
183
183
  headers: headers,
184
184
  body: body,
185
+ timeoutMs: timeoutMs,
185
186
  idleTimeoutMs: timeoutMs,
186
187
  allowedProtocols: allowedProtocols,
187
188
  errorClass: ObjectStoreError,