@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/audit-tools.js
CHANGED
|
@@ -58,6 +58,7 @@ var nodeFs = require("node:fs");
|
|
|
58
58
|
var nodePath = require("node:path");
|
|
59
59
|
var pkg = require("../package.json");
|
|
60
60
|
var atomicFile = require("./atomic-file");
|
|
61
|
+
var C = require("./constants");
|
|
61
62
|
var auditChain = require("./audit-chain");
|
|
62
63
|
var canonicalJson = require("./canonical-json");
|
|
63
64
|
var auditSign = require("./audit-sign");
|
|
@@ -349,6 +350,12 @@ async function _defaultReadPredecessorRowHash(firstCounter) {
|
|
|
349
350
|
async function _buildBundle(args) {
|
|
350
351
|
var kind = args.kind;
|
|
351
352
|
var rows = args.rows;
|
|
353
|
+
// Verification witnesses: the rows between the purgeable slice's tip and
|
|
354
|
+
// the covering checkpoint's anchored counter. They ride the bundle so the
|
|
355
|
+
// chain can be walked up to the SIGNED atRowHash, but they are NOT part of
|
|
356
|
+
// the purgeable range (the manifest range below is computed from `rows`,
|
|
357
|
+
// and purge() only deletes [first..lastCounter]).
|
|
358
|
+
var witnessRows = args.witnessRows || [];
|
|
352
359
|
var checkpoint = args.checkpoint || null;
|
|
353
360
|
var passphrase = args.passphrase;
|
|
354
361
|
var predecessorRowHash = args.predecessorRowHash;
|
|
@@ -357,8 +364,9 @@ async function _buildBundle(args) {
|
|
|
357
364
|
var lastRow = rows[rows.length - 1];
|
|
358
365
|
var files = {};
|
|
359
366
|
|
|
360
|
-
// 1. Encrypt the rows JSONL
|
|
361
|
-
|
|
367
|
+
// 1. Encrypt the rows JSONL — purgeable slice followed by any witnesses
|
|
368
|
+
// (contiguous, ascending) so the verifier walks one unbroken chain.
|
|
369
|
+
var jsonl = rows.concat(witnessRows).map(function (r) {
|
|
362
370
|
return JSON.stringify(_rowToWireForm(r));
|
|
363
371
|
}).join("\n") + "\n";
|
|
364
372
|
var rowsEnc = await backupCrypto.encryptWithFreshSalt(jsonl, passphrase);
|
|
@@ -437,26 +445,39 @@ async function _readBundle(inDir, passphrase) {
|
|
|
437
445
|
"bundle directory does not exist: " + inDir);
|
|
438
446
|
}
|
|
439
447
|
var manifestPath = nodePath.join(inDir, "manifest.json");
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
var manifest = safeJson.parse(
|
|
448
|
+
// Capped fd-bound read (no existsSync check-then-read window): an externally-
|
|
449
|
+
// supplied bundle manifest is parsed before verification, so an oversized
|
|
450
|
+
// manifest.json would OOM the verifier before safeJson sees it. 4 MiB is far
|
|
451
|
+
// above any real manifest.
|
|
452
|
+
var manifest = safeJson.parse(atomicFile.fdSafeReadSync(manifestPath, {
|
|
453
|
+
maxBytes: C.BYTES.mib(4), encoding: "utf8",
|
|
454
|
+
errorFor: function (kind, detail) {
|
|
455
|
+
if (kind === "enoent") return new AuditToolsError("audit-tools/no-manifest", "manifest.json missing in " + inDir);
|
|
456
|
+
if (kind === "too-large") return new AuditToolsError("audit-tools/bad-format", "manifest.json too large (" + detail.size + " > " + detail.max + ")");
|
|
457
|
+
return new AuditToolsError("audit-tools/bad-format", "manifest.json unreadable: " + kind);
|
|
458
|
+
},
|
|
459
|
+
}), { maxBytes: C.BYTES.mib(4) });
|
|
445
460
|
if (!manifest || manifest.format !== BUNDLE_FORMAT) {
|
|
446
461
|
throw new AuditToolsError("audit-tools/bad-format",
|
|
447
462
|
"manifest.format is not " + BUNDLE_FORMAT);
|
|
448
463
|
}
|
|
449
|
-
if (!VALID_KINDS
|
|
464
|
+
if (!Object.prototype.hasOwnProperty.call(VALID_KINDS, manifest.kind)) {
|
|
450
465
|
throw new AuditToolsError("audit-tools/bad-kind",
|
|
451
466
|
"manifest.kind must be one of " + Object.keys(VALID_KINDS).join(", "));
|
|
452
467
|
}
|
|
453
468
|
|
|
454
469
|
var rowsEncPath = nodePath.join(inDir, frameworkFiles.fileName("rowsEnc"));
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
470
|
+
// Capped fd-bound read: rows.enc is the PQC-encrypted archived audit slice
|
|
471
|
+
// (can be large); a hostile multi-GB blob would be read + decrypted in memory
|
|
472
|
+
// before the checksum check. 512 MiB ceiling bounds it (opt-tunable).
|
|
473
|
+
var rowsEnc = atomicFile.fdSafeReadSync(rowsEncPath, {
|
|
474
|
+
maxBytes: C.BYTES.mib(512),
|
|
475
|
+
errorFor: function (kind) {
|
|
476
|
+
if (kind === "enoent") return new AuditToolsError("audit-tools/no-rows-blob", "rows.enc missing in " + inDir);
|
|
477
|
+
if (kind === "too-large") return new AuditToolsError("audit-tools/rows-too-large", "rows.enc exceeds the bundle size cap");
|
|
478
|
+
return new AuditToolsError("audit-tools/no-rows-blob", "rows.enc unreadable: " + kind);
|
|
479
|
+
},
|
|
480
|
+
});
|
|
460
481
|
if (manifest.checksum && manifest.checksum.rowsSha3_512 &&
|
|
461
482
|
backupCrypto.checksum(rowsEnc) !== manifest.checksum.rowsSha3_512) {
|
|
462
483
|
throw new AuditToolsError("audit-tools/rows-checksum-mismatch",
|
|
@@ -470,11 +491,16 @@ async function _readBundle(inDir, passphrase) {
|
|
|
470
491
|
var checkpoint = null;
|
|
471
492
|
if (manifest.kind === KIND_ARCHIVE) {
|
|
472
493
|
var ckptPath = nodePath.join(inDir, frameworkFiles.fileName("checkpointEnc"));
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
494
|
+
// Capped fd-bound read: checkpoint.enc is a single PQC-encrypted
|
|
495
|
+
// audit_checkpoints row (a few KiB); 4 MiB bounds a hostile blob.
|
|
496
|
+
var ckptEnc = atomicFile.fdSafeReadSync(ckptPath, {
|
|
497
|
+
maxBytes: C.BYTES.mib(4),
|
|
498
|
+
errorFor: function (kind) {
|
|
499
|
+
if (kind === "enoent") return new AuditToolsError("audit-tools/no-checkpoint-blob", "checkpoint.enc missing in " + inDir + " (archive bundles must include the covering checkpoint)");
|
|
500
|
+
if (kind === "too-large") return new AuditToolsError("audit-tools/checkpoint-too-large", "checkpoint.enc exceeds the cap");
|
|
501
|
+
return new AuditToolsError("audit-tools/no-checkpoint-blob", "checkpoint.enc unreadable: " + kind);
|
|
502
|
+
},
|
|
503
|
+
});
|
|
478
504
|
if (manifest.checksum && manifest.checksum.checkpointSha3_512 &&
|
|
479
505
|
backupCrypto.checksum(ckptEnc) !== manifest.checksum.checkpointSha3_512) {
|
|
480
506
|
throw new AuditToolsError("audit-tools/checkpoint-checksum-mismatch",
|
|
@@ -562,10 +588,31 @@ async function archive(opts) {
|
|
|
562
588
|
|
|
563
589
|
var predecessorRowHash = await readPredecessorHash(firstCounter);
|
|
564
590
|
|
|
591
|
+
// The signed checkpoint is the bundle's only unforgeable anchor; it commits
|
|
592
|
+
// the row at checkpoint.atMonotonicCounter. When that counter sits BEYOND
|
|
593
|
+
// the purgeable slice's tip (the operator archived a subset older than the
|
|
594
|
+
// last checkpoint), carry the in-between rows as verification witnesses so
|
|
595
|
+
// verifyBundle can chain-walk up to the anchored row and bind atRowHash to
|
|
596
|
+
// it. Without them an attacker could pair any genuine high-counter
|
|
597
|
+
// checkpoint with a wholly fabricated slice. The witnesses are NOT purged.
|
|
598
|
+
var anchorCounter = Number(checkpoint.atMonotonicCounter);
|
|
599
|
+
var witnessRows = [];
|
|
600
|
+
if (anchorCounter > lastCounter) {
|
|
601
|
+
witnessRows = await readRows({ firstCounter: lastCounter + 1, lastCounter: anchorCounter });
|
|
602
|
+
var witnessTip = witnessRows.length ? Number(witnessRows[witnessRows.length - 1].monotonicCounter) : null;
|
|
603
|
+
if (witnessTip !== anchorCounter) {
|
|
604
|
+
throw new AuditToolsError("audit-tools/anchor-rows-missing",
|
|
605
|
+
"archive: covering checkpoint anchors counter=" + anchorCounter +
|
|
606
|
+
" but the rows up to it are not all available (read up to " + witnessTip +
|
|
607
|
+
") — cannot prove the slice chains to the signed anchor");
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
565
611
|
if (returnBytes) {
|
|
566
612
|
var built = await _buildBundle({
|
|
567
613
|
kind: KIND_ARCHIVE,
|
|
568
614
|
rows: rows,
|
|
615
|
+
witnessRows: witnessRows,
|
|
569
616
|
checkpoint: checkpoint,
|
|
570
617
|
passphrase: opts.passphrase,
|
|
571
618
|
predecessorRowHash: predecessorRowHash,
|
|
@@ -582,6 +629,7 @@ async function archive(opts) {
|
|
|
582
629
|
outDir: opts.out,
|
|
583
630
|
kind: KIND_ARCHIVE,
|
|
584
631
|
rows: rows,
|
|
632
|
+
witnessRows: witnessRows,
|
|
585
633
|
checkpoint: checkpoint,
|
|
586
634
|
passphrase: opts.passphrase,
|
|
587
635
|
predecessorRowHash: predecessorRowHash,
|
|
@@ -769,26 +817,41 @@ async function verifyBundle(opts) {
|
|
|
769
817
|
};
|
|
770
818
|
}
|
|
771
819
|
|
|
772
|
-
// 2. Confirm the stored firstRowHash + lastRowHash match the
|
|
820
|
+
// 2. Confirm the stored firstRowHash + lastRowHash match the PURGEABLE
|
|
821
|
+
// slice boundary. The slice tip is the row at range.lastCounter — not
|
|
822
|
+
// necessarily the physical last row, which may be a verification witness
|
|
823
|
+
// carried past the slice so the chain can reach the signed checkpoint.
|
|
824
|
+
var lastCounterN = Number(read.manifest.range.lastCounter);
|
|
825
|
+
var sliceLastRow = null;
|
|
826
|
+
for (var ri = 0; ri < read.rows.length; ri++) {
|
|
827
|
+
if (Number(read.rows[ri].monotonicCounter) === lastCounterN) { sliceLastRow = read.rows[ri]; break; }
|
|
828
|
+
}
|
|
773
829
|
if (read.rows[0].rowHash !== read.manifest.range.firstRowHash) {
|
|
774
830
|
return {
|
|
775
831
|
ok: false, kind: read.manifest.kind, rowsVerified: read.rows.length,
|
|
776
832
|
reason: "manifest.range.firstRowHash does not match first row's rowHash",
|
|
777
833
|
};
|
|
778
834
|
}
|
|
779
|
-
if (
|
|
835
|
+
if (!sliceLastRow || sliceLastRow.rowHash !== read.manifest.range.lastRowHash) {
|
|
780
836
|
return {
|
|
781
837
|
ok: false, kind: read.manifest.kind, rowsVerified: read.rows.length,
|
|
782
|
-
reason: "manifest.range.lastRowHash does not match
|
|
838
|
+
reason: "manifest.range.lastRowHash does not match the slice row at lastCounter",
|
|
783
839
|
};
|
|
784
840
|
}
|
|
785
841
|
|
|
786
|
-
// 3. (archive only) verify the covering checkpoint signature
|
|
842
|
+
// 3. (archive only) verify the covering checkpoint signature AND bind its
|
|
843
|
+
// anchored rowHash to the slice. The signature alone proves only that a
|
|
844
|
+
// checkpoint exists for some (counter, rowHash); without binding atRowHash
|
|
845
|
+
// to the archived rows, any genuine high-counter checkpoint could be paired
|
|
846
|
+
// with a wholly fabricated slice. The row at checkpoint.atMonotonicCounter
|
|
847
|
+
// must be present in the bundle (slice or witness) and its rowHash must
|
|
848
|
+
// equal checkpoint.atRowHash — mirroring b.audit.verifyCheckpoints against
|
|
849
|
+
// the live table.
|
|
787
850
|
if (read.manifest.kind === KIND_ARCHIVE) {
|
|
788
851
|
if (!read.checkpoint) {
|
|
789
852
|
return { ok: false, kind: KIND_ARCHIVE, reason: "checkpoint missing from archive bundle" };
|
|
790
853
|
}
|
|
791
|
-
if (Number(read.checkpoint.atMonotonicCounter) <
|
|
854
|
+
if (Number(read.checkpoint.atMonotonicCounter) < lastCounterN) {
|
|
792
855
|
return {
|
|
793
856
|
ok: false, kind: KIND_ARCHIVE,
|
|
794
857
|
reason: "checkpoint atMonotonicCounter (" + read.checkpoint.atMonotonicCounter +
|
|
@@ -805,6 +868,28 @@ async function verifyBundle(opts) {
|
|
|
805
868
|
};
|
|
806
869
|
}
|
|
807
870
|
}
|
|
871
|
+
// Bind: the row the signature anchors must be in the bundle and match.
|
|
872
|
+
var anchorCounterN = Number(read.checkpoint.atMonotonicCounter);
|
|
873
|
+
var anchoredRow = null;
|
|
874
|
+
for (var ai = 0; ai < read.rows.length; ai++) {
|
|
875
|
+
if (Number(read.rows[ai].monotonicCounter) === anchorCounterN) { anchoredRow = read.rows[ai]; break; }
|
|
876
|
+
}
|
|
877
|
+
if (!anchoredRow) {
|
|
878
|
+
return {
|
|
879
|
+
ok: false, kind: KIND_ARCHIVE,
|
|
880
|
+
reason: "checkpoint anchors counter=" + anchorCounterN +
|
|
881
|
+
" but no such row is present in the bundle — checkpoint not bound to the archived slice",
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
if (anchoredRow.rowHash !== read.checkpoint.atRowHash) {
|
|
885
|
+
return {
|
|
886
|
+
ok: false, kind: KIND_ARCHIVE,
|
|
887
|
+
reason: "checkpoint atRowHash does not match the bundle row at counter=" + anchorCounterN +
|
|
888
|
+
" — the signed anchor does not bind this slice",
|
|
889
|
+
expected: read.checkpoint.atRowHash,
|
|
890
|
+
actual: anchoredRow.rowHash,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
808
893
|
}
|
|
809
894
|
|
|
810
895
|
return {
|
package/lib/audit.js
CHANGED
|
@@ -797,7 +797,12 @@ async function query(criteria) {
|
|
|
797
797
|
if (criteria.resourceKind) q = q.where({ resourceKind: criteria.resourceKind });
|
|
798
798
|
if (criteria.outcome) q = q.where({ outcome: criteria.outcome });
|
|
799
799
|
|
|
800
|
-
|
|
800
|
+
// order: "asc" (default, chronological) | "desc" (newest-first). A capped
|
|
801
|
+
// query (limit set) returns the FIRST `limit` rows in this order — so a
|
|
802
|
+
// consumer that wants the most RECENT events under a cap (e.g. the daily
|
|
803
|
+
// review) must pass order:"desc", else `limit` keeps the OLDEST and drops the
|
|
804
|
+
// newest events in the window.
|
|
805
|
+
q.orderBy("monotonicCounter", criteria.order === "desc" ? "desc" : "asc");
|
|
801
806
|
if (criteria.limit != null) q.limit(criteria.limit);
|
|
802
807
|
if (criteria.offset != null) q.offset(criteria.offset);
|
|
803
808
|
|
|
@@ -834,7 +839,7 @@ async function _queryCluster(criteria) {
|
|
|
834
839
|
if (criteria.resourceKind) qb.where("resourceKind", criteria.resourceKind);
|
|
835
840
|
if (criteria.outcome) qb.where("outcome", criteria.outcome);
|
|
836
841
|
|
|
837
|
-
qb.orderBy("monotonicCounter", "asc");
|
|
842
|
+
qb.orderBy("monotonicCounter", criteria.order === "desc" ? "desc" : "asc");
|
|
838
843
|
if (criteria.limit != null) qb.limit(criteria.limit);
|
|
839
844
|
if (criteria.offset != null) qb.offset(criteria.offset);
|
|
840
845
|
|
package/lib/auth/access-lock.js
CHANGED
|
@@ -54,7 +54,7 @@ var SAFE_METHODS = Object.freeze({ GET: 1, HEAD: 1, OPTIONS: 1 });
|
|
|
54
54
|
function _normalizeMode(mode) {
|
|
55
55
|
if (typeof mode !== "string") return null;
|
|
56
56
|
var m = mode.toLowerCase();
|
|
57
|
-
return VALID_MODES
|
|
57
|
+
return Object.prototype.hasOwnProperty.call(VALID_MODES, m) ? m : null;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function create(opts) {
|
|
@@ -164,7 +164,7 @@ function create(opts) {
|
|
|
164
164
|
if (_hasUnlockRole(req)) return next();
|
|
165
165
|
if (currentMode === "read-only") {
|
|
166
166
|
var method = (req.method || "GET").toUpperCase();
|
|
167
|
-
if (SAFE_METHODS
|
|
167
|
+
if (Object.prototype.hasOwnProperty.call(SAFE_METHODS, method)) return next();
|
|
168
168
|
_emitAudit("refused", "denied", { mode: currentMode, method: method, path: req.url });
|
|
169
169
|
_emitMetric("refused", 1, { mode: currentMode, reason: "non-safe-method" });
|
|
170
170
|
return _refuse(res, "non-safe-method-in-read-only");
|
|
@@ -343,7 +343,7 @@ function create(opts) {
|
|
|
343
343
|
var secret = opts.secret;
|
|
344
344
|
|
|
345
345
|
var providerKey = opts.provider !== undefined ? opts.provider : DEFAULT_PROVIDER;
|
|
346
|
-
if (typeof providerKey !== "string" || !PROVIDERS
|
|
346
|
+
if (typeof providerKey !== "string" || !Object.prototype.hasOwnProperty.call(PROVIDERS, providerKey)) {
|
|
347
347
|
var supported = Object.keys(PROVIDERS).join(", ");
|
|
348
348
|
throw new BotChallengeError("bot-challenge/bad-opt",
|
|
349
349
|
"provider: expected one of [" + supported + "], got " + JSON.stringify(providerKey));
|
package/lib/auth/ciba.js
CHANGED
|
@@ -162,6 +162,10 @@ function create(opts) {
|
|
|
162
162
|
tokenEndpoint: opts.tokenEndpoint,
|
|
163
163
|
httpClientOpts: opts.httpClientOpts,
|
|
164
164
|
allowHttp: opts.allowHttp === true,
|
|
165
|
+
// Thread the SSRF opt-in through to the inner client's discovery / JWKS /
|
|
166
|
+
// token fetches — an operator with an internal-network IdP needs it, and
|
|
167
|
+
// dropping it silently made allowInternal an accepted-but-ignored option.
|
|
168
|
+
allowInternal: opts.allowInternal,
|
|
165
169
|
isOidc: true,
|
|
166
170
|
});
|
|
167
171
|
|
|
@@ -452,6 +456,45 @@ function create(opts) {
|
|
|
452
456
|
});
|
|
453
457
|
}
|
|
454
458
|
|
|
459
|
+
// Verify an OIDC ID token (if present) via the composed inner OAuth client,
|
|
460
|
+
// returning its verified claims, or null when no id_token was supplied. A
|
|
461
|
+
// present-but-invalid id_token throws (fail-closed) so a forged token's
|
|
462
|
+
// sub/acr/amr can never be returned as trusted.
|
|
463
|
+
async function _verifyIdTokenIfPresent(idToken, auditKey, op, expectedAuthReqId) {
|
|
464
|
+
if (!idToken) return null;
|
|
465
|
+
var verified;
|
|
466
|
+
try {
|
|
467
|
+
verified = await inner.verifyIdToken(idToken);
|
|
468
|
+
} catch (e) {
|
|
469
|
+
_emitAudit("id_token_verify_fail", "failure",
|
|
470
|
+
auditKey ? { authReqIdHash: sha3Hash(auditKey) } : {});
|
|
471
|
+
throw new AuthError("auth-ciba/id-token-invalid",
|
|
472
|
+
"ciba." + op + ": id_token failed verification: " +
|
|
473
|
+
((e && e.code) || (e && e.message) || String(e)));
|
|
474
|
+
}
|
|
475
|
+
// OpenID CIBA Core §7.3 / §10.1: the ID Token MUST carry the
|
|
476
|
+
// urn:openid:params:jwt:claim:auth_req_id claim and the RP MUST verify it
|
|
477
|
+
// equals the auth_req_id this flow used. Without it, an id_token minted for
|
|
478
|
+
// a DIFFERENT auth_req_id (another user's CIBA flow at the same RP) could be
|
|
479
|
+
// substituted — cross-user token substitution. verifyIdToken returns
|
|
480
|
+
// { header, claims }, so the claim lives on `.claims`. Constant-time compare.
|
|
481
|
+
// idToken is present (we returned early if not), so the binding is
|
|
482
|
+
// MANDATORY and ALWAYS checked — never gated behind a truthiness test that
|
|
483
|
+
// an empty string could slip. Both call sites pass a non-empty auth_req_id
|
|
484
|
+
// (validated at the parseNotification / pollToken entry), so a falsy value
|
|
485
|
+
// can't reach here to skip the comparison.
|
|
486
|
+
var payload = verified && verified.claims;
|
|
487
|
+
var boundId = payload && payload["urn:openid:params:jwt:claim:auth_req_id"];
|
|
488
|
+
if (typeof boundId !== "string" || !timingSafeEqual(boundId, expectedAuthReqId)) {
|
|
489
|
+
_emitAudit("id_token_authreqid_mismatch", "failure",
|
|
490
|
+
auditKey ? { authReqIdHash: sha3Hash(auditKey) } : {});
|
|
491
|
+
throw new AuthError("auth-ciba/id-token-authreqid-mismatch",
|
|
492
|
+
"ciba." + op + ": id_token urn:openid:params:jwt:claim:auth_req_id does not " +
|
|
493
|
+
"match the expected auth_req_id (cross-user token-substitution defense)");
|
|
494
|
+
}
|
|
495
|
+
return verified;
|
|
496
|
+
}
|
|
497
|
+
|
|
455
498
|
async function pollToken(popts) {
|
|
456
499
|
popts = popts || {};
|
|
457
500
|
if (typeof popts.authReqId !== "string" || popts.authReqId.length === 0) {
|
|
@@ -514,6 +557,12 @@ function create(opts) {
|
|
|
514
557
|
}
|
|
515
558
|
// Token issued — clear interval tracking for this authReqId.
|
|
516
559
|
_intervalState.delete(popts.authReqId);
|
|
560
|
+
// Verify the ID token (OIDC Core §3.1.3.7) via the composed inner OAuth
|
|
561
|
+
// client — it applies the create()-level issuer / clientId / JWKS /
|
|
562
|
+
// accepted-algorithms and enforces signature + iss/aud/exp. Returning an
|
|
563
|
+
// unverified id_token let a forged token's sub/acr/amr be trusted.
|
|
564
|
+
var pollClaims = await _verifyIdTokenIfPresent(rv.id_token,
|
|
565
|
+
"auth-ciba:" + popts.authReqId, "pollToken", popts.authReqId);
|
|
517
566
|
_emitAudit("token_received", "success", {
|
|
518
567
|
authReqIdHash: sha3Hash("auth-ciba:" + popts.authReqId),
|
|
519
568
|
});
|
|
@@ -521,6 +570,7 @@ function create(opts) {
|
|
|
521
570
|
return {
|
|
522
571
|
accessToken: rv.access_token || null,
|
|
523
572
|
idToken: rv.id_token || null,
|
|
573
|
+
claims: pollClaims,
|
|
524
574
|
refreshToken: rv.refresh_token || null,
|
|
525
575
|
tokenType: rv.token_type || null,
|
|
526
576
|
scope: rv.scope || null,
|
|
@@ -545,17 +595,23 @@ function create(opts) {
|
|
|
545
595
|
* - In **push** mode the body carries the full token-response
|
|
546
596
|
* object; no follow-up call needed.
|
|
547
597
|
*
|
|
598
|
+
* Async because a pushed `id_token` is verified (signature + iss/aud/exp)
|
|
599
|
+
* via the composed inner OAuth client before it is returned — the
|
|
600
|
+
* verified claims are surfaced as `claims`. A present-but-invalid
|
|
601
|
+
* id_token throws `auth-ciba/id-token-invalid` (the notification-token
|
|
602
|
+
* bearer authenticates the caller, never the token itself).
|
|
603
|
+
*
|
|
548
604
|
* @opts
|
|
549
605
|
* { body?: object } // pre-parsed body; defaults to req.body
|
|
550
606
|
*
|
|
551
607
|
* @example
|
|
552
|
-
* app.post("/ciba/notify", function (req, res) {
|
|
553
|
-
* var info = ciba.parseNotification(req, { body: req.body });
|
|
554
|
-
* // → { authReqId, accessToken, idToken, ... }
|
|
608
|
+
* app.post("/ciba/notify", async function (req, res) {
|
|
609
|
+
* var info = await ciba.parseNotification(req, { body: req.body });
|
|
610
|
+
* // → { authReqId, accessToken, idToken, claims, ... }
|
|
555
611
|
* res.statusCode = 204; res.end();
|
|
556
612
|
* });
|
|
557
613
|
*/
|
|
558
|
-
function parseNotification(req, popts) {
|
|
614
|
+
async function parseNotification(req, popts) {
|
|
559
615
|
popts = popts || {};
|
|
560
616
|
if (!req || !req.headers) {
|
|
561
617
|
throw new AuthError("auth-ciba/bad-notification-req",
|
|
@@ -601,10 +657,16 @@ function create(opts) {
|
|
|
601
657
|
throw new AuthError("auth-ciba/no-notification-body",
|
|
602
658
|
"ciba.parseNotification: body required (Buffer/string parsed by middleware)");
|
|
603
659
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
660
|
+
validateOpts.requireNonEmptyString(body.auth_req_id,
|
|
661
|
+
"ciba.parseNotification: auth_req_id", AuthError, "auth-ciba/no-auth-req-id-in-body");
|
|
662
|
+
// Verify the pushed ID token (CIBA §10.2 / OIDC Core MUST) via the
|
|
663
|
+
// composed inner OAuth client before returning it as trusted. The
|
|
664
|
+
// notification-token bearer authenticates the CALLER, not the token; a
|
|
665
|
+
// leaked/observed notification token previously let an attacker inject
|
|
666
|
+
// arbitrary id_token claims the RP would trust on the documented
|
|
667
|
+
// "no follow-up call" push path.
|
|
668
|
+
var pushClaims = await _verifyIdTokenIfPresent(body.id_token,
|
|
669
|
+
"auth-ciba:" + body.auth_req_id, "parseNotification", body.auth_req_id);
|
|
608
670
|
_emitAudit("notification_received", "success", {
|
|
609
671
|
authReqIdHash: sha3Hash("auth-ciba:" + body.auth_req_id),
|
|
610
672
|
mode: deliveryMode,
|
|
@@ -614,6 +676,7 @@ function create(opts) {
|
|
|
614
676
|
authReqId: body.auth_req_id,
|
|
615
677
|
accessToken: body.access_token || null,
|
|
616
678
|
idToken: body.id_token || null,
|
|
679
|
+
claims: pushClaims,
|
|
617
680
|
refreshToken: body.refresh_token || null,
|
|
618
681
|
tokenType: body.token_type || null,
|
|
619
682
|
scope: body.scope || null,
|
package/lib/auth/dpop.js
CHANGED
|
@@ -92,7 +92,7 @@ function thumbprint(key) {
|
|
|
92
92
|
if (!key || typeof key !== "object" || typeof key.kty !== "string" || key.kty.length === 0) {
|
|
93
93
|
throw new AuthError("auth-dpop/bad-jwk", "jwk must be an object with a kty");
|
|
94
94
|
}
|
|
95
|
-
if (!DPOP_KTY
|
|
95
|
+
if (!Object.prototype.hasOwnProperty.call(DPOP_KTY, key.kty)) {
|
|
96
96
|
throw new AuthError("auth-dpop/refused-kty", "jwk.kty='" + key.kty + "' is not allowed (DPoP requires asymmetric kty)");
|
|
97
97
|
}
|
|
98
98
|
// The RFC 7638 thumbprint itself is computed by b.jwk.
|
|
@@ -317,6 +317,15 @@ async function verify(proof, opts) {
|
|
|
317
317
|
catch (_e) { throw new AuthError("auth-dpop/malformed", "header is not valid base64url-JSON"); }
|
|
318
318
|
try { payload = safeJson.parse(_b64urlDecode(parts[1]).toString("utf8")); }
|
|
319
319
|
catch (_e) { throw new AuthError("auth-dpop/malformed", "payload is not valid base64url-JSON"); }
|
|
320
|
+
// safeJson.parse accepts the literal `null` / scalars, so a header segment of
|
|
321
|
+
// base64url("null") would otherwise reach `header.typ` and throw a raw
|
|
322
|
+
// TypeError rather than the typed malformed error. Require JSON objects.
|
|
323
|
+
if (!safeJson.isJsonObject(header)) {
|
|
324
|
+
throw new AuthError("auth-dpop/malformed", "header is not a JSON object");
|
|
325
|
+
}
|
|
326
|
+
if (!safeJson.isJsonObject(payload)) {
|
|
327
|
+
throw new AuthError("auth-dpop/malformed", "payload is not a JSON object");
|
|
328
|
+
}
|
|
320
329
|
|
|
321
330
|
// Header checks
|
|
322
331
|
if (header.typ !== "dpop+jwt") {
|
|
@@ -348,6 +357,11 @@ async function verify(proof, opts) {
|
|
|
348
357
|
"DPoP proof declares 'crit' header — refused");
|
|
349
358
|
}
|
|
350
359
|
|
|
360
|
+
// Bind the declared alg to the embedded JWK's kty/curve before handing
|
|
361
|
+
// the self-asserted key to node:crypto — every other JWS verifier in the
|
|
362
|
+
// framework enforces this (alg-confusion family, CVE-2026-22817 class).
|
|
363
|
+
jwtExternal._assertAlgKtyMatch(header.alg, header.jwk);
|
|
364
|
+
|
|
351
365
|
// Verify signature against the embedded jwk
|
|
352
366
|
var key = _jwkToKeyObject(header.jwk);
|
|
353
367
|
var params = _signParamsForAlg(header.alg);
|
package/lib/auth/fido-mds3.js
CHANGED
|
@@ -43,6 +43,7 @@ var safeJson = require("../safe-json");
|
|
|
43
43
|
var safeBuffer = require("../safe-buffer");
|
|
44
44
|
var lazyRequire = require("../lazy-require");
|
|
45
45
|
var validateOpts = require("../validate-opts");
|
|
46
|
+
var x509Chain = require("../x509-chain");
|
|
46
47
|
var jwtExternal = require("./jwt-external");
|
|
47
48
|
var _wa = require("../vendor/simplewebauthn-server.cjs");
|
|
48
49
|
var { FidoMds3Error } = require("../framework-error");
|
|
@@ -239,7 +240,10 @@ function _validateChain(x5c, rootPems) {
|
|
|
239
240
|
var root;
|
|
240
241
|
try { root = new nodeCrypto.X509Certificate(rootPems[r]); }
|
|
241
242
|
catch (_e) { continue; }
|
|
242
|
-
|
|
243
|
+
// issuerValidlyIssued enforces basicConstraints cA:TRUE on the root in
|
|
244
|
+
// addition to the issuance + signature linkage — a non-CA cert cannot
|
|
245
|
+
// anchor the x5c chain (basicConstraints bypass, CVE-2002-0862 class).
|
|
246
|
+
if (x509Chain.issuerValidlyIssued(root, tail)) {
|
|
243
247
|
anchored = true;
|
|
244
248
|
break;
|
|
245
249
|
}
|
|
@@ -546,24 +550,37 @@ function lookupAaguid(blob, aaguid) {
|
|
|
546
550
|
return null;
|
|
547
551
|
}
|
|
548
552
|
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
553
|
+
// Resolve the CURRENT certified level from the status-report history. FIDO MDS3
|
|
554
|
+
// status reports are chronological; the level is whatever the MOST RECENT
|
|
555
|
+
// certification-status report says — a FIDO_CERTIFIED_L{N}[_PLUS], or 0 when the
|
|
556
|
+
// latest such report is NOT_FIDO_CERTIFIED (a decertification). Taking the
|
|
557
|
+
// highest-ever level instead let a step-up / risk policy that requires L{N}
|
|
558
|
+
// accept an authenticator that was later decertified or downgraded (the
|
|
559
|
+
// certifiedLevel is the policy input, so a stale max is an authenticator-
|
|
560
|
+
// assurance bypass). Ordering is by effectiveDate (ISO YYYY-MM-DD, so lexical ==
|
|
561
|
+
// chronological); ties and missing dates fall back to array order (append
|
|
562
|
+
// order, which the spec defines as chronological).
|
|
552
563
|
function _certifiedLevel(statusReports) {
|
|
553
564
|
if (!Array.isArray(statusReports)) return { level: 0, plus: false };
|
|
554
|
-
var
|
|
565
|
+
var latest = null;
|
|
566
|
+
var latestDate = null;
|
|
555
567
|
for (var i = 0; i < statusReports.length; i++) {
|
|
556
568
|
var sr = statusReports[i];
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if (
|
|
563
|
-
|
|
569
|
+
// Only certification-status reports move the level: a level grant, or its
|
|
570
|
+
// explicit revocation (NOT_FIDO_CERTIFIED). Other statuses (UPDATE_AVAILABLE,
|
|
571
|
+
// REVOKED, …) are handled elsewhere and must not be read as a level. The
|
|
572
|
+
// status length is bounded before the regex test below — FIDO status tokens
|
|
573
|
+
// are short enums; bounding input before any .test() is the convention.
|
|
574
|
+
if (!sr || typeof sr.status !== "string" || sr.status.length > 64) continue;
|
|
575
|
+
if (!CERT_LEVEL_RE.test(sr.status) && sr.status !== "NOT_FIDO_CERTIFIED") continue;
|
|
576
|
+
var d = typeof sr.effectiveDate === "string" ? sr.effectiveDate : "";
|
|
577
|
+
if (latest === null || d >= latestDate) {
|
|
578
|
+
latest = sr; latestDate = d;
|
|
564
579
|
}
|
|
565
580
|
}
|
|
566
|
-
return
|
|
581
|
+
if (!latest || latest.status === "NOT_FIDO_CERTIFIED") return { level: 0, plus: false };
|
|
582
|
+
var m = CERT_LEVEL_RE.exec(latest.status);
|
|
583
|
+
return { level: parseInt(m[1], 10), plus: !!m[2] };
|
|
567
584
|
}
|
|
568
585
|
|
|
569
586
|
/**
|
package/lib/auth/jwt.js
CHANGED
|
@@ -117,7 +117,7 @@ function _toKeyObject(pemOrKey, kind) {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
function _resolveAlgorithm(alg) {
|
|
120
|
-
if (typeof alg !== "string" || !ALGORITHM_TO_NODE
|
|
120
|
+
if (typeof alg !== "string" || !Object.prototype.hasOwnProperty.call(ALGORITHM_TO_NODE, alg)) {
|
|
121
121
|
throw new AuthError("auth-jwt/unsupported-algorithm",
|
|
122
122
|
"algorithm must be one of " + SUPPORTED_ALGORITHMS.join(", ") + " (got: " + alg + ")");
|
|
123
123
|
}
|
|
@@ -193,6 +193,16 @@ function decode(token) {
|
|
|
193
193
|
catch (_e) { throw new AuthError("auth-jwt/malformed", "header is not valid base64url-JSON"); }
|
|
194
194
|
try { payload = safeJson.parse(_b64urlDecode(parts[1])); }
|
|
195
195
|
catch (_e) { throw new AuthError("auth-jwt/malformed", "payload is not valid base64url-JSON"); }
|
|
196
|
+
// The JOSE header and the claims set MUST each be a JSON object. safeJson.parse
|
|
197
|
+
// accepts the literal `null` (a valid JSON document) and scalars/arrays, so a
|
|
198
|
+
// header segment of base64url("null") would otherwise survive to a `.crit` /
|
|
199
|
+
// `.kid` dereference and throw a raw TypeError instead of a typed AuthError.
|
|
200
|
+
if (!safeJson.isJsonObject(header)) {
|
|
201
|
+
throw new AuthError("auth-jwt/malformed", "header is not a JSON object");
|
|
202
|
+
}
|
|
203
|
+
if (!safeJson.isJsonObject(payload)) {
|
|
204
|
+
throw new AuthError("auth-jwt/malformed", "payload is not a JSON object");
|
|
205
|
+
}
|
|
196
206
|
var signature;
|
|
197
207
|
try { signature = _b64urlDecode(parts[2]); }
|
|
198
208
|
catch (_e) { throw new AuthError("auth-jwt/malformed", "signature is not valid base64url"); }
|
|
@@ -222,7 +232,7 @@ async function verify(token, opts) {
|
|
|
222
232
|
// Validate the allowlist itself — typoed entries should surface here,
|
|
223
233
|
// not as silent "every token rejected."
|
|
224
234
|
for (var i = 0; i < allowed.length; i++) {
|
|
225
|
-
if (!ALGORITHM_TO_NODE
|
|
235
|
+
if (!Object.prototype.hasOwnProperty.call(ALGORITHM_TO_NODE, allowed[i])) {
|
|
226
236
|
throw new AuthError("auth-jwt/unsupported-algorithm",
|
|
227
237
|
"opts.algorithms[" + i + "] = '" + allowed[i] + "' is not in the supported list (" +
|
|
228
238
|
SUPPORTED_ALGORITHMS.join(", ") + ")");
|
|
@@ -351,10 +361,16 @@ async function verify(token, opts) {
|
|
|
351
361
|
"token not yet valid: nbf=" + p.nbf + " (now=" + nowSec + ", tolerance=" + tol + "s)");
|
|
352
362
|
}
|
|
353
363
|
|
|
354
|
-
// String-claim assertions
|
|
355
|
-
|
|
364
|
+
// String-claim assertions. `iss` is StringOrURI (RFC 7519 §4.1.1) — a single
|
|
365
|
+
// value, not a list: reject a non-string iss before matching. Routing it
|
|
366
|
+
// through the aud-style any-of _matchClaim let an attacker-influenced
|
|
367
|
+
// multi-issuer array (iss:["evil","trusted"]) satisfy a single-issuer
|
|
368
|
+
// expectation (CVE-2025-30144 / fast-jwt iss-array class) — the same defense
|
|
369
|
+
// jwtExternal.verifyExternal and oauth.verifyIdToken already apply.
|
|
370
|
+
if (opts.issuer !== undefined &&
|
|
371
|
+
(typeof p.iss !== "string" || !_matchClaim(p.iss, opts.issuer, "iss"))) {
|
|
356
372
|
throw new AuthError("auth-jwt/iss-mismatch",
|
|
357
|
-
"iss=
|
|
373
|
+
"iss=" + JSON.stringify(p.iss) + " does not match expected " + JSON.stringify(opts.issuer));
|
|
358
374
|
}
|
|
359
375
|
if (opts.audience !== undefined && !_matchClaim(p.aud, opts.audience, "aud")) {
|
|
360
376
|
throw new AuthError("auth-jwt/aud-mismatch",
|
package/lib/auth/lockout.js
CHANGED
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
*/
|
|
81
81
|
|
|
82
82
|
var C = require("../constants");
|
|
83
|
+
var numericBounds = require("../numeric-bounds");
|
|
83
84
|
var lazyRequire = require("../lazy-require");
|
|
84
85
|
var requestHelpers = require("../request-helpers");
|
|
85
86
|
var validateOpts = require("../validate-opts");
|
|
@@ -119,7 +120,7 @@ function _requireString(name, val) {
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
function _requirePositiveInt(name, val) {
|
|
122
|
-
if (
|
|
123
|
+
if (!numericBounds.isPositiveFiniteInt(val)) {
|
|
123
124
|
throw _err("BAD_OPT", name + ": expected positive integer, got " +
|
|
124
125
|
typeof val + " " + JSON.stringify(val));
|
|
125
126
|
}
|
|
@@ -261,7 +262,22 @@ function create(opts) {
|
|
|
261
262
|
|
|
262
263
|
// ---- Public surface ----
|
|
263
264
|
|
|
264
|
-
|
|
265
|
+
// Per-key serialization of the failure counter (read→increment→write on an
|
|
266
|
+
// async store): concurrent recordFailure calls for the same key would lose
|
|
267
|
+
// updates, letting parallel failures stay under the lockout threshold. A
|
|
268
|
+
// per-key promise chain applies them sequentially in-process.
|
|
269
|
+
var _recordChains = new Map();
|
|
270
|
+
function recordFailure(key, callOpts) {
|
|
271
|
+
var prev = _recordChains.get(key) || Promise.resolve();
|
|
272
|
+
var run = prev.then(function () { return _doRecordFailure(key, callOpts); },
|
|
273
|
+
function () { return _doRecordFailure(key, callOpts); });
|
|
274
|
+
var tail = run.then(function () {}, function () {});
|
|
275
|
+
_recordChains.set(key, tail);
|
|
276
|
+
tail.then(function () { if (_recordChains.get(key) === tail) _recordChains.delete(key); });
|
|
277
|
+
return run;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function _doRecordFailure(key, callOpts) {
|
|
265
281
|
_requireKey(key);
|
|
266
282
|
callOpts = callOpts || {};
|
|
267
283
|
var now = clock();
|
package/lib/auth/oauth.js
CHANGED
|
@@ -1276,9 +1276,15 @@ function create(opts) {
|
|
|
1276
1276
|
// browser session could be replayed without detection. Throw
|
|
1277
1277
|
// loudly so the operator sees the bug at config time, not at
|
|
1278
1278
|
// first-replay-attempt time.
|
|
1279
|
-
|
|
1279
|
+
// Require a NON-EMPTY STRING nonce, not merely a defined one: a falsy
|
|
1280
|
+
// nonce (null / "" — e.g. a session field that was never set) would slip a
|
|
1281
|
+
// strict `=== undefined` guard, and the downstream verifier only checks the
|
|
1282
|
+
// nonce when vopts.nonce is truthy, so the ID-token nonce check would be
|
|
1283
|
+
// silently skipped and a token captured from another session replayed.
|
|
1284
|
+
if (isOidc && eopts.skipNonceCheck !== true &&
|
|
1285
|
+
(typeof eopts.nonce !== "string" || eopts.nonce.length === 0)) {
|
|
1280
1286
|
throw new OAuthError("auth-oauth/no-nonce",
|
|
1281
|
-
"exchangeCode: nonce is required on OIDC flows. Pass the " +
|
|
1287
|
+
"exchangeCode: a non-empty nonce is required on OIDC flows. Pass the " +
|
|
1282
1288
|
"value returned from authorizationUrl() through to exchangeCode " +
|
|
1283
1289
|
"({ code, state, verifier, nonce }). Operators with a deliberate " +
|
|
1284
1290
|
"no-nonce flow must pass `skipNonceCheck: true` (audited reason).");
|
package/lib/auth/passkey.js
CHANGED
|
@@ -385,7 +385,7 @@ function _largeBlobExt(args) {
|
|
|
385
385
|
var SUPPORT = { preferred: 1, required: 1 };
|
|
386
386
|
var modes = 0;
|
|
387
387
|
if (args.support !== undefined) {
|
|
388
|
-
if (!SUPPORT
|
|
388
|
+
if (!Object.prototype.hasOwnProperty.call(SUPPORT, args.support)) {
|
|
389
389
|
throw new AuthError("auth-passkey/bad-largeblob-support",
|
|
390
390
|
"extensions.largeBlob support must be 'preferred' or 'required'");
|
|
391
391
|
}
|