@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.
- package/CHANGELOG.md +4 -0
- package/index.js +2 -0
- package/lib/a2a-tasks.js +38 -6
- package/lib/agent-event-bus.js +13 -0
- package/lib/agent-idempotency.js +5 -1
- package/lib/agent-snapshot.js +32 -2
- package/lib/ai-aedt-bias-audit.js +2 -1
- package/lib/ai-content-detect.js +1 -3
- package/lib/ai-frontier-protocol.js +1 -1
- package/lib/ai-model-manifest.js +1 -1
- package/lib/ai-output.js +16 -7
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +7 -1
- package/lib/archive-gz.js +9 -0
- package/lib/archive-read.js +9 -7
- package/lib/archive-tar-read.js +51 -8
- package/lib/archive.js +4 -2
- package/lib/asn1-der.js +70 -22
- package/lib/atomic-file.js +204 -2
- package/lib/audit-chain.js +54 -5
- package/lib/audit-daily-review.js +12 -2
- package/lib/audit-sign.js +2 -1
- package/lib/audit-tools.js +108 -23
- package/lib/audit.js +7 -2
- package/lib/auth/access-lock.js +2 -2
- package/lib/auth/bot-challenge.js +1 -1
- package/lib/auth/ciba.js +71 -8
- package/lib/auth/dpop.js +15 -1
- package/lib/auth/fido-mds3.js +30 -13
- package/lib/auth/jwt.js +21 -5
- package/lib/auth/lockout.js +18 -2
- package/lib/auth/oauth.js +8 -2
- package/lib/auth/passkey.js +1 -1
- package/lib/auth/password.js +1 -1
- package/lib/auth/saml.js +42 -12
- package/lib/auth/sd-jwt-vc.js +24 -4
- package/lib/auth/status-list.js +14 -2
- package/lib/auth/step-up.js +9 -1
- package/lib/auth-bot-challenge.js +21 -2
- package/lib/backup/bundle.js +7 -2
- package/lib/backup/crypto.js +23 -8
- package/lib/backup/index.js +41 -20
- package/lib/backup/manifest.js +7 -1
- package/lib/break-glass.js +41 -22
- package/lib/cbor.js +34 -11
- package/lib/cdn-cache-control.js +7 -3
- package/lib/cert.js +5 -3
- package/lib/cli.js +5 -1
- package/lib/cloud-events.js +3 -2
- package/lib/cluster-storage.js +7 -3
- package/lib/codepoint-class.js +17 -0
- package/lib/compliance-eaa.js +1 -1
- package/lib/compliance-sanctions.js +9 -7
- package/lib/compliance.js +1 -1
- package/lib/config-drift.js +22 -8
- package/lib/content-credentials.js +11 -8
- package/lib/content-digest.js +11 -4
- package/lib/cookies.js +10 -2
- package/lib/cose.js +20 -0
- package/lib/crdt.js +2 -1
- package/lib/crypto-field.js +48 -24
- package/lib/crypto.js +18 -4
- package/lib/csp.js +13 -0
- package/lib/daemon.js +4 -1
- package/lib/data-act.js +27 -4
- package/lib/db-file-lifecycle.js +14 -6
- package/lib/db-query.js +102 -11
- package/lib/db.js +32 -15
- package/lib/dora.js +43 -13
- package/lib/dr-runbook.js +1 -1
- package/lib/dsa.js +2 -1
- package/lib/dsr.js +22 -8
- package/lib/early-hints.js +19 -0
- package/lib/eat.js +5 -1
- package/lib/external-db.js +60 -4
- package/lib/fda-21cfr11.js +30 -5
- package/lib/flag-providers.js +6 -2
- package/lib/forms.js +1 -1
- package/lib/gate-contract.js +46 -5
- package/lib/gdpr-ropa.js +18 -9
- package/lib/graphql-federation.js +17 -4
- package/lib/guard-all.js +2 -2
- package/lib/guard-dsn.js +1 -1
- package/lib/guard-envelope.js +1 -1
- package/lib/guard-html.js +9 -11
- package/lib/guard-imap-command.js +1 -1
- package/lib/guard-jmap.js +1 -1
- package/lib/guard-json.js +14 -6
- package/lib/guard-mail-move.js +1 -1
- package/lib/guard-managesieve-command.js +1 -1
- package/lib/guard-pop3-command.js +1 -1
- package/lib/guard-smtp-command.js +1 -1
- package/lib/guard-svg.js +8 -9
- package/lib/html-balance.js +7 -3
- package/lib/http-client-cookie-jar.js +33 -12
- package/lib/http-client.js +225 -53
- package/lib/iab-tcf.js +3 -2
- package/lib/importmap-integrity.js +41 -1
- package/lib/incident-report.js +9 -6
- package/lib/json-patch.js +1 -1
- package/lib/json-path.js +24 -3
- package/lib/jtd.js +2 -2
- package/lib/legal-hold.js +24 -8
- package/lib/log.js +2 -2
- package/lib/mail-agent.js +2 -2
- package/lib/mail-arf.js +1 -1
- package/lib/mail-auth.js +27 -4
- package/lib/mail-bimi.js +16 -16
- package/lib/mail-bounce.js +3 -3
- package/lib/mail-crypto-smime.js +71 -6
- package/lib/mail-deploy.js +9 -5
- package/lib/mail-dkim.js +20 -7
- package/lib/mail-greylist.js +2 -4
- package/lib/mail-helo.js +2 -4
- package/lib/mail-journal.js +11 -8
- package/lib/mail-mdn.js +8 -4
- package/lib/mail-rbl.js +2 -4
- package/lib/mail-scan.js +3 -5
- package/lib/mail-server-jmap.js +4 -1
- package/lib/mail-server-registry.js +1 -1
- package/lib/mail-server-tls.js +9 -2
- package/lib/mail-spam-score.js +2 -4
- package/lib/mail-store-fts.js +1 -1
- package/lib/mail.js +22 -2
- package/lib/markup-tokenizer.js +24 -0
- package/lib/mcp.js +6 -4
- package/lib/mdoc.js +26 -3
- package/lib/metrics.js +14 -3
- package/lib/middleware/api-encrypt.js +2 -2
- package/lib/middleware/body-parser.js +10 -4
- package/lib/middleware/bot-guard.js +26 -18
- package/lib/middleware/clear-site-data.js +5 -1
- package/lib/middleware/compose-pipeline.js +39 -5
- package/lib/middleware/compression.js +9 -0
- package/lib/middleware/cors.js +32 -23
- package/lib/middleware/csrf-protect.js +60 -21
- package/lib/middleware/daily-byte-quota.js +6 -4
- package/lib/middleware/fetch-metadata.js +28 -4
- package/lib/middleware/network-allowlist.js +61 -30
- package/lib/middleware/rate-limit.js +25 -16
- package/lib/middleware/scim-server.js +2 -1
- package/lib/middleware/security-headers.js +24 -6
- package/lib/middleware/speculation-rules.js +6 -3
- package/lib/middleware/tus-upload.js +2 -2
- package/lib/money.js +1 -1
- package/lib/mtls-ca.js +10 -6
- package/lib/network-dns-resolver.js +1 -1
- package/lib/network-dns.js +9 -2
- package/lib/network-dnssec.js +2 -1
- package/lib/network-smtp-policy.js +23 -5
- package/lib/network-tls.js +27 -5
- package/lib/network-tsig.js +2 -2
- package/lib/nis2-report.js +1 -1
- package/lib/nist-crosswalk.js +1 -1
- package/lib/ntp-check.js +28 -0
- package/lib/numeric-bounds.js +9 -0
- package/lib/object-store/azure-blob.js +1 -2
- package/lib/object-store/gcs-bucket-ops.js +4 -2
- package/lib/object-store/gcs.js +6 -4
- package/lib/object-store/http-put.js +1 -2
- package/lib/object-store/http-request.js +30 -1
- package/lib/object-store/local.js +37 -17
- package/lib/object-store/sigv4.js +1 -2
- package/lib/observability-otlp-exporter.js +20 -4
- package/lib/outbox.js +11 -4
- package/lib/parsers/safe-xml.js +1 -1
- package/lib/parsers/safe-yaml.js +21 -3
- package/lib/pipl-cn.js +11 -8
- package/lib/queue-local.js +10 -3
- package/lib/redact.js +7 -3
- package/lib/request-helpers.js +347 -36
- package/lib/resource-access-lock.js +3 -3
- package/lib/restore-bundle.js +46 -18
- package/lib/restore-rollback.js +10 -4
- package/lib/restore.js +19 -0
- package/lib/retention.js +20 -4
- package/lib/router.js +17 -4
- package/lib/safe-ical.js +2 -2
- package/lib/safe-icap.js +1 -1
- package/lib/safe-json.js +70 -0
- package/lib/safe-sieve.js +1 -1
- package/lib/safe-vcard.js +1 -1
- package/lib/sandbox-worker.js +6 -0
- package/lib/sandbox.js +1 -1
- package/lib/scheduler.js +17 -1
- package/lib/self-update-standalone-verifier.js +16 -0
- package/lib/session.js +62 -120
- package/lib/sql.js +25 -3
- package/lib/static.js +65 -13
- package/lib/template.js +7 -5
- package/lib/tenant-quota.js +52 -19
- package/lib/tsa.js +5 -2
- package/lib/vault/index.js +5 -0
- package/lib/vault/passphrase-ops.js +22 -26
- package/lib/vault/passphrase-source.js +8 -3
- package/lib/vault/rotate.js +13 -18
- package/lib/vault/seal-pem-file.js +4 -1
- package/lib/vc.js +1 -1
- package/lib/vendor/MANIFEST.json +10 -10
- package/lib/vendor/public-suffix-list.dat +23 -10
- package/lib/vendor/public-suffix-list.data.js +5498 -5494
- package/lib/webhook.js +16 -1
- package/lib/websocket.js +1 -1
- package/lib/worm.js +1 -1
- package/lib/ws-client.js +83 -46
- package/lib/x509-chain.js +91 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/restore-bundle.js
CHANGED
|
@@ -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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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(
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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 = {
|
package/lib/restore-rollback.js
CHANGED
|
@@ -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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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.
|
|
438
|
-
// through b.sql so the operator-supplied table / ageField /
|
|
439
|
-
// identifiers are quoted by construction and every value
|
|
440
|
-
// placeholder (the '' empty-string compare included — no
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)");
|
package/lib/sandbox-worker.js
CHANGED
|
@@ -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
|
|
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
|
|
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;
|