@blamejs/core 0.15.13 → 0.15.15

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 (208) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +38 -6
  4. package/lib/agent-event-bus.js +13 -0
  5. package/lib/agent-idempotency.js +5 -1
  6. package/lib/agent-snapshot.js +32 -2
  7. package/lib/ai-aedt-bias-audit.js +2 -1
  8. package/lib/ai-content-detect.js +1 -3
  9. package/lib/ai-frontier-protocol.js +1 -1
  10. package/lib/ai-model-manifest.js +1 -1
  11. package/lib/ai-output.js +16 -7
  12. package/lib/api-snapshot.js +4 -1
  13. package/lib/app-shutdown.js +7 -1
  14. package/lib/archive-gz.js +9 -0
  15. package/lib/archive-read.js +9 -7
  16. package/lib/archive-tar-read.js +51 -8
  17. package/lib/archive.js +4 -2
  18. package/lib/asn1-der.js +70 -22
  19. package/lib/atomic-file.js +204 -2
  20. package/lib/audit-chain.js +54 -5
  21. package/lib/audit-daily-review.js +12 -2
  22. package/lib/audit-sign.js +2 -1
  23. package/lib/audit-tools.js +108 -23
  24. package/lib/audit.js +7 -2
  25. package/lib/auth/access-lock.js +2 -2
  26. package/lib/auth/bot-challenge.js +1 -1
  27. package/lib/auth/ciba.js +71 -8
  28. package/lib/auth/dpop.js +15 -1
  29. package/lib/auth/fido-mds3.js +30 -13
  30. package/lib/auth/jwt.js +21 -5
  31. package/lib/auth/lockout.js +18 -2
  32. package/lib/auth/oauth.js +8 -2
  33. package/lib/auth/passkey.js +1 -1
  34. package/lib/auth/password.js +1 -1
  35. package/lib/auth/saml.js +42 -12
  36. package/lib/auth/sd-jwt-vc.js +24 -4
  37. package/lib/auth/status-list.js +14 -2
  38. package/lib/auth/step-up.js +9 -1
  39. package/lib/auth-bot-challenge.js +21 -2
  40. package/lib/backup/bundle.js +7 -2
  41. package/lib/backup/crypto.js +23 -8
  42. package/lib/backup/index.js +41 -20
  43. package/lib/backup/manifest.js +7 -1
  44. package/lib/break-glass.js +41 -22
  45. package/lib/cbor.js +34 -11
  46. package/lib/cdn-cache-control.js +7 -3
  47. package/lib/cert.js +5 -3
  48. package/lib/cli.js +5 -1
  49. package/lib/cloud-events.js +3 -2
  50. package/lib/cluster-storage.js +7 -3
  51. package/lib/codepoint-class.js +17 -0
  52. package/lib/compliance-eaa.js +1 -1
  53. package/lib/compliance-sanctions.js +9 -7
  54. package/lib/compliance.js +1 -1
  55. package/lib/config-drift.js +22 -8
  56. package/lib/content-credentials.js +11 -8
  57. package/lib/content-digest.js +11 -4
  58. package/lib/cookies.js +10 -2
  59. package/lib/cose.js +20 -0
  60. package/lib/crdt.js +2 -1
  61. package/lib/crypto-field.js +48 -24
  62. package/lib/crypto.js +18 -4
  63. package/lib/csp.js +13 -0
  64. package/lib/daemon.js +4 -1
  65. package/lib/data-act.js +27 -4
  66. package/lib/db-file-lifecycle.js +14 -6
  67. package/lib/db-query.js +102 -11
  68. package/lib/db.js +32 -15
  69. package/lib/dora.js +43 -13
  70. package/lib/dr-runbook.js +1 -1
  71. package/lib/dsa.js +2 -1
  72. package/lib/dsr.js +22 -8
  73. package/lib/early-hints.js +19 -0
  74. package/lib/eat.js +5 -1
  75. package/lib/external-db.js +60 -4
  76. package/lib/fda-21cfr11.js +30 -5
  77. package/lib/flag-providers.js +6 -2
  78. package/lib/forms.js +1 -1
  79. package/lib/gate-contract.js +46 -5
  80. package/lib/gdpr-ropa.js +18 -9
  81. package/lib/graphql-federation.js +17 -4
  82. package/lib/guard-all.js +2 -2
  83. package/lib/guard-dsn.js +1 -1
  84. package/lib/guard-envelope.js +1 -1
  85. package/lib/guard-html.js +9 -11
  86. package/lib/guard-imap-command.js +1 -1
  87. package/lib/guard-jmap.js +1 -1
  88. package/lib/guard-json.js +14 -6
  89. package/lib/guard-mail-move.js +1 -1
  90. package/lib/guard-managesieve-command.js +1 -1
  91. package/lib/guard-pop3-command.js +1 -1
  92. package/lib/guard-smtp-command.js +1 -1
  93. package/lib/guard-svg.js +8 -9
  94. package/lib/html-balance.js +7 -3
  95. package/lib/http-client-cookie-jar.js +33 -12
  96. package/lib/http-client.js +225 -53
  97. package/lib/iab-tcf.js +3 -2
  98. package/lib/importmap-integrity.js +41 -1
  99. package/lib/incident-report.js +9 -6
  100. package/lib/json-patch.js +1 -1
  101. package/lib/json-path.js +24 -3
  102. package/lib/jtd.js +2 -2
  103. package/lib/legal-hold.js +24 -8
  104. package/lib/log.js +2 -2
  105. package/lib/mail-agent.js +2 -2
  106. package/lib/mail-arf.js +1 -1
  107. package/lib/mail-auth.js +27 -4
  108. package/lib/mail-bimi.js +16 -16
  109. package/lib/mail-bounce.js +3 -3
  110. package/lib/mail-crypto-smime.js +71 -6
  111. package/lib/mail-deploy.js +9 -5
  112. package/lib/mail-dkim.js +20 -7
  113. package/lib/mail-greylist.js +2 -4
  114. package/lib/mail-helo.js +2 -4
  115. package/lib/mail-journal.js +11 -8
  116. package/lib/mail-mdn.js +8 -4
  117. package/lib/mail-rbl.js +2 -4
  118. package/lib/mail-scan.js +3 -5
  119. package/lib/mail-server-jmap.js +4 -1
  120. package/lib/mail-server-registry.js +1 -1
  121. package/lib/mail-server-tls.js +9 -2
  122. package/lib/mail-spam-score.js +2 -4
  123. package/lib/mail-store-fts.js +1 -1
  124. package/lib/mail.js +22 -2
  125. package/lib/markup-tokenizer.js +24 -0
  126. package/lib/mcp.js +6 -4
  127. package/lib/mdoc.js +26 -3
  128. package/lib/metrics.js +14 -3
  129. package/lib/middleware/api-encrypt.js +2 -2
  130. package/lib/middleware/body-parser.js +10 -4
  131. package/lib/middleware/bot-guard.js +26 -18
  132. package/lib/middleware/clear-site-data.js +5 -1
  133. package/lib/middleware/compose-pipeline.js +39 -5
  134. package/lib/middleware/compression.js +9 -0
  135. package/lib/middleware/cors.js +32 -23
  136. package/lib/middleware/csrf-protect.js +60 -21
  137. package/lib/middleware/daily-byte-quota.js +6 -4
  138. package/lib/middleware/fetch-metadata.js +28 -4
  139. package/lib/middleware/network-allowlist.js +61 -30
  140. package/lib/middleware/rate-limit.js +25 -16
  141. package/lib/middleware/scim-server.js +2 -1
  142. package/lib/middleware/security-headers.js +24 -6
  143. package/lib/middleware/speculation-rules.js +6 -3
  144. package/lib/middleware/tus-upload.js +2 -2
  145. package/lib/money.js +1 -1
  146. package/lib/mtls-ca.js +10 -6
  147. package/lib/network-dns-resolver.js +1 -1
  148. package/lib/network-dns.js +9 -2
  149. package/lib/network-dnssec.js +2 -1
  150. package/lib/network-smtp-policy.js +23 -5
  151. package/lib/network-tls.js +27 -5
  152. package/lib/network-tsig.js +2 -2
  153. package/lib/nis2-report.js +1 -1
  154. package/lib/nist-crosswalk.js +1 -1
  155. package/lib/ntp-check.js +28 -0
  156. package/lib/numeric-bounds.js +9 -0
  157. package/lib/object-store/azure-blob.js +1 -2
  158. package/lib/object-store/gcs-bucket-ops.js +4 -2
  159. package/lib/object-store/gcs.js +6 -4
  160. package/lib/object-store/http-put.js +1 -2
  161. package/lib/object-store/http-request.js +30 -1
  162. package/lib/object-store/local.js +37 -17
  163. package/lib/object-store/sigv4.js +1 -2
  164. package/lib/observability-otlp-exporter.js +20 -4
  165. package/lib/outbox.js +11 -4
  166. package/lib/parsers/safe-xml.js +1 -1
  167. package/lib/parsers/safe-yaml.js +21 -3
  168. package/lib/pipl-cn.js +11 -8
  169. package/lib/queue-local.js +10 -3
  170. package/lib/redact.js +7 -3
  171. package/lib/request-helpers.js +347 -36
  172. package/lib/resource-access-lock.js +3 -3
  173. package/lib/restore-bundle.js +46 -18
  174. package/lib/restore-rollback.js +10 -4
  175. package/lib/restore.js +19 -0
  176. package/lib/retention.js +20 -4
  177. package/lib/router.js +17 -4
  178. package/lib/safe-ical.js +2 -2
  179. package/lib/safe-icap.js +1 -1
  180. package/lib/safe-json.js +70 -0
  181. package/lib/safe-sieve.js +1 -1
  182. package/lib/safe-vcard.js +1 -1
  183. package/lib/sandbox-worker.js +6 -0
  184. package/lib/sandbox.js +1 -1
  185. package/lib/scheduler.js +17 -1
  186. package/lib/self-update-standalone-verifier.js +16 -0
  187. package/lib/session.js +62 -120
  188. package/lib/sql.js +25 -3
  189. package/lib/static.js +65 -13
  190. package/lib/template.js +7 -5
  191. package/lib/tenant-quota.js +52 -19
  192. package/lib/tsa.js +5 -2
  193. package/lib/vault/index.js +5 -0
  194. package/lib/vault/passphrase-ops.js +22 -26
  195. package/lib/vault/passphrase-source.js +8 -3
  196. package/lib/vault/rotate.js +13 -18
  197. package/lib/vault/seal-pem-file.js +4 -1
  198. package/lib/vc.js +1 -1
  199. package/lib/vendor/MANIFEST.json +10 -10
  200. package/lib/vendor/public-suffix-list.dat +23 -10
  201. package/lib/vendor/public-suffix-list.data.js +5498 -5494
  202. package/lib/webhook.js +16 -1
  203. package/lib/websocket.js +1 -1
  204. package/lib/worm.js +1 -1
  205. package/lib/ws-client.js +83 -46
  206. package/lib/x509-chain.js +91 -0
  207. package/package.json +1 -1
  208. package/sbom.cdx.json +6 -6
@@ -48,6 +48,7 @@
48
48
  var nodeFs = require("node:fs");
49
49
  var nodePath = require("node:path");
50
50
  var atomicFile = require("./atomic-file");
51
+ var C = require("./constants");
51
52
  var backupCrypto = require("./backup/crypto");
52
53
  var backupManifest = require("./backup/manifest");
53
54
  var validateOpts = require("./validate-opts");
@@ -146,13 +147,21 @@ async function extract(opts) {
146
147
  // 1. Read + parse + validate manifest
147
148
  _emit(progress, { phase: "read_manifest" });
148
149
  var manifestPath = nodePath.join(bundleDir, "manifest.json");
149
- if (!nodeFs.existsSync(manifestPath)) {
150
- throw new RestoreBundleError("restore-bundle/missing-manifest",
151
- "extract: bundleDir has no manifest.json bundle is incomplete or not a blamejs backup");
152
- }
150
+ // Capped fd-bound read OUTSIDE the parse try/catch, so a missing / oversized
151
+ // bundle manifest surfaces the precise restore-bundle code (not a generic
152
+ // parse error). A backup manifest is small JSON; 4 MiB bounds a hostile one
153
+ // before backupManifest.parse materializes it.
154
+ var manifestRaw = atomicFile.fdSafeReadSync(manifestPath, {
155
+ maxBytes: C.BYTES.mib(4), encoding: "utf8",
156
+ errorFor: function (kind, detail) {
157
+ if (kind === "enoent") return new RestoreBundleError("restore-bundle/missing-manifest", "extract: bundleDir has no manifest.json — bundle is incomplete or not a blamejs backup");
158
+ if (kind === "too-large") return new RestoreBundleError("restore-bundle/bad-manifest", "extract: manifest.json exceeds " + detail.max + " bytes");
159
+ return new RestoreBundleError("restore-bundle/bad-manifest", "extract: manifest unreadable: " + kind);
160
+ },
161
+ });
153
162
  var manifest;
154
163
  try {
155
- manifest = backupManifest.parse(nodeFs.readFileSync(manifestPath, "utf8"));
164
+ manifest = backupManifest.parse(manifestRaw);
156
165
  } catch (e) {
157
166
  if (e && e.isBackupManifestError) throw e;
158
167
  throw new RestoreBundleError("restore-bundle/bad-manifest",
@@ -216,12 +225,20 @@ async function extract(opts) {
216
225
  }
217
226
 
218
227
  var blobPath = nodePath.join(bundleDir, entry.encryptedPath);
219
- if (!nodeFs.existsSync(blobPath)) {
220
- throw new RestoreBundleError("restore-bundle/missing-blob",
221
- "extract: manifest references '" + entry.encryptedPath +
222
- "' but the bundle has no such file");
223
- }
224
- var blob = nodeFs.readFileSync(blobPath);
228
+ // Cap the read to the manifest's declared encryptedSize so an oversize-on-
229
+ // disk blob is refused BEFORE it is read into memory (was: read fully, then
230
+ // compare an OOM window for a huge swapped blob). A valid blob is exactly
231
+ // encryptedSize; the post-read compare still catches the under-size case.
232
+ var blobCap = (typeof entry.encryptedSize === "number" && entry.encryptedSize > 0)
233
+ ? entry.encryptedSize : C.BYTES.gib(8);
234
+ var blob = atomicFile.fdSafeReadSync(blobPath, {
235
+ maxBytes: blobCap,
236
+ errorFor: function (kind, detail) {
237
+ if (kind === "enoent") return new RestoreBundleError("restore-bundle/missing-blob", "extract: manifest references '" + entry.encryptedPath + "' but the bundle has no such file");
238
+ if (kind === "too-large") return new RestoreBundleError("restore-bundle/size-mismatch", "extract: blob '" + entry.encryptedPath + "' has size " + detail.size + " but manifest expected " + entry.encryptedSize);
239
+ return new RestoreBundleError("restore-bundle/missing-blob", "extract: blob '" + entry.encryptedPath + "' unreadable: " + kind);
240
+ },
241
+ });
225
242
  if (blob.length !== entry.encryptedSize) {
226
243
  throw new RestoreBundleError("restore-bundle/size-mismatch",
227
244
  "extract: blob '" + entry.encryptedPath + "' has size " + blob.length +
@@ -235,12 +252,18 @@ async function extract(opts) {
235
252
 
236
253
  var plaintext;
237
254
  try {
238
- plaintext = await backupCrypto.decryptWithPassphrase(blob, passphrase, entry.salt);
255
+ // Bundles written with manifest.aadBound sealed each blob with its
256
+ // relativePath as AEAD associated data — pass the SAME path so a blob
257
+ // remapped to a different manifest entry fails the tag here (the
258
+ // blob-remap / restore-corruption defense). Legacy bundles without
259
+ // the flag decrypt with no AAD (backward compatible).
260
+ var blobAad = manifest.aadBound === true ? Buffer.from(entry.relativePath, "utf8") : undefined;
261
+ plaintext = await backupCrypto.decryptWithPassphrase(blob, passphrase, entry.salt, blobAad);
239
262
  } catch (e) {
240
263
  if (e && e.isBackupCryptoError && e.code === "backup-crypto/decrypt-failed") {
241
264
  throw new RestoreBundleError("restore-bundle/decrypt-failed",
242
265
  "extract: blob '" + entry.encryptedPath + "' did not decrypt — " +
243
- "passphrase rejected or ciphertext tampered");
266
+ "passphrase rejected, ciphertext tampered, or blob remapped to a different path");
244
267
  }
245
268
  throw e;
246
269
  }
@@ -326,11 +349,16 @@ function inspect(opts) {
326
349
  "inspect: opts.bundleDir is required and must exist");
327
350
  }
328
351
  var manifestPath = nodePath.join(opts.bundleDir, "manifest.json");
329
- if (!nodeFs.existsSync(manifestPath)) {
330
- throw new RestoreBundleError("restore-bundle/missing-manifest",
331
- "inspect: bundleDir has no manifest.json");
332
- }
333
- return backupManifest.parse(nodeFs.readFileSync(manifestPath, "utf8"));
352
+ // Capped fd-bound read (no-passphrase preview path, reachable with only read
353
+ // access to the bundle): bound a hostile manifest before parse.
354
+ return backupManifest.parse(atomicFile.fdSafeReadSync(manifestPath, {
355
+ maxBytes: C.BYTES.mib(4), encoding: "utf8",
356
+ errorFor: function (kind) {
357
+ if (kind === "enoent") return new RestoreBundleError("restore-bundle/missing-manifest", "inspect: bundleDir has no manifest.json");
358
+ if (kind === "too-large") return new RestoreBundleError("restore-bundle/bad-manifest", "inspect: manifest.json too large");
359
+ return new RestoreBundleError("restore-bundle/missing-manifest", "inspect: manifest unreadable: " + kind);
360
+ },
361
+ }));
334
362
  }
335
363
 
336
364
  module.exports = {
@@ -283,10 +283,16 @@ function list(opts) {
283
283
  var p = nodePath.join(rollbackRoot, name);
284
284
  var markerPath = p + ".marker.json";
285
285
  var marker = null;
286
- if (nodeFs.existsSync(markerPath)) {
287
- try { marker = safeJson.parse(nodeFs.readFileSync(markerPath, "utf8"), { maxBytes: C.BYTES.kib(64) }); }
288
- catch (_e) { marker = null; }
289
- }
286
+ // Capped fd-bound read (no existsSync check-then-read window): the fs read is
287
+ // now bounded to 64 KiB too, so a tampered multi-GB marker.json is refused
288
+ // BEFORE allocation (the parse-only cap let readFileSync slurp the whole file
289
+ // first). refuseSymlink: the marker lives under the operator's rollbackRoot,
290
+ // never a secret-mount. Any failure → marker:null, the best-effort behavior.
291
+ try {
292
+ marker = safeJson.parse(
293
+ atomicFile.fdSafeReadSync(markerPath, { maxBytes: C.BYTES.kib(64), encoding: "utf8", refuseSymlink: true }),
294
+ { maxBytes: C.BYTES.kib(64) });
295
+ } catch (_e) { marker = null; }
290
296
  var stat;
291
297
  try { stat = nodeFs.statSync(p); } catch (_e) { continue; }
292
298
  out.push({
package/lib/restore.js CHANGED
@@ -83,6 +83,7 @@ function create(opts) {
83
83
  validateOpts(opts, [
84
84
  "dataDir", "storage", "passphrase", "rollbackRoot", "audit",
85
85
  "maxPulledBytes", "maxPulledFiles",
86
+ "requireSignature", "expectedFingerprint", "verifySignature",
86
87
  ], "restore");
87
88
  validateOpts.requireNonEmptyString(opts.dataDir, "create: opts.dataDir", RestoreError, "restore/no-datadir");
88
89
  _validateStorage(opts.storage);
@@ -96,6 +97,19 @@ function create(opts) {
96
97
  var passphrase = opts.passphrase;
97
98
  var rollbackRoot = opts.rollbackRoot || (dataDir + ".rollbacks");
98
99
  var auditOn = opts.audit !== false;
100
+ // Manifest-signature policy. The framework signs bundles best-effort (an
101
+ // unsigned bundle is the documented CLI / standalone / worker case), so the
102
+ // non-opt-in integrity default is the per-blob AEAD path-binding below
103
+ // (which defeats the blob-remap attack on EVERY bundle, signed or not);
104
+ // requireSignature is the additional provenance policy operators under
105
+ // HIPAA/PCI opt into to mandate a verified signer. expectedFingerprint pins
106
+ // a signer; verifySignature:false allows a cold/cross-org restore. All
107
+ // three are threaded into restoreBundle.extract on every run — previously
108
+ // omitted entirely, so a present signature was never even verified and a
109
+ // requireSignature policy could not be enforced (CWE-347).
110
+ var requireSignature = opts.requireSignature === true;
111
+ var expectedFingerprint = opts.expectedFingerprint;
112
+ var verifySignature = opts.verifySignature;
99
113
 
100
114
  // Preflight footprint caps. Defended against storage that returns a
101
115
  // tampered or oversized bundle: we cap both the storage-reported size
@@ -271,6 +285,9 @@ function create(opts) {
271
285
  passphrase: passphrase,
272
286
  filter: runOpts.filter,
273
287
  progressCallback: runOpts.progressCallback,
288
+ requireSignature: requireSignature,
289
+ expectedFingerprint: expectedFingerprint,
290
+ verifySignature: verifySignature,
274
291
  });
275
292
  } catch (e) {
276
293
  _cleanupTmp();
@@ -283,6 +300,8 @@ function create(opts) {
283
300
  else if (code === "restore-bundle/missing-manifest") mappedCode = "restore/missing-manifest";
284
301
  else if (code === "restore-bundle/missing-blob") mappedCode = "restore/missing-blob";
285
302
  else if (code === "restore-bundle/size-mismatch") mappedCode = "restore/size-mismatch";
303
+ else if (code === "restore-bundle/missing-signature") mappedCode = "restore/missing-signature";
304
+ else if (code === "restore-bundle/bad-signature") mappedCode = "restore/bad-signature";
286
305
  _emitAudit("restore.failure",
287
306
  { bundleId: bundleId, reason: (e && e.message) || String(e) }, "failure");
288
307
  throw new RestoreError(mappedCode,
package/lib/retention.js CHANGED
@@ -431,17 +431,29 @@ function create(opts) {
431
431
 
432
432
  try {
433
433
  var moreRows = true;
434
+ // Keyset pagination cursor. Without it the loop re-selects the SAME batch
435
+ // forever whenever a full batch of rows is NOT removed from the candidate
436
+ // set by its action — legal-hold-skipped, "warn"-stage, errored, and
437
+ // (critically) EVERY row under dryRun mutate nothing, so a LIMIT-from-the-
438
+ // top query returns the identical rows each pass and `rows.length ===
439
+ // batchSize` never goes false. Ordering by _id and advancing past the last
440
+ // row seen guarantees forward progress regardless of whether a row was
441
+ // actioned. (Deleted / anonymized rows are also simply skipped past.)
442
+ var lastId = null;
434
443
  while (moreRows) {
435
444
  var rows;
436
445
  // The candidate WHERE-clause: age + not-already-erased + not-on-legal-hold +
437
- // (when soft-delete is configured) not-already-soft-deleted. Built
438
- // through b.sql so the operator-supplied table / ageField / softDeleteField
439
- // identifiers are quoted by construction and every value binds as a
440
- // placeholder (the '' empty-string compare included — no embedded literal).
446
+ // (when soft-delete is configured) not-already-soft-deleted + keyset cursor.
447
+ // Built through b.sql so the operator-supplied table / ageField /
448
+ // softDeleteField identifiers are quoted by construction and every value
449
+ // binds as a placeholder (the '' empty-string compare included — no
450
+ // embedded literal).
441
451
  function _candidateBase() {
442
452
  var qb = sql.select(rule.table, SQL_OPTS)
443
453
  .where(rule.ageField, "<=", cutoff);
444
454
  if (rule.softDeleteField) qb.whereNull(rule.softDeleteField);
455
+ if (lastId !== null) qb.where("_id", ">", lastId);
456
+ qb.orderBy("_id", "asc");
445
457
  return qb;
446
458
  }
447
459
  var selStmt;
@@ -504,6 +516,10 @@ function create(opts) {
504
516
  reason: (e && e.message) || String(e) });
505
517
  }
506
518
  }
519
+ // Advance the keyset cursor past this batch so already-seen rows
520
+ // (including skipped / warned / errored / dry-run rows that stay in the
521
+ // candidate set) are never re-selected — the loop-termination guarantee.
522
+ lastId = rows[rows.length - 1]._id;
507
523
  if (rows.length < rule.batchSize) moreRows = false;
508
524
  }
509
525
  } catch (e) {
package/lib/router.js CHANGED
@@ -36,6 +36,7 @@ var http = require("node:http");
36
36
  var http2 = require("node:http2");
37
37
  var nodeFs = require("node:fs");
38
38
  var nodePath = require("node:path");
39
+ var atomicFile = require("./atomic-file");
39
40
  var C = require("./constants");
40
41
  var requestHelpers = require("./request-helpers");
41
42
  var lazyRequire = require("./lazy-require");
@@ -147,7 +148,11 @@ function _makeSchemaValidator(spec) {
147
148
  if (!qq.ok) return _writeValidationError(req, res, "query", qq.errors);
148
149
  req.query = qq.value;
149
150
  }
150
- if (spec.body && req.body !== undefined) {
151
+ if (spec.body) {
152
+ // Validate even when req.body is undefined (no body parsed / empty POST):
153
+ // a required body schema must report the violation, not be silently
154
+ // skipped. safeParse(undefined) fails a non-optional schema and passes an
155
+ // optional()/default() one. Mirrors the always-run params/query checks.
151
156
  var bb = spec.body.safeParse(req.body);
152
157
  if (!bb.ok) return _writeValidationError(req, res, "body", bb.errors);
153
158
  req.body = bb.value;
@@ -1425,11 +1430,19 @@ function serveStatic(dir) {
1425
1430
  // that shares the root's name as a prefix — e.g. root `/srv/public`
1426
1431
  // vs `/srv/public-evil` — cannot satisfy the containment check.
1427
1432
  if (filePath !== root && !filePath.startsWith(root + nodePath.sep)) return next();
1428
- if (!nodeFs.existsSync(filePath) || nodeFs.statSync(filePath).isDirectory()) return next();
1433
+ // Open the (lexically-confined) path ONCE with O_NOFOLLOW and bind existence,
1434
+ // stat, and streaming to that single fd. The previous existsSync → statSync →
1435
+ // statSync → createReadStream sequence was a triple check-then-read TOCTOU
1436
+ // (CWE-367) on a request-derived pathname, and the plain createReadStream
1437
+ // would follow a symlink swapped in after the lexical check (CWE-22).
1438
+ // Opening before writeHead lets a refusal still fall through to next().
1439
+ var fd;
1440
+ try { fd = atomicFile.openNoFollowSync(filePath); } catch (_e) { return next(); }
1441
+ var stat = nodeFs.fstatSync(fd);
1442
+ if (stat.isDirectory()) { nodeFs.closeSync(fd); return next(); }
1429
1443
 
1430
1444
  var ext = nodePath.extname(filePath).toLowerCase();
1431
1445
  var mime = MIME_TYPES[ext] || "application/octet-stream";
1432
- var stat = nodeFs.statSync(filePath);
1433
1446
  var hasVersion = req.url && req.url.includes("?v=");
1434
1447
  var cacheControl = hasVersion
1435
1448
  ? "public, max-age=31536000, immutable"
@@ -1439,7 +1452,7 @@ function serveStatic(dir) {
1439
1452
  "Content-Length": stat.size,
1440
1453
  "Cache-Control": cacheControl,
1441
1454
  });
1442
- nodeFs.createReadStream(filePath).pipe(res);
1455
+ nodeFs.createReadStream(filePath, { fd: fd }).pipe(res);
1443
1456
  };
1444
1457
  }
1445
1458
 
package/lib/safe-ical.js CHANGED
@@ -444,7 +444,7 @@ function _parseComponent(lines, startIdx, ctx, depth) {
444
444
  "safeIcal.parse: expected BEGIN, got '" + begin.name + "'");
445
445
  }
446
446
  var compName = begin.value.toUpperCase();
447
- if (!KNOWN_COMPONENTS[compName] && !ctx.extraComps[compName] &&
447
+ if (!Object.prototype.hasOwnProperty.call(KNOWN_COMPONENTS, compName) && !ctx.extraComps[compName] &&
448
448
  compName.indexOf("X-") !== 0) {
449
449
  throw new SafeIcalError("safe-ical/unknown-component",
450
450
  "safeIcal.parse: unknown component '" + compName +
@@ -475,7 +475,7 @@ function _parseComponent(lines, startIdx, ctx, depth) {
475
475
  }
476
476
  // Validate property name.
477
477
  var pn = ln.name;
478
- if (!KNOWN_PROPERTIES[pn] && !ctx.extraProps[pn] && pn.indexOf("X-") !== 0) {
478
+ if (!Object.prototype.hasOwnProperty.call(KNOWN_PROPERTIES, pn) && !ctx.extraProps[pn] && pn.indexOf("X-") !== 0) {
479
479
  throw new SafeIcalError("safe-ical/unknown-property",
480
480
  "safeIcal.parse: unknown property '" + pn +
481
481
  "' (extend via opts.extraProperties or use X- prefix)");
package/lib/safe-icap.js CHANGED
@@ -426,7 +426,7 @@ function _parseEncapsulated(value) {
426
426
  }
427
427
  var part = token.slice(0, eq);
428
428
  var offStr = token.slice(eq + 1);
429
- if (!ENCAPSULATED_PARTS[part]) {
429
+ if (!Object.prototype.hasOwnProperty.call(ENCAPSULATED_PARTS, part)) {
430
430
  throw new SafeIcapError("safe-icap/bad-encapsulated",
431
431
  "safeIcap.parse: Encapsulated part '" + part + "' is not one of " +
432
432
  Object.keys(ENCAPSULATED_PARTS).join(", "));
package/lib/safe-json.js CHANGED
@@ -354,6 +354,49 @@ function stringify(value, opts) {
354
354
  }
355
355
  }
356
356
 
357
+ /**
358
+ * @primitive b.safeJson.stringifyForScript
359
+ * @signature b.safeJson.stringifyForScript(value, opts?)
360
+ * @since 0.15.14
361
+ * @status stable
362
+ * @related b.safeJson.stringify
363
+ *
364
+ * Like `b.safeJson.stringify` but safe to embed verbatim inside an
365
+ * inline `<script>` element. Raw `JSON.stringify` does not escape `<`,
366
+ * `>`, or `&`, so a string value containing `</script>` (or `<!--`)
367
+ * closes the surrounding script element and injects markup; the
368
+ * Unicode line/paragraph separators U+2028 / U+2029 are also illegal
369
+ * unescaped in a script context on older parsers. This escapes all of
370
+ * them to their equivalent `\uXXXX` JSON escapes — the parsed value is
371
+ * byte-identical, but no substring can break out of a `<script>` block.
372
+ *
373
+ * @opts
374
+ * indent: number | string, // forwarded to b.safeJson.stringify
375
+ * allowProto:boolean, // forwarded
376
+ *
377
+ * @example
378
+ * var json = b.safeJson.stringifyForScript({ url: "/a</script>x" });
379
+ * res.end('<script type="importmap">' + json + '</script>');
380
+ * // → the "</script>" inside the value is emitted as "</script>"
381
+ */
382
+ function stringifyForScript(value, opts) {
383
+ var json = stringify(value, opts);
384
+ // BS is a single backslash built at runtime so this source carries no
385
+ // escape literals (they round-trip badly through tooling). Escape < > &
386
+ // so the JSON cannot close / comment-open the surrounding inline
387
+ // <script>; escape U+2028 / U+2029 (illegal unescaped in a script on
388
+ // older parsers). The parsed value is unchanged.
389
+ var BS = String.fromCharCode(92);
390
+ json = json.replace(/[<>&]/g, function (c) {
391
+ if (c === "<") return BS + "u003c";
392
+ if (c === ">") return BS + "u003e";
393
+ return BS + "u0026";
394
+ });
395
+ json = json.split(String.fromCharCode(0x2028)).join(BS + "u2028");
396
+ json = json.split(String.fromCharCode(0x2029)).join(BS + "u2029");
397
+ return json;
398
+ }
399
+
357
400
  // Walk the value, substituting any references that would create a cycle
358
401
  // with `replacement`. Uses an active-stack Set so SHARED non-circular
359
402
  // subtrees are preserved (only true cycles are replaced).
@@ -926,10 +969,37 @@ function _capInt(value, defaultValue, ceiling) {
926
969
  * });
927
970
  */
928
971
 
972
+ /**
973
+ * @primitive b.safeJson.isJsonObject
974
+ * @signature b.safeJson.isJsonObject(value)
975
+ * @since 0.15.14
976
+ * @status stable
977
+ * @related b.safeJson.parse
978
+ *
979
+ * True iff <code>value</code> is a plain JSON object — not <code>null</code>,
980
+ * not an array, not a scalar. <code>safeJson.parse</code> accepts the literal
981
+ * <code>null</code> and scalars / arrays (all valid JSON documents), so a
982
+ * parsed JWS header, claims set, or document must be re-checked before its
983
+ * fields are dereferenced. This is that check, shared so the
984
+ * <code>!x || typeof x !== "object" || Array.isArray(x)</code> idiom isn't
985
+ * re-rolled (and silently varied) at every call site.
986
+ *
987
+ * @example
988
+ * var b = require("blamejs");
989
+ * b.safeJson.isJsonObject(b.safeJson.parse('{"a":1}')); // → true
990
+ * b.safeJson.isJsonObject(b.safeJson.parse("null")); // → false
991
+ * b.safeJson.isJsonObject(b.safeJson.parse("[1,2]")); // → false
992
+ */
993
+ function isJsonObject(value) {
994
+ return value !== null && typeof value === "object" && !Array.isArray(value);
995
+ }
996
+
929
997
  module.exports = {
930
998
  parse: parse,
931
999
  parseOrDefault: parseOrDefault,
1000
+ isJsonObject: isJsonObject,
932
1001
  stringify: stringify,
1002
+ stringifyForScript: stringifyForScript,
933
1003
  canonical: canonical,
934
1004
  validate: validate,
935
1005
  registerFormat: registerFormat,
package/lib/safe-sieve.js CHANGED
@@ -133,7 +133,7 @@ function _resolveProfile(opts) {
133
133
  if (!opts) return "strict";
134
134
  if (typeof opts.profile === "string") return opts.profile;
135
135
  if (typeof opts.compliancePosture === "string") {
136
- return COMPLIANCE_POSTURES[opts.compliancePosture] || "strict";
136
+ return (Object.prototype.hasOwnProperty.call(COMPLIANCE_POSTURES, opts.compliancePosture) && COMPLIANCE_POSTURES[opts.compliancePosture]) || "strict";
137
137
  }
138
138
  return "strict";
139
139
  }
package/lib/safe-vcard.js CHANGED
@@ -361,7 +361,7 @@ function _parseVcard(lines, startIdx, caps, extraProps) {
361
361
  "safeVcard.parse: nested BEGIN inside VCARD (vCard does not support sub-components)");
362
362
  }
363
363
  var pn = ln.name;
364
- if (!KNOWN_PROPERTIES[pn] && !extraProps[pn] && pn.indexOf("X-") !== 0) {
364
+ if (!Object.prototype.hasOwnProperty.call(KNOWN_PROPERTIES, pn) && !extraProps[pn] && pn.indexOf("X-") !== 0) {
365
365
  throw new SafeVcardError("safe-vcard/unknown-property",
366
366
  "safeVcard.parse: unknown property '" + pn +
367
367
  "' (extend via opts.extraProperties or use X- prefix)");
@@ -63,6 +63,12 @@ var workerThreads = require("node:worker_threads");
63
63
  "setInterval", "clearInterval",
64
64
  "queueMicrotask",
65
65
  "global",
66
+ // WebAssembly linear memory is allocated OUTSIDE the V8 heap, so the
67
+ // worker's maxOldGenerationSizeMb cap (derived from opts.maxBytes) does
68
+ // NOT bound `new WebAssembly.Memory(...).grow(N)` — operator source could
69
+ // commit multi-GiB of off-heap RAM under a tiny maxBytes (CWE-770). It is
70
+ // not in KNOWN_SAFE_BUILTINS so no `allowed` opt can re-expose it.
71
+ "WebAssembly",
66
72
  ];
67
73
  for (var k = 0; k < NODE_BUILTINS.length; k += 1) {
68
74
  var nm = NODE_BUILTINS[k];
package/lib/sandbox.js CHANGED
@@ -133,7 +133,7 @@ function _validateAllowed(allowed) {
133
133
  throw new SandboxError("sandbox/bad-allowed",
134
134
  "sandbox.run: opts.allowed[" + i + "] must be a non-empty identifier string");
135
135
  }
136
- if (!KNOWN_SAFE_BUILTINS[name]) {
136
+ if (!Object.prototype.hasOwnProperty.call(KNOWN_SAFE_BUILTINS, name)) {
137
137
  throw new SandboxError("sandbox/bad-allowed",
138
138
  "sandbox.run: opts.allowed[" + i + "] = " + JSON.stringify(name) +
139
139
  " is not in the sandbox built-in allowlist " +
package/lib/scheduler.js CHANGED
@@ -177,7 +177,7 @@ function parseCron(expr) {
177
177
  "cron expression must be a non-empty string", true);
178
178
  }
179
179
  var trimmed = expr.trim();
180
- if (CRON_SHORTHANDS[trimmed.toLowerCase()]) {
180
+ if (Object.prototype.hasOwnProperty.call(CRON_SHORTHANDS, trimmed.toLowerCase())) {
181
181
  trimmed = CRON_SHORTHANDS[trimmed.toLowerCase()];
182
182
  }
183
183
  var fields = trimmed.split(/\s+/);
@@ -711,8 +711,24 @@ function create(opts) {
711
711
  });
712
712
  }
713
713
 
714
+ // Node coerces a setTimeout delay greater than the 32-bit signed max
715
+ // (2147483647 ms ≈ 24.8 days) to 1 ms — so a far-future task would fire
716
+ // immediately, and an interval longer than the max would tight-loop. Wait
717
+ // the max chunk, then re-arm for the remainder WITHOUT firing.
718
+ var TIMEOUT_MAX_MS = 2147483647;
719
+
714
720
  function _arm(task) {
715
721
  var delay = Math.max(0, task.nextRun - Date.now());
722
+ if (delay > TIMEOUT_MAX_MS) {
723
+ var tc = setTimeout(function () {
724
+ timers.delete(tc);
725
+ if (!started) return;
726
+ _arm(task); // re-arm for the remaining delay; do NOT fire yet
727
+ }, TIMEOUT_MAX_MS);
728
+ if (typeof tc.unref === "function") tc.unref();
729
+ timers.add(tc);
730
+ return;
731
+ }
716
732
  var t = setTimeout(function () {
717
733
  timers.delete(t);
718
734
  if (!started) return;
@@ -181,6 +181,14 @@ function verify(assetPath, signaturePath, pubkeyPem) {
181
181
  var signature;
182
182
  try {
183
183
  var sigStat = nodeFs.fstatSync(sigFd);
184
+ // Bound the alloc: the largest supported signature (ML-DSA-87) is ~4.6 KB;
185
+ // 64 KiB is far above any legitimate sig and stops Buffer.allocUnsafe(stat.size)
186
+ // from OOM-ing if signaturePath is pointed at a giant file. Zero-dep by
187
+ // contract — inline literal, cannot import C.BYTES.
188
+ if (sigStat.size > 64 * 1024) { // allow:raw-byte-literal — zero-dep module
189
+ throw new Error("standalone-verifier.verify: signature file implausibly large (" +
190
+ sigStat.size + " bytes)");
191
+ }
184
192
  signature = Buffer.allocUnsafe(sigStat.size);
185
193
  if (sigStat.size > 0) nodeFs.readSync(sigFd, signature, 0, sigStat.size, 0);
186
194
  } finally {
@@ -219,6 +227,14 @@ function verify(assetPath, signaturePath, pubkeyPem) {
219
227
  // to come from the same {0..assetStat.size} range fixed at open
220
228
  // time.
221
229
  var assetStat = nodeFs.fstatSync(assetFd);
230
+ // Bound the asset alloc before Buffer.allocUnsafe(assetStat.size): a self-update
231
+ // bundle (SEA) is intentionally large, so the ceiling is generous (2 GiB), but
232
+ // it stops a signaturePath/assetPath pointed at an unbounded file from OOM-ing
233
+ // the verifier before any byte is hashed. Zero-dep by contract — inline literal.
234
+ if (assetStat.size > 2 * 1024 * 1024 * 1024) { // allow:raw-byte-literal — zero-dep module, 2 GiB asset ceiling
235
+ throw new Error("standalone-verifier.verify: asset implausibly large (" +
236
+ assetStat.size + " bytes) — exceeds the 2 GiB self-update ceiling");
237
+ }
222
238
  var sha256 = nodeCrypto.createHash("sha256");
223
239
  var sha3 = nodeCrypto.createHash("sha3-512");
224
240
  var verifier = (alg === "ecdsa-p384") ? nodeCrypto.createVerify("sha3-512") : null;