@blamejs/core 0.15.13 → 0.15.14
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 +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 +43 -4
- package/lib/auth/dpop.js +6 -1
- package/lib/auth/fido-mds3.js +5 -1
- package/lib/auth/jwt.js +2 -2
- package/lib/auth/lockout.js +18 -2
- package/lib/auth/passkey.js +1 -1
- package/lib/auth/password.js +1 -1
- package/lib/auth/saml.js +33 -13
- 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 +29 -23
- package/lib/crypto.js +18 -4
- package/lib/csp.js +4 -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 +69 -9
- 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 +3 -3
- 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-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/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/queue-local.js +10 -3
- package/lib/redact.js +7 -3
- package/lib/request-helpers.js +201 -23
- 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 +44 -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 +27 -3
- package/lib/sql.js +3 -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 +57 -46
- package/lib/x509-chain.js +44 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/codepoint-class.js
CHANGED
|
@@ -356,9 +356,26 @@ function firstControlCharOffset(s, opts) {
|
|
|
356
356
|
return -1;
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
+
// Decode HTML numeric character references (hex &#x..; and decimal &#..;) just
|
|
360
|
+
// enough to expose a scheme hidden behind entity-encoding. The trailing
|
|
361
|
+
// semicolon is OPTIONAL — a browser decodes `javascript:` (no semicolon)
|
|
362
|
+
// the same as `javascript:`, so a semicolon-required decoder lets the
|
|
363
|
+
// no-semicolon form bypass a scheme allowlist. Shared so guard-html / guard-svg
|
|
364
|
+
// / guard-markdown cannot drift on this (the bug class that shipped one buggy
|
|
365
|
+
// + one correct copy).
|
|
366
|
+
var NUMERIC_ENTITY_RE_G = /&#(?:x([0-9a-f]+)|(\d+));?/gi;
|
|
367
|
+
function decodeNumericEntities(s) {
|
|
368
|
+
return String(s == null ? "" : s).replace(NUMERIC_ENTITY_RE_G, function (m, hex, dec) {
|
|
369
|
+
var cp = hex !== undefined ? parseInt(hex, 16) : parseInt(dec, 10);
|
|
370
|
+
if (!isFinite(cp) || cp < 0 || cp > 0x10FFFF) return m;
|
|
371
|
+
try { return String.fromCodePoint(cp); } catch (_e) { return m; }
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
359
375
|
module.exports = {
|
|
360
376
|
isForbiddenControlChar: isForbiddenControlChar,
|
|
361
377
|
firstControlCharOffset: firstControlCharOffset,
|
|
378
|
+
decodeNumericEntities: decodeNumericEntities,
|
|
362
379
|
isAsciiAlnum: isAsciiAlnum,
|
|
363
380
|
isUnreserved: isUnreserved,
|
|
364
381
|
hex4: hex4,
|
package/lib/compliance-eaa.js
CHANGED
|
@@ -82,7 +82,7 @@ function create(opts) {
|
|
|
82
82
|
throw new ComplianceEaaError("compliance-eaa/bad-decl",
|
|
83
83
|
"compliance.eaa.declareCriterion: decl must be an object with { conformance, note? }");
|
|
84
84
|
}
|
|
85
|
-
if (!VALID_CONFORMANCE
|
|
85
|
+
if (!Object.prototype.hasOwnProperty.call(VALID_CONFORMANCE, decl.conformance)) {
|
|
86
86
|
throw new ComplianceEaaError("compliance-eaa/bad-conformance",
|
|
87
87
|
"compliance.eaa.declareCriterion: conformance must be one of " + Object.keys(VALID_CONFORMANCE).join(", "));
|
|
88
88
|
}
|
|
@@ -368,13 +368,14 @@ function create(opts) {
|
|
|
368
368
|
|
|
369
369
|
for (var c = 0; c < index.length; c++) {
|
|
370
370
|
var candidate = index[c];
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
371
|
+
// A sanctions screen MUST over-match on the legally-operative NAME. The
|
|
372
|
+
// operator-asserted input.type is an unverified counterparty
|
|
373
|
+
// self-classification, NOT data-driven non-match confidence — using it to
|
|
374
|
+
// EXCLUDE a name hit inverts the screen to under-match (a false negative
|
|
375
|
+
// that processes a payment which should have been blocked). So never skip
|
|
376
|
+
// a candidate on type; the name match runs against every record and the
|
|
377
|
+
// type mismatch is surfaced as a non-dispositive `typeMatch` signal on
|
|
378
|
+
// each hit for operator triage.
|
|
378
379
|
|
|
379
380
|
var bestForCandidate = { score: 0, name: "" };
|
|
380
381
|
for (var qi = 0; qi < queryNames.length; qi++) {
|
|
@@ -404,6 +405,7 @@ function create(opts) {
|
|
|
404
405
|
listed: candidate.listedAt,
|
|
405
406
|
programs: candidate.programs,
|
|
406
407
|
type: candidate.type,
|
|
408
|
+
typeMatch: input.type ? candidate.type === input.type : null,
|
|
407
409
|
country: candidate.country,
|
|
408
410
|
});
|
|
409
411
|
}
|
package/lib/compliance.js
CHANGED
|
@@ -953,7 +953,7 @@ var REGIME_MAP = Object.freeze({
|
|
|
953
953
|
* b.compliance.describe("not-a-real-posture"); // → null
|
|
954
954
|
*/
|
|
955
955
|
function describe(posture) {
|
|
956
|
-
return REGIME_MAP[posture]
|
|
956
|
+
return Object.prototype.hasOwnProperty.call(REGIME_MAP, posture) ? REGIME_MAP[posture] : null;
|
|
957
957
|
}
|
|
958
958
|
|
|
959
959
|
// POSTURE_DEFAULTS — per-posture configuration knobs that primitives
|
package/lib/config-drift.js
CHANGED
|
@@ -35,7 +35,6 @@
|
|
|
35
35
|
* @card
|
|
36
36
|
* Monitor + alert when runtime config diverges from a declared baseline.
|
|
37
37
|
*/
|
|
38
|
-
var nodeFs = require("node:fs");
|
|
39
38
|
var nodePath = require("node:path");
|
|
40
39
|
var auditSign = require("./audit-sign");
|
|
41
40
|
var canonicalJson = require("./canonical-json");
|
|
@@ -45,6 +44,7 @@ var safeJson = require("./safe-json");
|
|
|
45
44
|
var validateOpts = require("./validate-opts");
|
|
46
45
|
var { defineClass } = require("./framework-error");
|
|
47
46
|
var atomicFile = require("./atomic-file");
|
|
47
|
+
var C = require("./constants");
|
|
48
48
|
|
|
49
49
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
50
50
|
var auditEmit = require("./audit-emit");
|
|
@@ -163,9 +163,13 @@ function create(opts) {
|
|
|
163
163
|
var _emit = auditEmit.gatedReasonEmitter({ audit: auditOn, sink: auditInstance });
|
|
164
164
|
|
|
165
165
|
function _readSidecar() {
|
|
166
|
-
|
|
166
|
+
// Capped fd-bound read (no existsSync check-then-read window): the signed
|
|
167
|
+
// config-baseline sidecar is parsed + verified, so a tampered multi-GB file
|
|
168
|
+
// would OOM the reader before signature verify. refuseSymlink stays OFF: the
|
|
169
|
+
// data dir may be a symlink-mounted volume (k8s PVC). Any read failure
|
|
170
|
+
// (missing / too-large) → null, the existing "no baseline yet" behavior.
|
|
167
171
|
var raw;
|
|
168
|
-
try { raw =
|
|
172
|
+
try { raw = atomicFile.fdSafeReadSync(sidecarPath, { maxBytes: C.BYTES.mib(1), encoding: "utf8" }); }
|
|
169
173
|
catch (_e) { return null; }
|
|
170
174
|
var parsed;
|
|
171
175
|
try { parsed = safeJson.parse(raw); }
|
|
@@ -190,9 +194,12 @@ function create(opts) {
|
|
|
190
194
|
publicKeyPem: auditSign.getPublicKey(),
|
|
191
195
|
snapshot: snapshot,
|
|
192
196
|
};
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
197
|
+
// Atomic, symlink-refusing write. The previous hand-rolled form staged
|
|
198
|
+
// into a PREDICTABLE temp name (`sidecarPath + ".tmp"`) via a plain
|
|
199
|
+
// writeFileSync, so an attacker could pre-plant a symlink at that exact
|
|
200
|
+
// path and have the signed sidecar written through it (CWE-59 / CWE-377).
|
|
201
|
+
// writeSync uses a CSPRNG temp name opened O_EXCL | O_NOFOLLOW.
|
|
202
|
+
atomicFile.writeSync(sidecarPath, JSON.stringify(payload, null, 2), { fileMode: 0o600 });
|
|
196
203
|
}
|
|
197
204
|
|
|
198
205
|
function _verifySidecar(parsed) {
|
|
@@ -367,7 +374,10 @@ function verifyVendorIntegrity(opts) {
|
|
|
367
374
|
var libVendorDir = opts.libVendorDir || nodePath.join(__dirname, "vendor");
|
|
368
375
|
var manifestPath = opts.manifestPath || nodePath.join(libVendorDir, "MANIFEST.json");
|
|
369
376
|
var raw;
|
|
370
|
-
|
|
377
|
+
// Capped fd-bound read of the vendor MANIFEST.json (operator-bundled, install-
|
|
378
|
+
// time). refuseSymlink OFF — the vendored tree ships read-only and an install
|
|
379
|
+
// may symlink lib/. The cap precedes the alloc.
|
|
380
|
+
try { raw = atomicFile.fdSafeReadSync(manifestPath, { maxBytes: C.BYTES.mib(4), encoding: "utf8" }); }
|
|
371
381
|
catch (_e) {
|
|
372
382
|
throw _err("VENDOR_MANIFEST_MISSING",
|
|
373
383
|
"vendor MANIFEST.json missing at " + manifestPath, true);
|
|
@@ -397,7 +407,11 @@ function verifyVendorIntegrity(opts) {
|
|
|
397
407
|
var abs = nodePath.isAbsolute(rel) ? rel : nodePath.join(libVendorDir, relInVendor);
|
|
398
408
|
var actual;
|
|
399
409
|
try {
|
|
400
|
-
|
|
410
|
+
// Capped fd-bound read (raw bytes — hashing, no encoding). Sanity ceiling
|
|
411
|
+
// so a corrupted/huge vendored file is a read-failed mismatch (caught
|
|
412
|
+
// below) rather than an OOM in the boot integrity loop. NO refuseSymlink
|
|
413
|
+
// (vendored tree ships read-only; installs may symlink lib/).
|
|
414
|
+
var bytes = atomicFile.fdSafeReadSync(abs, { maxBytes: C.BYTES.mib(64) });
|
|
401
415
|
actual = "sha256:" + require("node:crypto")
|
|
402
416
|
.createHash("sha256").update(bytes).digest("hex");
|
|
403
417
|
} catch (_e) {
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
var nodeCrypto = require("node:crypto");
|
|
32
32
|
var C = require("./constants");
|
|
33
33
|
var bCrypto = require("./crypto");
|
|
34
|
+
var x509Chain = require("./x509-chain");
|
|
34
35
|
var canonicalJson = require("./canonical-json");
|
|
35
36
|
var safeJson = require("./safe-json");
|
|
36
37
|
var validateOpts = require("./validate-opts");
|
|
@@ -1103,7 +1104,7 @@ function attachIdentityAssertion(opts) {
|
|
|
1103
1104
|
validateOpts.requireObject(opts, "contentCredentials.attachIdentityAssertion", ContentCredentialsError);
|
|
1104
1105
|
validateOpts(opts, ["binding", "subject", "referencedAssertions", "privateKeyPem", "audit"],
|
|
1105
1106
|
"contentCredentials.attachIdentityAssertion");
|
|
1106
|
-
if (typeof opts.binding !== "string" || !IDENTITY_BINDINGS
|
|
1107
|
+
if (typeof opts.binding !== "string" || !Object.prototype.hasOwnProperty.call(IDENTITY_BINDINGS, opts.binding)) {
|
|
1107
1108
|
throw ContentCredentialsError.factory("content-credentials/bad-identity-binding",
|
|
1108
1109
|
"attachIdentityAssertion: binding must be one of " + Object.keys(IDENTITY_BINDINGS).join(" / "));
|
|
1109
1110
|
}
|
|
@@ -1217,7 +1218,7 @@ function verifyIdentityAssertion(assertion, publicKeyPem, opts) {
|
|
|
1217
1218
|
return _fail("public-key-required");
|
|
1218
1219
|
}
|
|
1219
1220
|
var sp = assertion.signer_payload;
|
|
1220
|
-
if (!sp || typeof sp !== "object" || typeof sp.binding !== "string" || !IDENTITY_BINDINGS
|
|
1221
|
+
if (!sp || typeof sp !== "object" || typeof sp.binding !== "string" || !Object.prototype.hasOwnProperty.call(IDENTITY_BINDINGS, sp.binding) ||
|
|
1221
1222
|
!Array.isArray(sp.referenced_assertions)) {
|
|
1222
1223
|
return _fail("signer-payload-shape");
|
|
1223
1224
|
}
|
|
@@ -1334,9 +1335,10 @@ function _verifyIdentityX509Chain(certChainPem, trustAnchorsPem) {
|
|
|
1334
1335
|
// only direct-root / self-signed leaves.
|
|
1335
1336
|
for (var li = 0; li < certs.length - 1; li += 1) {
|
|
1336
1337
|
var child = certs[li], parent = certs[li + 1];
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1338
|
+
// issuerValidlyIssued enforces basicConstraints cA:TRUE on the parent in
|
|
1339
|
+
// addition to the issuance + signature linkage — a non-CA cert cannot be
|
|
1340
|
+
// an intermediate issuer (basicConstraints bypass, CVE-2002-0862 class).
|
|
1341
|
+
var linked = x509Chain.issuerValidlyIssued(parent, child);
|
|
1340
1342
|
if (!linked) { return { ok: false, reason: "broken-chain" }; }
|
|
1341
1343
|
}
|
|
1342
1344
|
// The top of the presented chain must chain to (or BE) a trust anchor.
|
|
@@ -1347,8 +1349,9 @@ function _verifyIdentityX509Chain(certChainPem, trustAnchorsPem) {
|
|
|
1347
1349
|
if (top.fingerprint256 === anchor.fingerprint256) {
|
|
1348
1350
|
chained = true; // top of the chain IS the anchor (root-in-chain or self-signed leaf == anchor)
|
|
1349
1351
|
} else {
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
+
// issuerValidlyIssued enforces basicConstraints cA:TRUE on the anchor
|
|
1353
|
+
// in addition to issuance + signature (basicConstraints bypass class).
|
|
1354
|
+
chained = x509Chain.issuerValidlyIssued(anchor, top);
|
|
1352
1355
|
}
|
|
1353
1356
|
if (chained) {
|
|
1354
1357
|
if (now < anchor.validFromDate.getTime() || now > anchor.validToDate.getTime()) {
|
|
@@ -1427,7 +1430,7 @@ function cacImplicitLabel(opts) {
|
|
|
1427
1430
|
throw new ContentCredentialsError("cac-implicit-label/bad-content-id",
|
|
1428
1431
|
"cacImplicitLabel: contentId must match [A-Za-z0-9._:/-]");
|
|
1429
1432
|
}
|
|
1430
|
-
if (typeof opts.contentKind !== "string" || !CAC_KIND_ENUM
|
|
1433
|
+
if (typeof opts.contentKind !== "string" || !Object.prototype.hasOwnProperty.call(CAC_KIND_ENUM, opts.contentKind)) {
|
|
1431
1434
|
throw new ContentCredentialsError("cac-implicit-label/bad-content-kind",
|
|
1432
1435
|
"cacImplicitLabel: contentKind must be one of " +
|
|
1433
1436
|
Object.keys(CAC_KIND_ENUM).join("/"));
|
package/lib/content-digest.js
CHANGED
|
@@ -99,9 +99,12 @@ function create(body, opts) {
|
|
|
99
99
|
if (!Array.isArray(algos) || algos.length === 0) throw new ContentDigestError("content-digest/bad-arg", "contentDigest.create: opts.algorithms must be a non-empty array");
|
|
100
100
|
var members = algos.map(function (a) {
|
|
101
101
|
var name = String(a).toLowerCase();
|
|
102
|
-
|
|
102
|
+
// hasOwnProperty: the algorithm name is operator/caller input; a bracket
|
|
103
|
+
// lookup lets "constructor"/"toString" inherit a truthy value off the
|
|
104
|
+
// prototype and pass the support check (proto shadowing).
|
|
105
|
+
var nodeAlg = Object.prototype.hasOwnProperty.call(ACTIVE, name) ? ACTIVE[name] : undefined;
|
|
103
106
|
if (!nodeAlg) {
|
|
104
|
-
if (DEPRECATED
|
|
107
|
+
if (Object.prototype.hasOwnProperty.call(DEPRECATED, name)) throw new ContentDigestError("content-digest/insecure-algorithm", "contentDigest.create: '" + name + "' is a deprecated/insecure digest algorithm (RFC 9530 §6); use sha-256 or sha-512");
|
|
105
108
|
throw new ContentDigestError("content-digest/unsupported-algorithm", "contentDigest.create: unsupported digest algorithm '" + name + "'");
|
|
106
109
|
}
|
|
107
110
|
var digest = nodeCrypto.createHash(nodeAlg).update(bytes).digest("base64");
|
|
@@ -154,8 +157,12 @@ function verify(fieldValue, body, opts) {
|
|
|
154
157
|
var kvp = structuredFields.parseKeyValuePiece(m);
|
|
155
158
|
var name = kvp.key;
|
|
156
159
|
var raw = kvp.value.trim();
|
|
160
|
+
// hasOwnProperty: `name` is the algorithm token from the untrusted inbound
|
|
161
|
+
// Content-Digest header; a bracket lookup lets "constructor"/"__proto__"
|
|
162
|
+
// inherit a truthy value off the prototype and reach createHash (proto
|
|
163
|
+
// shadowing).
|
|
164
|
+
if (!Object.prototype.hasOwnProperty.call(ACTIVE, name)) continue; // ignore legacy / unknown entries
|
|
157
165
|
var nodeAlg = ACTIVE[name];
|
|
158
|
-
if (!nodeAlg) continue; // ignore legacy / unknown entries
|
|
159
166
|
if (raw.length < 2 || raw.charAt(0) !== ":" || raw.charAt(raw.length - 1) !== ":") {
|
|
160
167
|
throw new ContentDigestError("content-digest/bad-field", "contentDigest.verify: '" + name + "' value is not an RFC 8941 byte sequence (:base64:)");
|
|
161
168
|
}
|
|
@@ -172,7 +179,7 @@ function verify(fieldValue, body, opts) {
|
|
|
172
179
|
if (!Array.isArray(opts.required)) throw new ContentDigestError("content-digest/bad-arg", "contentDigest.verify: opts.required must be an array");
|
|
173
180
|
for (var r = 0; r < opts.required.length; r++) {
|
|
174
181
|
var req = String(opts.required[r]).toLowerCase();
|
|
175
|
-
if (!ACTIVE
|
|
182
|
+
if (!Object.prototype.hasOwnProperty.call(ACTIVE, req)) throw new ContentDigestError("content-digest/unsupported-algorithm", "contentDigest.verify: required algorithm '" + req + "' is not a modern digest");
|
|
176
183
|
if (!seen[req]) throw new ContentDigestError("content-digest/missing-algorithm", "contentDigest.verify: required digest '" + req + "' is not present");
|
|
177
184
|
}
|
|
178
185
|
}
|
package/lib/cookies.js
CHANGED
|
@@ -96,7 +96,11 @@ function _validateValue(value) {
|
|
|
96
96
|
// Length cap before the regex test (defense in depth — the regex
|
|
97
97
|
// here is a simple character class, but the same discipline that
|
|
98
98
|
// bounds longer regexes elsewhere applies).
|
|
99
|
-
|
|
99
|
+
// Byte length, not value.length (UTF-16 code units): the cap bounds the
|
|
100
|
+
// on-the-wire Set-Cookie size, and a multibyte value is 2-4x larger in bytes
|
|
101
|
+
// than chars, so a char-count check under-enforces and a 4096-char value can
|
|
102
|
+
// emit a ~12 KiB+ header.
|
|
103
|
+
if (Buffer.byteLength(value, "utf8") > MAX_VALUE_LENGTH || FORBIDDEN_VALUE_RE.test(value)) {
|
|
100
104
|
throw new CookieError("cookies/invalid-value",
|
|
101
105
|
"cookie value is too long or contains forbidden control character (CRLF/NUL/;/,)");
|
|
102
106
|
}
|
|
@@ -108,7 +112,11 @@ function _validateValue(value) {
|
|
|
108
112
|
// header — never trust unscrubbed values reach the wire.
|
|
109
113
|
function _scrubAttr(s) {
|
|
110
114
|
if (typeof s !== "string") return s;
|
|
111
|
-
|
|
115
|
+
// `;` joins Set-Cookie attributes, so an unscrubbed `;` in Path/SameSite is
|
|
116
|
+
// attribute injection (e.g. Path=/x; HttpOnly). Domain is regex-validated
|
|
117
|
+
// upstream; Path/SameSite only flow through here, so strip `;` alongside the
|
|
118
|
+
// CR/LF/NUL header-injection set.
|
|
119
|
+
return s.replace(/[\r\n\0;]/g, ""); // allow:duplicate-regex — CR/LF/NUL(+;) header-injection rejection appears in cookies / mail / security-headers; each is the boundary primitive for its domain
|
|
112
120
|
|
|
113
121
|
}
|
|
114
122
|
|
package/lib/cose.js
CHANGED
|
@@ -110,6 +110,24 @@ function _algParamsFor(algId) {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
// RFC 9053 §2 binds each ECDSA alg to ONE curve. node:crypto will happily
|
|
114
|
+
// verify a sha512 (ES512-shape) signature against a P-256 key as
|
|
115
|
+
// self-consistent, so without this check a COSE_Sign1 declaring ES512 but
|
|
116
|
+
// carrying a P-256 key passes when ES512 is allowlisted — an alg/curve
|
|
117
|
+
// confusion (the COSE sibling of jwt-external._assertAlgKtyMatch). EdDSA /
|
|
118
|
+
// ML-DSA carry their parameters in the KeyObject and need no binding.
|
|
119
|
+
var _ES_ALG_CURVE = { ES256: "prime256v1", ES384: "secp384r1", ES512: "secp521r1" };
|
|
120
|
+
function _assertEcAlgCurve(algName, key) {
|
|
121
|
+
var want = _ES_ALG_CURVE[algName];
|
|
122
|
+
if (want === undefined) return; // not an ECDSA alg — nothing to bind
|
|
123
|
+
var got = (key && key.asymmetricKeyDetails && key.asymmetricKeyDetails.namedCurve) || null;
|
|
124
|
+
if (key.asymmetricKeyType !== "ec" || got !== want) {
|
|
125
|
+
throw new CoseError("cose/alg-curve-mismatch",
|
|
126
|
+
"cose: alg '" + algName + "' requires an EC key on " + want +
|
|
127
|
+
", got " + (key.asymmetricKeyType === "ec" ? got : key.asymmetricKeyType));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
113
131
|
function _bstr(x) {
|
|
114
132
|
return safeBuffer.toBuffer(x, {
|
|
115
133
|
errorFactory: function (code, msg) { return new CoseError(code, msg); },
|
|
@@ -169,6 +187,7 @@ async function sign(payload, opts) {
|
|
|
169
187
|
var algId = ALG_NAME_TO_ID[opts.alg];
|
|
170
188
|
var params = _algParamsFor(algId);
|
|
171
189
|
var key = _toKeyObject(opts.privateKey, "private");
|
|
190
|
+
_assertEcAlgCurve(opts.alg, key); // RFC 9053 alg↔curve binding — refuse e.g. ES512 over a P-256 key
|
|
172
191
|
|
|
173
192
|
var protMap = new Map();
|
|
174
193
|
protMap.set(HDR_ALG, algId);
|
|
@@ -352,6 +371,7 @@ async function verify(coseSign1, opts) {
|
|
|
352
371
|
var key = opts.publicKey
|
|
353
372
|
? _toKeyObject(opts.publicKey, "public")
|
|
354
373
|
: _toKeyObject(opts.keyResolver(protMap, unprotected), "public");
|
|
374
|
+
_assertEcAlgCurve(algName, key); // RFC 9053 alg↔curve binding
|
|
355
375
|
|
|
356
376
|
var externalAad = opts.externalAad == null ? Buffer.alloc(0) : _bstr(opts.externalAad);
|
|
357
377
|
var toBeSigned = _toBeSigned(protectedBstr, externalAad, payload);
|
package/lib/crdt.js
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
var bCrypto = require("./crypto");
|
|
43
43
|
var safeJson = require("./safe-json");
|
|
44
|
+
var numericBounds = require("./numeric-bounds");
|
|
44
45
|
var { defineClass } = require("./framework-error");
|
|
45
46
|
|
|
46
47
|
var CrdtError = defineClass("CrdtError", { alwaysPermanent: true });
|
|
@@ -52,7 +53,7 @@ function _replicaId(opts) {
|
|
|
52
53
|
return id;
|
|
53
54
|
}
|
|
54
55
|
function _posInt(n, label) {
|
|
55
|
-
if (
|
|
56
|
+
if (!numericBounds.isNonNegativeFiniteInt(n)) throw new CrdtError("crdt/bad-value", "crdt: " + label + " must be a non-negative integer");
|
|
56
57
|
return n;
|
|
57
58
|
}
|
|
58
59
|
function _maxMerge(a, b) {
|
package/lib/crypto-field.js
CHANGED
|
@@ -62,7 +62,6 @@ var vaultAad = require("./vault-aad");
|
|
|
62
62
|
var validateOpts = require("./validate-opts");
|
|
63
63
|
var numericBounds = require("./numeric-bounds");
|
|
64
64
|
var safeJson = require("./safe-json");
|
|
65
|
-
var frameworkSchema = require("./framework-schema");
|
|
66
65
|
var sql = require("./sql");
|
|
67
66
|
var { defineClass } = require("./framework-error");
|
|
68
67
|
var { sha3Hash, kdf, generateBytes, encryptPacked, decryptPacked, generateToken } = require("./crypto");
|
|
@@ -226,14 +225,19 @@ var PER_ROW_KEYS_TABLE = "_blamejs_per_row_keys"; // allow:hand-rolled-sql
|
|
|
226
225
|
var PER_ROW_KEYS_COLUMN = "wrappedKey";
|
|
227
226
|
var PER_ROW_KEYS_SCHEMA_VERSION = "1";
|
|
228
227
|
|
|
229
|
-
// The per-row-key registry
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
228
|
+
// The per-row-key registry ALWAYS lives in the LOCAL sqlite — reconcile creates
|
|
229
|
+
// it under its RAW schema name, and per-row keys are never dispatched to an
|
|
230
|
+
// external backend (see _kRowOnce's db() fallback). It is read/written against
|
|
231
|
+
// the local db() / dbHandle directly, NOT through clusterStorage, so it must use
|
|
232
|
+
// the RAW table name. frameworkSchema.tableName() resolves to the cluster-mode
|
|
233
|
+
// EXTERNAL prefixed name (the prefix applies only to the external backend, via
|
|
234
|
+
// resolveTables — local DDL stays raw); under a custom tablePrefix that names a
|
|
235
|
+
// table that does not exist locally, which SILENTLY breaks crypto-shred:
|
|
236
|
+
// destroyPerRowKey deletes 0 rows and sealed cells stay decryptable after
|
|
237
|
+
// eraseHard. quoteName so b.sql emits the quoted identifier the local path expects.
|
|
234
238
|
var _PER_ROW_SQL_OPTS = { dialect: "sqlite", quoteName: true };
|
|
235
239
|
function _perRowKeysTableName() {
|
|
236
|
-
return
|
|
240
|
+
return PER_ROW_KEYS_TABLE;
|
|
237
241
|
}
|
|
238
242
|
|
|
239
243
|
// Build the canonical AAD parts for a row-secret wrap in
|
|
@@ -1271,19 +1275,19 @@ function unsealRow(table, row, actor, dbHandle) {
|
|
|
1271
1275
|
unsealed = out[field];
|
|
1272
1276
|
}
|
|
1273
1277
|
} catch (e) {
|
|
1274
|
-
// A
|
|
1275
|
-
//
|
|
1276
|
-
//
|
|
1277
|
-
//
|
|
1278
|
-
//
|
|
1279
|
-
//
|
|
1280
|
-
//
|
|
1281
|
-
//
|
|
1282
|
-
|
|
1278
|
+
// A crypto-shredded (or never-materialized) per-row key is an EXPECTED
|
|
1279
|
+
// absence, not a decryption-oracle attack: the wrapped secret is gone,
|
|
1280
|
+
// so there is no oracle to brute-force. Reading such a row must read as
|
|
1281
|
+
// "no value" WITHOUT counting against the rate cap — otherwise a bulk
|
|
1282
|
+
// read over a table with many erased rows (GDPR eraseHard) trips the
|
|
1283
|
+
// cap and DoS's the live rows (self-DoS, CWE-307 mis-applied). It is
|
|
1284
|
+
// audited under a distinct, non-failure action so operators don't alert
|
|
1285
|
+
// on routine post-erasure reads as forged-ciphertext bursts.
|
|
1286
|
+
var _shredded = e && e.code === "crypto-field/row-key-unavailable";
|
|
1283
1287
|
try {
|
|
1284
1288
|
audit().safeEmit({
|
|
1285
|
-
action: "system.crypto.unseal_failed",
|
|
1286
|
-
outcome: "failure",
|
|
1289
|
+
action: _shredded ? "system.crypto.shredded_read" : "system.crypto.unseal_failed",
|
|
1290
|
+
outcome: _shredded ? "success" : "failure",
|
|
1287
1291
|
metadata: {
|
|
1288
1292
|
table: table,
|
|
1289
1293
|
field: field,
|
|
@@ -1293,11 +1297,13 @@ function unsealRow(table, row, actor, dbHandle) {
|
|
|
1293
1297
|
},
|
|
1294
1298
|
});
|
|
1295
1299
|
} catch (_e) { /* drop-silent */ }
|
|
1296
|
-
// Default-on rate cap: account
|
|
1297
|
-
//
|
|
1298
|
-
//
|
|
1299
|
-
//
|
|
1300
|
-
|
|
1300
|
+
// Default-on rate cap: account a genuine decryption / AEAD-verify /
|
|
1301
|
+
// AAD-downgrade failure (a possible forged-ciphertext attack) against
|
|
1302
|
+
// the (actor, table, column) tuple. A shredded-key read is exempt (see
|
|
1303
|
+
// above). When the cap trips the threshold, arm the cooldown + emit the
|
|
1304
|
+
// distinct rate-exceeded audit once on the transition. No-op when the
|
|
1305
|
+
// cap is disabled.
|
|
1306
|
+
if (!_shredded && _rateNoteFailure(capActor, table, field)) {
|
|
1301
1307
|
_emitRateAudit({
|
|
1302
1308
|
table: table, field: field, actor: capActor, shape: shape,
|
|
1303
1309
|
threshold: _rateCap.threshold, windowMs: _rateCap.windowMs, cooldownMs: _rateCap.cooldownMs,
|
package/lib/crypto.js
CHANGED
|
@@ -963,7 +963,7 @@ var SRI_ALGORITHMS = { "sha256": "sha256", "sha384": "sha384", "sha512": "sha512
|
|
|
963
963
|
function sri(content, opts) {
|
|
964
964
|
opts = opts || {};
|
|
965
965
|
var algorithm = (opts.algorithm || "sha384").toLowerCase();
|
|
966
|
-
if (!SRI_ALGORITHMS
|
|
966
|
+
if (!Object.prototype.hasOwnProperty.call(SRI_ALGORITHMS, algorithm)) {
|
|
967
967
|
throw new Error("crypto.sri: unsupported algorithm '" + algorithm +
|
|
968
968
|
"' (W3C SRI 1.0 §3.2 supports sha256/sha384/sha512)");
|
|
969
969
|
}
|
|
@@ -1356,7 +1356,21 @@ function decrypt(ciphertext, privateKeys, opts) {
|
|
|
1356
1356
|
opts && opts.raw === true ? { raw: true } : undefined);
|
|
1357
1357
|
}
|
|
1358
1358
|
|
|
1359
|
+
// Bounds-checked 2-byte length read — a bare _envU16(packed, pos) on a
|
|
1360
|
+
// truncated envelope throws a raw Node RangeError that escapes the documented
|
|
1361
|
+
// "Invalid envelope: ..." error contract (untrusted ciphertext must fail with
|
|
1362
|
+
// the typed envelope error, not a stack-trace-leaking RangeError).
|
|
1363
|
+
function _envU16(buf, at) {
|
|
1364
|
+
if (at + 2 > buf.length) {
|
|
1365
|
+
throw new Error("Invalid envelope: truncated (expected a 2-byte length at offset " + at + ")");
|
|
1366
|
+
}
|
|
1367
|
+
return buf.readUInt16BE(at);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1359
1370
|
function decryptEnvelope(packed, privateKeys, internalOpts) {
|
|
1371
|
+
if (!Buffer.isBuffer(packed) || packed.length < 4) {
|
|
1372
|
+
throw new Error("Invalid envelope: too short (need at least the 4-byte suite header)");
|
|
1373
|
+
}
|
|
1360
1374
|
var kemId = packed[1], cipherId = packed[2], kdfId = packed[3], pos = 4;
|
|
1361
1375
|
|
|
1362
1376
|
// The legacy 0xE1 envelope predates the FixedInfo / suite-binding
|
|
@@ -1372,7 +1386,7 @@ function decryptEnvelope(packed, privateKeys, internalOpts) {
|
|
|
1372
1386
|
throw new Error("Invalid envelope: unsupported KDF (only SHAKE256 supported)");
|
|
1373
1387
|
}
|
|
1374
1388
|
|
|
1375
|
-
var kemCtLen = packed
|
|
1389
|
+
var kemCtLen = _envU16(packed, pos); pos += 2;
|
|
1376
1390
|
var kemCt = packed.subarray(pos, pos + kemCtLen); pos += kemCtLen;
|
|
1377
1391
|
|
|
1378
1392
|
var mlkemPriv = nodeCrypto.createPrivateKey(
|
|
@@ -1383,7 +1397,7 @@ function decryptEnvelope(packed, privateKeys, internalOpts) {
|
|
|
1383
1397
|
var fixedInfo = omitFixedInfo ? Buffer.alloc(0) : _suiteFixedInfo(kemId, cipherId, kdfId);
|
|
1384
1398
|
|
|
1385
1399
|
if (kemId === C.KEM_IDS.ML_KEM_1024_P384) {
|
|
1386
|
-
var ecEphLen = packed
|
|
1400
|
+
var ecEphLen = _envU16(packed, pos); pos += 2;
|
|
1387
1401
|
var ecEphDer = packed.subarray(pos, pos + ecEphLen); pos += ecEphLen;
|
|
1388
1402
|
var ecPrivPem = typeof privateKeys === "string" ? null : privateKeys.ecPrivateKey;
|
|
1389
1403
|
if (!ecPrivPem) throw new Error("Hybrid KEM requires EC private key");
|
|
@@ -1400,7 +1414,7 @@ function decryptEnvelope(packed, privateKeys, internalOpts) {
|
|
|
1400
1414
|
// the correct keypair via privateKeys when the envelope was sealed
|
|
1401
1415
|
// with this algorithm. Same length-prefixed shape as the P-384
|
|
1402
1416
|
// hybrid: 2-byte ec-eph-len + DER X25519 pubkey + nonce + ct.
|
|
1403
|
-
var x25519EphLen = packed
|
|
1417
|
+
var x25519EphLen = _envU16(packed, pos); pos += 2;
|
|
1404
1418
|
var x25519EphDer = packed.subarray(pos, pos + x25519EphLen); pos += x25519EphLen;
|
|
1405
1419
|
var x25519PrivPem = typeof privateKeys === "string" ? null : privateKeys.x25519PrivateKey;
|
|
1406
1420
|
if (!x25519PrivPem) throw new Error("ML-KEM-768 + X25519 hybrid envelope requires x25519PrivateKey");
|
package/lib/csp.js
CHANGED
|
@@ -416,6 +416,10 @@ function mergeDirectives(base, additions, opts) {
|
|
|
416
416
|
? directives[name].slice()
|
|
417
417
|
: (directives["default-src"] ? directives["default-src"].slice() : ["'self'"]);
|
|
418
418
|
var added = additions[name];
|
|
419
|
+
// A directive value of 'none' MUST stand alone (CSP3 §2.3.1) — appending a
|
|
420
|
+
// host to it emits the malformed "'none' https://x". When merging real
|
|
421
|
+
// sources in, the added sources supersede 'none', so drop it first.
|
|
422
|
+
if (added.length && existing.length === 1 && existing[0] === "'none'") existing = [];
|
|
419
423
|
for (var si = 0; si < added.length; si += 1) {
|
|
420
424
|
if (existing.indexOf(added[si]) === -1) existing.push(added[si]);
|
|
421
425
|
}
|
package/lib/daemon.js
CHANGED
|
@@ -73,7 +73,10 @@ function _isLivePid(pid) {
|
|
|
73
73
|
|
|
74
74
|
function _readPidFile(pidFile) {
|
|
75
75
|
try {
|
|
76
|
-
|
|
76
|
+
// Same fd-safe + capped + symlink-refusing read as app-shutdown's lockfile
|
|
77
|
+
// reader (one shape): a PID file is never a legit symlink mount, so
|
|
78
|
+
// refuseSymlink is safe; any throw → null ("nothing live there").
|
|
79
|
+
var raw = atomicFile.fdSafeReadSync(pidFile, { maxBytes: C.BYTES.kib(1), refuseSymlink: true, encoding: "utf8" });
|
|
77
80
|
var pid = parseInt(String(raw).trim(), 10);
|
|
78
81
|
return isFinite(pid) && pid > 0 ? pid : null;
|
|
79
82
|
} catch (_e) { return null; }
|
package/lib/data-act.js
CHANGED
|
@@ -89,6 +89,32 @@ var DESIGNATED_GATEKEEPERS = Object.freeze([
|
|
|
89
89
|
"booking",
|
|
90
90
|
]);
|
|
91
91
|
|
|
92
|
+
// Is `recipient` a designated DMA gatekeeper? Match the gatekeeper token as a
|
|
93
|
+
// DNS LABEL of the recipient's host authority — any position, including the
|
|
94
|
+
// FINAL label. A substring `indexOf(g + ".")` test missed a gatekeeper that is
|
|
95
|
+
// the last label (e.g. "blog.google" / "drive.google" on the real ".google"
|
|
96
|
+
// TLD, or "data.amazon"), letting an Art 32 §1 third-party share through — and
|
|
97
|
+
// also false-flagged a non-gatekeeper whose name merely contained the token
|
|
98
|
+
// ("notgoogle.com"). Label matching closes the bypass and drops the false
|
|
99
|
+
// positive. A bare name ("Google") still matches exactly. URL / userinfo / port
|
|
100
|
+
// forms are normalized to the host authority first.
|
|
101
|
+
function _isGatekeeperRecipient(recipient) {
|
|
102
|
+
var s = String(recipient).toLowerCase().trim();
|
|
103
|
+
var scheme = s.indexOf("://");
|
|
104
|
+
if (scheme !== -1) s = s.slice(scheme + 3);
|
|
105
|
+
s = s.split("/")[0].split("?")[0].split("#")[0]; // drop path / query / fragment
|
|
106
|
+
var at = s.lastIndexOf("@");
|
|
107
|
+
if (at !== -1) s = s.slice(at + 1); // drop userinfo
|
|
108
|
+
s = s.split(":")[0]; // drop port
|
|
109
|
+
if (s.length === 0) return false;
|
|
110
|
+
var labels = s.split(".");
|
|
111
|
+
return DESIGNATED_GATEKEEPERS.some(function (g) {
|
|
112
|
+
if (s === g) return true;
|
|
113
|
+
for (var i = 0; i < labels.length; i++) { if (labels[i] === g) return true; }
|
|
114
|
+
return false;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
92
118
|
/**
|
|
93
119
|
* @primitive b.dataAct.declareProduct
|
|
94
120
|
* @signature b.dataAct.declareProduct(opts)
|
|
@@ -229,10 +255,7 @@ function shareWithThirdParty(opts) {
|
|
|
229
255
|
acceptGatekeeper: "optional-plain-object",
|
|
230
256
|
}, "dataAct.shareWithThirdParty", DataActError, "dataact/bad-opts");
|
|
231
257
|
|
|
232
|
-
var
|
|
233
|
-
var isGatekeeper = DESIGNATED_GATEKEEPERS.some(function (g) {
|
|
234
|
-
return recipientLower === g || recipientLower.indexOf(g + ".") !== -1;
|
|
235
|
-
});
|
|
258
|
+
var isGatekeeper = _isGatekeeperRecipient(opts.recipient);
|
|
236
259
|
if (isGatekeeper) {
|
|
237
260
|
if (!opts.acceptGatekeeper || typeof opts.acceptGatekeeper !== "object" ||
|
|
238
261
|
typeof opts.acceptGatekeeper.reason !== "string" ||
|
package/lib/db-file-lifecycle.js
CHANGED
|
@@ -172,8 +172,11 @@ function fileLifecycle(opts) {
|
|
|
172
172
|
: nodePath.join(opts.dataDir, frameworkFiles.fileName("dbKeyEnc"));
|
|
173
173
|
var flushIntervalMs = opts.flushIntervalMs || DEFAULT_FLUSH_INTERVAL_MS;
|
|
174
174
|
var tmpDir = _resolveTmpDir(opts.tmpDir, opts.allowDiskFallback === true);
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
// 0o700 — these hold the DB encryption tmpfs scratch + the data dir
|
|
176
|
+
// (db.key.enc, the decrypted DB while open); a default-mode mkdir leaves
|
|
177
|
+
// them group/other-traversable.
|
|
178
|
+
if (!nodeFs.existsSync(tmpDir)) nodeFs.mkdirSync(tmpDir, { recursive: true, mode: 0o700 });
|
|
179
|
+
if (!nodeFs.existsSync(opts.dataDir)) nodeFs.mkdirSync(opts.dataDir, { recursive: true, mode: 0o700 });
|
|
177
180
|
|
|
178
181
|
var dbPath = null;
|
|
179
182
|
var encKey = null;
|
|
@@ -183,7 +186,9 @@ function fileLifecycle(opts) {
|
|
|
183
186
|
function _loadOrGenerateKey() {
|
|
184
187
|
if (encKey) return encKey;
|
|
185
188
|
if (nodeFs.existsSync(keyPath)) {
|
|
186
|
-
|
|
189
|
+
// Cap + fd-bound. NO refuseSymlink: dbKeyPath may be an operator-absolute
|
|
190
|
+
// KMS-fronted volume that legitimately symlinks the sealed key.
|
|
191
|
+
var sealedKey = atomicFile.fdSafeReadSync(keyPath, { maxBytes: C.BYTES.kib(64), encoding: "utf8" });
|
|
187
192
|
var keyB64;
|
|
188
193
|
try { keyB64 = opts.vault.unseal(sealedKey); }
|
|
189
194
|
catch (e) {
|
|
@@ -212,7 +217,10 @@ function fileLifecycle(opts) {
|
|
|
212
217
|
dbPath = nodePath.join(tmpDir, "blamejs-fl-" + label + "-" +
|
|
213
218
|
generateToken(TMP_NAME_BYTES) + ".db");
|
|
214
219
|
if (nodeFs.existsSync(encPath)) {
|
|
215
|
-
|
|
220
|
+
// Cap + fd-bound (deployment-tunable ceiling). NO refuseSymlink: the data
|
|
221
|
+
// dir may be a symlink-mounted volume (k8s PVC). db.enc is AEAD-checked
|
|
222
|
+
// downstream, so the cap is the OOM-before-cap defense.
|
|
223
|
+
var packed = atomicFile.fdSafeReadSync(encPath, { maxBytes: (opts.maxDbBytes) || C.BYTES.gib(2) });
|
|
216
224
|
if (packed.length < 26) { // minimum envelope length
|
|
217
225
|
throw new DbFileLifecycleError("db-file-lifecycle/short-envelope",
|
|
218
226
|
"fileLifecycle: " + encPath + " too short to be a valid envelope (" + packed.length + " bytes)");
|
|
@@ -248,7 +256,7 @@ function fileLifecycle(opts) {
|
|
|
248
256
|
catch (_e) { /* best-effort — operators on read-only handles or pre-init still flush */ }
|
|
249
257
|
}
|
|
250
258
|
if (!nodeFs.existsSync(dbPath)) return null;
|
|
251
|
-
var plain =
|
|
259
|
+
var plain = atomicFile.fdSafeReadSync(dbPath, { maxBytes: (opts.maxDbBytes) || C.BYTES.gib(2) });
|
|
252
260
|
var packed = encryptPacked(plain, encKey, _aad(opts.dataDir, label));
|
|
253
261
|
atomicFile.writeSync(encPath, packed);
|
|
254
262
|
_emitAudit("flushed", "success", { label: label, bytes: plain.length });
|
|
@@ -269,7 +277,7 @@ function fileLifecycle(opts) {
|
|
|
269
277
|
throw new DbFileLifecycleError("db-file-lifecycle/no-source",
|
|
270
278
|
"fileLifecycle.snapshot: " + dbPath + " is missing");
|
|
271
279
|
}
|
|
272
|
-
var plain =
|
|
280
|
+
var plain = atomicFile.fdSafeReadSync(dbPath, { maxBytes: (opts.maxDbBytes) || C.BYTES.gib(2) });
|
|
273
281
|
return encryptPacked(plain, encKey, _aad(opts.dataDir, label));
|
|
274
282
|
}
|
|
275
283
|
|