@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/auth/status-list.js
CHANGED
|
@@ -78,7 +78,7 @@ var _fromB64url = bCrypto.makeBase64UrlDecoder({
|
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
function _validateBits(bits) {
|
|
81
|
-
if (!SUPPORTED_BIT_SIZES
|
|
81
|
+
if (!Object.prototype.hasOwnProperty.call(SUPPORTED_BIT_SIZES, bits)) {
|
|
82
82
|
throw new StatusListError("status-list/bad-bits",
|
|
83
83
|
"statusList: bits must be 1, 2, 4, or 8 (draft §6.1.1) — got " + bits);
|
|
84
84
|
}
|
|
@@ -249,7 +249,19 @@ async function fromJwt(token, opts) {
|
|
|
249
249
|
list: {
|
|
250
250
|
size: size,
|
|
251
251
|
bits: bits,
|
|
252
|
-
|
|
252
|
+
// Bounds-check the relying-party status read, mirroring create().get. An
|
|
253
|
+
// out-of-range index must FAIL CLOSED (throw): _getAt over-reads the buffer
|
|
254
|
+
// and returns 0 for an out-of-bounds index, and status 0 = VALID — so a
|
|
255
|
+
// credential whose status_list index points past the list would otherwise
|
|
256
|
+
// read as "not revoked", a revocation bypass.
|
|
257
|
+
get: function (idx) {
|
|
258
|
+
if (typeof idx !== "number" || idx < 0 || idx >= size || (idx >> 0) !== idx) {
|
|
259
|
+
throw new StatusListError("status-list/bad-index",
|
|
260
|
+
"statusList.fromJwt get: idx out of range — got " + idx + ", size=" + size +
|
|
261
|
+
" (an out-of-range status index fails closed, never reads as status 0/valid)");
|
|
262
|
+
}
|
|
263
|
+
return _getAt(inflated, bits, idx);
|
|
264
|
+
},
|
|
253
265
|
snapshot: function () { return { size: size, bits: bits, bytes: Buffer.from(inflated) }; },
|
|
254
266
|
},
|
|
255
267
|
claims: claims,
|
package/lib/auth/step-up.js
CHANGED
|
@@ -386,7 +386,15 @@ function parseChallenge(headerValue) {
|
|
|
386
386
|
if (key === "error") out.error = val;
|
|
387
387
|
else if (key === "scope") out.scope = val;
|
|
388
388
|
else if (key === "acr_values") out.acrValues = val.split(/\s+/);
|
|
389
|
-
else if (key === "max_age")
|
|
389
|
+
else if (key === "max_age") {
|
|
390
|
+
// Defensive: a malformed max_age (non-numeric / negative) from the
|
|
391
|
+
// server's challenge must not land as NaN — a downstream `age > maxAge`
|
|
392
|
+
// comparison against NaN is always false and would silently mis-handle
|
|
393
|
+
// the freshness requirement. Omit it unless it parses to a non-negative
|
|
394
|
+
// integer, so callers fall back to their own default.
|
|
395
|
+
var ma = parseInt(val, 10);
|
|
396
|
+
if (isFinite(ma) && ma >= 0) out.maxAge = ma;
|
|
397
|
+
}
|
|
390
398
|
});
|
|
391
399
|
return out;
|
|
392
400
|
}
|
|
@@ -40,6 +40,7 @@ var C = require("./constants");
|
|
|
40
40
|
var lazyRequire = require("./lazy-require");
|
|
41
41
|
var requestHelpers = require("./request-helpers");
|
|
42
42
|
var validateOpts = require("./validate-opts");
|
|
43
|
+
var numericBounds = require("./numeric-bounds");
|
|
43
44
|
var { AuthBotChallengeError } = require("./framework-error");
|
|
44
45
|
|
|
45
46
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
@@ -67,7 +68,7 @@ function _requireFunction(name, val) {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
function _requirePositiveInt(name, val) {
|
|
70
|
-
if (
|
|
71
|
+
if (!numericBounds.isPositiveFiniteInt(val)) {
|
|
71
72
|
throw new AuthBotChallengeError("auth-bot-challenge/bad-opt",
|
|
72
73
|
name + ": expected positive integer, got " + JSON.stringify(val));
|
|
73
74
|
}
|
|
@@ -272,7 +273,25 @@ function create(opts) {
|
|
|
272
273
|
|
|
273
274
|
// ---- Internal staircase advance ----
|
|
274
275
|
|
|
275
|
-
|
|
276
|
+
// Per-key serialization: the failure staircase is a read→increment→write on
|
|
277
|
+
// an async store, so concurrent recordFailure calls for the SAME key would
|
|
278
|
+
// both read N and both write N+1 (lost update), letting an attacker fire
|
|
279
|
+
// parallel failures to stay under the challenge / lockout thresholds. A
|
|
280
|
+
// per-key promise chain applies advances for a key sequentially. (Cross-node
|
|
281
|
+
// atomicity additionally needs an atomic store; this fixes the in-process
|
|
282
|
+
// race the gate actually depends on.)
|
|
283
|
+
var _advanceChains = new Map();
|
|
284
|
+
function _advanceFailure(key, req) {
|
|
285
|
+
var prev = _advanceChains.get(key) || Promise.resolve();
|
|
286
|
+
var run = prev.then(function () { return _doAdvanceFailure(key, req); },
|
|
287
|
+
function () { return _doAdvanceFailure(key, req); });
|
|
288
|
+
var tail = run.then(function () {}, function () {});
|
|
289
|
+
_advanceChains.set(key, tail);
|
|
290
|
+
tail.then(function () { if (_advanceChains.get(key) === tail) _advanceChains.delete(key); });
|
|
291
|
+
return run;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function _doAdvanceFailure(key, req) {
|
|
276
295
|
var now = clock();
|
|
277
296
|
var state = await _readState(key) || {
|
|
278
297
|
stage: STATE_NEW, failures: 0, challengedAt: null, passedAt: null,
|
package/lib/backup/bundle.js
CHANGED
|
@@ -160,14 +160,18 @@ async function create(opts) {
|
|
|
160
160
|
},
|
|
161
161
|
});
|
|
162
162
|
var checksum = bCrypto.checksum(plain);
|
|
163
|
-
|
|
163
|
+
// Bind the ciphertext to this blob's canonical relativePath as AEAD
|
|
164
|
+
// associated data. A blob copied to a different manifest entry (the
|
|
165
|
+
// restore-corruption / blob-remap attack) then fails the Poly1305 tag on
|
|
166
|
+
// restore — tamper-evident even on an unsigned bundle (manifest.aadBound).
|
|
167
|
+
var encResult = await bCrypto.encryptWithFreshSalt(plain, passphrase, entry.relativePath);
|
|
164
168
|
var encPath = _encryptedPathFor(entry.relativePath);
|
|
165
169
|
var destFull = nodePath.join(outDir, encPath);
|
|
166
170
|
atomicFile.ensureDir(nodePath.dirname(destFull));
|
|
167
171
|
atomicFile.writeSync(destFull, encResult.encrypted, { fileMode: 0o600 });
|
|
168
172
|
|
|
169
173
|
var kind = entry.kind || "raw";
|
|
170
|
-
if (!backupManifest.VALID_KINDS
|
|
174
|
+
if (!Object.prototype.hasOwnProperty.call(backupManifest.VALID_KINDS, kind)) {
|
|
171
175
|
throw new BackupBundleError("backup-bundle/bad-kind",
|
|
172
176
|
"create: files[" + i + "].kind must be one of raw, vault-sealed, plaintext (got '" + kind + "')");
|
|
173
177
|
}
|
|
@@ -205,6 +209,7 @@ async function create(opts) {
|
|
|
205
209
|
vaultKeyEnc: wrappedVk.encrypted.toString("base64"),
|
|
206
210
|
files: fileEntries,
|
|
207
211
|
metadata: opts.metadata || undefined,
|
|
212
|
+
aadBound: true, // every blob above was sealed with its relativePath as AEAD AAD
|
|
208
213
|
});
|
|
209
214
|
// Sign the manifest with the audit-sign keypair so a tampered
|
|
210
215
|
// manifest fails verification on restore. The signer is best-
|
package/lib/backup/crypto.js
CHANGED
|
@@ -117,7 +117,21 @@ async function deriveKey(passphrase, saltHex, opts) {
|
|
|
117
117
|
return hash;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
// Normalize optional associated-authenticated-data (AAD) into the
|
|
121
|
+
// Uint8Array the AEAD expects, or undefined when none. AAD is NOT encrypted
|
|
122
|
+
// but IS authenticated: ciphertext sealed under one AAD fails the Poly1305
|
|
123
|
+
// tag when decrypted under a different AAD. Backup file blobs pass their
|
|
124
|
+
// canonical relativePath so a blob remapped to a different manifest entry is
|
|
125
|
+
// cryptographically rejected (the blob-remap / restore-corruption defense).
|
|
126
|
+
function _aadBytes(aad) {
|
|
127
|
+
if (aad === undefined || aad === null) return undefined;
|
|
128
|
+
if (Buffer.isBuffer(aad)) return new Uint8Array(aad);
|
|
129
|
+
if (typeof aad === "string") return new Uint8Array(Buffer.from(aad, "utf8"));
|
|
130
|
+
throw new BackupCryptoError("backup-crypto/bad-aad",
|
|
131
|
+
"associated data must be a Buffer or string");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function encryptWithPassphrase(plaintext, passphrase, saltHex, aad) {
|
|
121
135
|
if (!Buffer.isBuffer(plaintext) && typeof plaintext !== "string") {
|
|
122
136
|
throw new BackupCryptoError("backup-crypto/bad-plaintext",
|
|
123
137
|
"encryptWithPassphrase: plaintext must be a Buffer or string");
|
|
@@ -125,11 +139,11 @@ async function encryptWithPassphrase(plaintext, passphrase, saltHex) {
|
|
|
125
139
|
var plainBuf = Buffer.isBuffer(plaintext) ? plaintext : Buffer.from(plaintext, "utf8");
|
|
126
140
|
var key = await deriveKey(passphrase, saltHex);
|
|
127
141
|
var nonce = nodeCrypto.randomBytes(NONCE_BYTES);
|
|
128
|
-
var ct = xchacha20poly1305(new Uint8Array(key), nonce).encrypt(new Uint8Array(plainBuf));
|
|
142
|
+
var ct = xchacha20poly1305(new Uint8Array(key), nonce, _aadBytes(aad)).encrypt(new Uint8Array(plainBuf));
|
|
129
143
|
return Buffer.concat([nonce, Buffer.from(ct)]);
|
|
130
144
|
}
|
|
131
145
|
|
|
132
|
-
async function decryptWithPassphrase(encrypted, passphrase, saltHex) {
|
|
146
|
+
async function decryptWithPassphrase(encrypted, passphrase, saltHex, aad) {
|
|
133
147
|
if (!Buffer.isBuffer(encrypted)) {
|
|
134
148
|
throw new BackupCryptoError("backup-crypto/bad-input",
|
|
135
149
|
"decryptWithPassphrase: encrypted must be a Buffer");
|
|
@@ -143,11 +157,11 @@ async function decryptWithPassphrase(encrypted, passphrase, saltHex) {
|
|
|
143
157
|
var ct = encrypted.subarray(NONCE_BYTES);
|
|
144
158
|
var plain;
|
|
145
159
|
try {
|
|
146
|
-
plain = xchacha20poly1305(new Uint8Array(key), new Uint8Array(nonce))
|
|
160
|
+
plain = xchacha20poly1305(new Uint8Array(key), new Uint8Array(nonce), _aadBytes(aad))
|
|
147
161
|
.decrypt(new Uint8Array(ct));
|
|
148
162
|
} catch (e) {
|
|
149
163
|
throw new BackupCryptoError("backup-crypto/decrypt-failed",
|
|
150
|
-
"XChaCha20-Poly1305 decryption failed (wrong passphrase or
|
|
164
|
+
"XChaCha20-Poly1305 decryption failed (wrong passphrase, tampered ciphertext, or blob remapped to a different path): " +
|
|
151
165
|
((e && e.message) || String(e)));
|
|
152
166
|
}
|
|
153
167
|
return Buffer.from(plain);
|
|
@@ -155,11 +169,12 @@ async function decryptWithPassphrase(encrypted, passphrase, saltHex) {
|
|
|
155
169
|
|
|
156
170
|
// Convenience for the common "encrypt this with a fresh salt" pattern.
|
|
157
171
|
// Returns the salt as hex so callers can store it alongside the
|
|
158
|
-
// ciphertext in the bundle manifest.
|
|
159
|
-
|
|
172
|
+
// ciphertext in the bundle manifest. `aad` (optional) is bound as AEAD
|
|
173
|
+
// associated data (see _aadBytes).
|
|
174
|
+
async function encryptWithFreshSalt(plaintext, passphrase, aad) {
|
|
160
175
|
var salt = nodeCrypto.randomBytes(SALT_BYTES);
|
|
161
176
|
var saltHex = salt.toString("hex");
|
|
162
|
-
var encrypted = await encryptWithPassphrase(plaintext, passphrase, saltHex);
|
|
177
|
+
var encrypted = await encryptWithPassphrase(plaintext, passphrase, saltHex, aad);
|
|
163
178
|
return { encrypted: encrypted, salt: saltHex };
|
|
164
179
|
}
|
|
165
180
|
|
package/lib/backup/index.js
CHANGED
|
@@ -53,6 +53,7 @@ var os = require("node:os");
|
|
|
53
53
|
var nodePath = require("node:path");
|
|
54
54
|
var bCrypto = require("../crypto");
|
|
55
55
|
var atomicFile = require("../atomic-file");
|
|
56
|
+
var C = require("../constants");
|
|
56
57
|
var backupBundle = require("./bundle");
|
|
57
58
|
var frameworkFiles = require("../framework-files");
|
|
58
59
|
var backupManifest = require("./manifest");
|
|
@@ -721,11 +722,16 @@ function create(opts) {
|
|
|
721
722
|
try {
|
|
722
723
|
await storage.readBundle(bundleId, stagingDir);
|
|
723
724
|
manifestPath = nodePath.join(stagingDir, "manifest.json");
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
725
|
+
// Capped fd-bound read inside the scheduled restore-drill tick: an
|
|
726
|
+
// oversized manifest must not OOM the scheduler worker.
|
|
727
|
+
manifest = backupManifest.parse(atomicFile.fdSafeReadSync(manifestPath, {
|
|
728
|
+
maxBytes: C.BYTES.mib(4), encoding: "utf8",
|
|
729
|
+
errorFor: function (kind) {
|
|
730
|
+
if (kind === "enoent") return new BackupError("backup/test-no-manifest", "manifest.json missing under restored bundle " + bundleId);
|
|
731
|
+
if (kind === "too-large") return new BackupError("backup/test-bad-manifest", "manifest.json too large under restored bundle " + bundleId);
|
|
732
|
+
return new BackupError("backup/test-no-manifest", "manifest.json unreadable under restored bundle " + bundleId + ": " + kind);
|
|
733
|
+
},
|
|
734
|
+
}));
|
|
729
735
|
// Verify the manifest signature so a tampered backup test
|
|
730
736
|
// surfaces here, not as a regulator finding later.
|
|
731
737
|
sigVerification = backupManifest.verifySignature(manifest, {
|
|
@@ -837,11 +843,17 @@ function verifyManifestSignature(target, opts) {
|
|
|
837
843
|
var manifest;
|
|
838
844
|
if (typeof target === "string") {
|
|
839
845
|
var manifestPath = nodePath.join(target, "manifest.json");
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
846
|
+
// Capped fd-bound read OUTSIDE the parse try (so a missing/oversized manifest
|
|
847
|
+
// surfaces backup/no-manifest|bad-manifest, not a generic parse error).
|
|
848
|
+
var manifestRaw = atomicFile.fdSafeReadSync(manifestPath, {
|
|
849
|
+
maxBytes: C.BYTES.mib(4), encoding: "utf8",
|
|
850
|
+
errorFor: function (kind) {
|
|
851
|
+
if (kind === "enoent") return new BackupError("backup/no-manifest", "verifyManifestSignature: manifest.json missing at " + manifestPath);
|
|
852
|
+
if (kind === "too-large") return new BackupError("backup/bad-manifest", "verifyManifestSignature: manifest.json too large");
|
|
853
|
+
return new BackupError("backup/bad-manifest", "verifyManifestSignature: unreadable: " + kind);
|
|
854
|
+
},
|
|
855
|
+
});
|
|
856
|
+
try { manifest = backupManifest.parse(manifestRaw); }
|
|
845
857
|
catch (e) {
|
|
846
858
|
throw new BackupError("backup/bad-manifest",
|
|
847
859
|
"verifyManifestSignature: parse failed: " + ((e && e.message) || String(e)));
|
|
@@ -2187,19 +2199,28 @@ bundleAdapterStorage.fsAdapter = function (fsOpts) {
|
|
|
2187
2199
|
// mode 0o600 matches the v0.12.9 directory-format readback
|
|
2188
2200
|
// discipline — backup payloads carry operator-owned bytes
|
|
2189
2201
|
// (potentially PHI / PCI / GDPR-scoped); owner-only is the
|
|
2190
|
-
// strict posture.
|
|
2191
|
-
//
|
|
2192
|
-
//
|
|
2193
|
-
//
|
|
2194
|
-
//
|
|
2195
|
-
|
|
2202
|
+
// strict posture. Overwrite of an existing key stays allowed
|
|
2203
|
+
// (operators legitimately rewrite the same key, e.g. resuming a
|
|
2204
|
+
// multipart upload); upper layers (writeBundle's `bundle-exists`
|
|
2205
|
+
// check) enforce no-overwrite at the bundle level. writeSync's
|
|
2206
|
+
// atomic rename preserves that overwrite semantic while refusing a
|
|
2207
|
+
// symlink pre-planted at `path` (CWE-59) and never leaving a torn
|
|
2208
|
+
// payload — a bare writeFileSync did both.
|
|
2209
|
+
atomicFile.writeSync(path, bytes, { fileMode: 0o600 });
|
|
2196
2210
|
},
|
|
2197
2211
|
async readFile(key) {
|
|
2198
2212
|
var path = _keyPath(key);
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
return
|
|
2213
|
+
// Capped fd-bound read (no existsSync check-then-read window): fetches a
|
|
2214
|
+
// whole bundle payload, so an oversize/swapped file is an OOM lever. 8 GiB
|
|
2215
|
+
// matches the writeBundle maxBundleBytes ceiling.
|
|
2216
|
+
return atomicFile.fdSafeReadSync(path, {
|
|
2217
|
+
maxBytes: C.BYTES.gib(8),
|
|
2218
|
+
errorFor: function (kind) {
|
|
2219
|
+
if (kind === "enoent") return new BackupError("backup/no-key", "fsAdapter: key not found: " + JSON.stringify(key));
|
|
2220
|
+
if (kind === "too-large") return new BackupError("backup/key-too-large", "fsAdapter: payload for key " + JSON.stringify(key) + " exceeds the read cap");
|
|
2221
|
+
return new BackupError("backup/no-key", "fsAdapter: key " + JSON.stringify(key) + " unreadable: " + kind);
|
|
2222
|
+
},
|
|
2223
|
+
});
|
|
2203
2224
|
},
|
|
2204
2225
|
async listKeys(prefix) {
|
|
2205
2226
|
var out = [];
|
package/lib/backup/manifest.js
CHANGED
|
@@ -140,7 +140,7 @@ function _validateFileEntry(f, idx, errors) {
|
|
|
140
140
|
if (!_isHex(f.salt, true)) {
|
|
141
141
|
errors.push("files[" + idx + "].salt: required hex string");
|
|
142
142
|
}
|
|
143
|
-
if (typeof f.kind !== "string" || !VALID_KINDS
|
|
143
|
+
if (typeof f.kind !== "string" || !Object.prototype.hasOwnProperty.call(VALID_KINDS, f.kind)) {
|
|
144
144
|
errors.push("files[" + idx + "].kind: must be one of raw, vault-sealed, plaintext");
|
|
145
145
|
}
|
|
146
146
|
}
|
|
@@ -239,6 +239,11 @@ function create(opts) {
|
|
|
239
239
|
if (opts.metadata && typeof opts.metadata === "object" && !Array.isArray(opts.metadata)) {
|
|
240
240
|
manifest.metadata = Object.assign({}, opts.metadata);
|
|
241
241
|
}
|
|
242
|
+
// Marks that every file blob was sealed with its relativePath as AEAD
|
|
243
|
+
// associated data (the blob-remap defense). Set on all bundles this version
|
|
244
|
+
// writes; absent on legacy bundles, which restore decrypts without AAD.
|
|
245
|
+
// Carried into the signed payload so it cannot be flipped on a signed bundle.
|
|
246
|
+
if (opts.aadBound === true) manifest.aadBound = true;
|
|
242
247
|
var v = validate(manifest);
|
|
243
248
|
if (!v.ok) {
|
|
244
249
|
throw new BackupManifestError("backup-manifest/invalid",
|
|
@@ -271,6 +276,7 @@ function _canonical(manifest, includeSignature) {
|
|
|
271
276
|
}),
|
|
272
277
|
};
|
|
273
278
|
if (manifest.metadata) canonical.metadata = manifest.metadata;
|
|
279
|
+
if (manifest.aadBound === true) canonical.aadBound = true;
|
|
274
280
|
// Signature block lives alongside the rest of the manifest fields
|
|
275
281
|
// and is itself stable-ordered. Sign-time canonicalization (the
|
|
276
282
|
// bytes the audit-sign keypair signs) excludes the signature field
|
package/lib/break-glass.js
CHANGED
|
@@ -120,10 +120,13 @@ function _appSqlOpts() { return { dialect: clusterStorage.dialect(), quoteName:
|
|
|
120
120
|
// Populated on first access per-table; invalidated on policy.set/delete.
|
|
121
121
|
var policyCache = new Map(); // table -> policy
|
|
122
122
|
var initialized = false;
|
|
123
|
-
// Framework-wide
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
123
|
+
// Framework-wide client-IP resolver (built at init). The grant row's `ip`
|
|
124
|
+
// field is a security control — a grant is pinned to the IP that minted it
|
|
125
|
+
// and re-checked on redeem. X-Forwarded-For is forgeable, so the binding is
|
|
126
|
+
// meaningful only when resolution is peer-gated: operators declare their
|
|
127
|
+
// reverse proxies via init({ trustedProxies }) or own resolution via
|
|
128
|
+
// init({ clientIpResolver }). Default resolves the socket address only.
|
|
129
|
+
var _ipResolver = requestHelpers.trustedClientIp();
|
|
127
130
|
|
|
128
131
|
// Factor lockout — wrap auth.lockout so a hostile actor brute-forcing
|
|
129
132
|
// TOTP codes against break-glass gets shut out after a few failures.
|
|
@@ -457,28 +460,45 @@ async function migrate(table, opts) {
|
|
|
457
460
|
* @related b.breakGlass.policy.set, b.breakGlass.grant
|
|
458
461
|
*
|
|
459
462
|
* One-shot boot wiring. Clears the in-memory policy cache, resets the
|
|
460
|
-
* factor-lockout counter, and records the
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
463
|
+
* factor-lockout counter, and records how the grant row's `ip` field is
|
|
464
|
+
* resolved. That IP is a security binding — the grant pins to it at mint
|
|
465
|
+
* and re-checks it on redeem — so resolution is peer-gated: declare your
|
|
466
|
+
* reverse proxies via `trustedProxies` (CIDRs; X-Forwarded-For honored
|
|
467
|
+
* only from a trusted peer) or own resolution via `clientIpResolver`. A
|
|
468
|
+
* bare `trustProxy` is refused — a forgeable pin is no pin. Operators call
|
|
469
|
+
* this once at boot, before any policy / grant / unseal call — every other
|
|
470
|
+
* primitive throws `breakglass/not-initialized` until init has run.
|
|
466
471
|
*
|
|
467
472
|
* @opts
|
|
468
|
-
*
|
|
473
|
+
* trustedProxies: string|string[], // CIDRs of your reverse proxies — peer-gates X-Forwarded-For
|
|
474
|
+
* clientIpResolver: function(req): string|null, // own grant-IP resolution
|
|
469
475
|
*
|
|
470
476
|
* @example
|
|
471
|
-
* b.breakGlass.init({
|
|
477
|
+
* b.breakGlass.init({ trustedProxies: ["10.0.0.0/8"] });
|
|
472
478
|
* // → undefined (init returns nothing; throws on bad opts)
|
|
473
479
|
*/
|
|
474
480
|
function init(opts) {
|
|
475
481
|
opts = opts || {};
|
|
476
|
-
validateOpts(opts, ["trustProxy"], "breakGlass.init");
|
|
482
|
+
validateOpts(opts, ["trustProxy", "trustedProxies", "clientIpResolver"], "breakGlass.init");
|
|
483
|
+
var resolver;
|
|
484
|
+
try {
|
|
485
|
+
resolver = requestHelpers.trustedClientIp({
|
|
486
|
+
trustedProxies: opts.trustedProxies,
|
|
487
|
+
clientIpResolver: opts.clientIpResolver,
|
|
488
|
+
});
|
|
489
|
+
} catch (e) {
|
|
490
|
+
throw new BreakGlassError("breakglass/bad-opt", e.message);
|
|
491
|
+
}
|
|
492
|
+
if ((opts.trustProxy === true || typeof opts.trustProxy === "number") && !resolver.peerGated) {
|
|
493
|
+
throw new BreakGlassError("breakglass/bad-opt",
|
|
494
|
+
"trustProxy is spoofable — a grant pinned to a forgeable X-Forwarded-For is no " +
|
|
495
|
+
"pin at all. Declare your reverse proxies via trustedProxies: [\"10.0.0.0/8\", …] " +
|
|
496
|
+
"or supply clientIpResolver(req).");
|
|
497
|
+
}
|
|
477
498
|
initialized = true;
|
|
478
499
|
policyCache.clear();
|
|
479
500
|
_factorLockout = null;
|
|
480
|
-
|
|
481
|
-
? opts.trustProxy : false;
|
|
501
|
+
_ipResolver = resolver;
|
|
482
502
|
}
|
|
483
503
|
|
|
484
504
|
function _resetForTest() {
|
|
@@ -490,7 +510,7 @@ function _resetForTest() {
|
|
|
490
510
|
}
|
|
491
511
|
_factorLockout = null;
|
|
492
512
|
_factorLockoutCache = null;
|
|
493
|
-
|
|
513
|
+
_ipResolver = requestHelpers.trustedClientIp();
|
|
494
514
|
}
|
|
495
515
|
|
|
496
516
|
function _requireInit() {
|
|
@@ -1119,11 +1139,10 @@ async function grant(opts) {
|
|
|
1119
1139
|
var nowMs = Date.now();
|
|
1120
1140
|
var grantId = "bg-" + generateToken(GRANT_ID_BYTES);
|
|
1121
1141
|
var sessionId = (opts.req && opts.req.session && opts.req.session.id) || null;
|
|
1122
|
-
//
|
|
1123
|
-
//
|
|
1124
|
-
//
|
|
1125
|
-
|
|
1126
|
-
var ipFromReq = requestHelpers.clientIp(opts.req, { trustProxy: _trustProxy });
|
|
1142
|
+
// Peer-gated client-IP resolution from init() (trustedProxies /
|
|
1143
|
+
// clientIpResolver). Without it, X-Forwarded-For is ignored as
|
|
1144
|
+
// attacker-forgeable and the grant pins to the socket remoteAddress only.
|
|
1145
|
+
var ipFromReq = _ipResolver.resolve(opts.req);
|
|
1127
1146
|
|
|
1128
1147
|
var grantRow = {
|
|
1129
1148
|
_id: grantId,
|
|
@@ -1218,7 +1237,7 @@ function _enforceGrantPins(policy, grantRow, redeemReq, actorFor) {
|
|
|
1218
1237
|
"captured at mint (fail-closed) — re-mint from a request whose client " +
|
|
1219
1238
|
"IP the framework can resolve", true);
|
|
1220
1239
|
}
|
|
1221
|
-
var redeemIp =
|
|
1240
|
+
var redeemIp = _ipResolver.resolve(redeemReq);
|
|
1222
1241
|
if (redeemIp !== grantRow.ip) {
|
|
1223
1242
|
audit.safeEmit({
|
|
1224
1243
|
action: "breakglass.unsealrow",
|
package/lib/cbor.js
CHANGED
|
@@ -55,10 +55,17 @@
|
|
|
55
55
|
|
|
56
56
|
var C = require("./constants");
|
|
57
57
|
var safeBuffer = require("./safe-buffer");
|
|
58
|
+
var boundedMap = require("./bounded-map");
|
|
58
59
|
var { defineClass } = require("./framework-error");
|
|
59
60
|
|
|
60
61
|
var CborError = defineClass("CborError", { alwaysPermanent: true });
|
|
61
62
|
|
|
63
|
+
// Hoisted so the map-decode loop's per-key uniqueness guard doesn't allocate a
|
|
64
|
+
// closure per key (the decode hot path).
|
|
65
|
+
function _throwDuplicateKey() {
|
|
66
|
+
throw new CborError("cbor/duplicate-key", "cbor.decode: duplicate map key (RFC 8949 §5.6)");
|
|
67
|
+
}
|
|
68
|
+
|
|
62
69
|
var DEFAULT_MAX_DEPTH = 64; // nesting depth, not a size
|
|
63
70
|
var ABSOLUTE_MAX_DEPTH = 256; // nesting depth ceiling, not a size
|
|
64
71
|
var DEFAULT_MAX_BYTES = C.BYTES.mib(16);
|
|
@@ -127,6 +134,7 @@ function _encodeFloat(value) {
|
|
|
127
134
|
// exponent must fit the half range and the low 13 mantissa bits must
|
|
128
135
|
// be zero (half has a 10-bit mantissa vs float32's 23).
|
|
129
136
|
function _doubleToHalfBits(value) {
|
|
137
|
+
if (value === 0) return Object.is(value, -0) ? 0x8000 : 0x0000; // ±zero → half zero (sign-preserving); -0 must not fall to the subnormal path
|
|
130
138
|
var fbuf = Buffer.alloc(4);
|
|
131
139
|
fbuf.writeFloatBE(value, 0);
|
|
132
140
|
if (fbuf.readFloatBE(0) !== value) return -1; // not exact in float32 → not in float16
|
|
@@ -177,8 +185,10 @@ function _encodeValue(value, opts) {
|
|
|
177
185
|
// Exact integers within the safe range encode as CBOR integers;
|
|
178
186
|
// an integer-VALUED number beyond 2^53 (e.g. 1e300) has lost
|
|
179
187
|
// integer precision and is a float — encode it as a float (use a
|
|
180
|
-
// bigint for exact 64-bit CBOR integers).
|
|
181
|
-
|
|
188
|
+
// bigint for exact 64-bit CBOR integers). NEGATIVE ZERO is excluded:
|
|
189
|
+
// CBOR integers have no -0, so encoding -0 as the uint 0 would silently
|
|
190
|
+
// drop the sign; it falls through to the float branch (float16 -0.0).
|
|
191
|
+
if (Number.isInteger(value) && !Object.is(value, -0) && Math.abs(value) <= Number.MAX_SAFE_INTEGER) {
|
|
182
192
|
return value >= 0 ? _head(0, value) : _head(1, -1 - value);
|
|
183
193
|
}
|
|
184
194
|
if (!isFinite(value) && !opts.allowNonFinite) {
|
|
@@ -337,7 +347,11 @@ function _need(state, n) {
|
|
|
337
347
|
|
|
338
348
|
function _readArgument(state, ai) {
|
|
339
349
|
// ai is the low-5-bits additional info. Returns the argument as a
|
|
340
|
-
// Number (or BigInt for 8-byte values beyond Number range).
|
|
350
|
+
// Number (or BigInt for 8-byte values beyond Number range). Non-minimal
|
|
351
|
+
// (non-canonical) heads are tolerated here by design — strict canonical
|
|
352
|
+
// enforcement is the opt-in `requireDeterministic` mode (round-trip compare);
|
|
353
|
+
// duplicate keys encoded non-minimally are caught value-based in the map
|
|
354
|
+
// decoder regardless of mode (RFC 8949 §5.6).
|
|
341
355
|
if (ai < CBOR_AI_1BYTE) return ai;
|
|
342
356
|
if (ai === CBOR_AI_1BYTE) { _need(state, 1); var v1 = state.buf[state.pos]; state.pos += 1; return v1; }
|
|
343
357
|
if (ai === 25) { _need(state, 2); var v2 = state.buf.readUInt16BE(state.pos); state.pos += 2; return v2; }
|
|
@@ -410,17 +424,26 @@ function _decodeItem(state, depth) {
|
|
|
410
424
|
case 5: { // map
|
|
411
425
|
var mlen = _lenOf(_readArgument(state, ai));
|
|
412
426
|
var m = new Map();
|
|
413
|
-
|
|
427
|
+
// O(1) duplicate-key detection (RFC 8949 §5.6) keyed on the CANONICAL
|
|
428
|
+
// re-encoding of each decoded key — not its raw input bytes. A per-key
|
|
429
|
+
// Buffer.compare scan over a `seen` array is O(n²), and cbor.decode runs on
|
|
430
|
+
// raw attacker bytes BEFORE any signature check on every COSE / CWT / EAT /
|
|
431
|
+
// mdoc verify path, so a large distinct-key map is an unauthenticated
|
|
432
|
+
// algorithmic-complexity DoS (CWE-407). Keying on the canonical encoding
|
|
433
|
+
// (rather than the input bytes) also closes the bypass where the SAME key
|
|
434
|
+
// value is sent twice with different (e.g. non-minimal) encodings to dodge
|
|
435
|
+
// a byte-wise dup check and silently last-value-win — both copies
|
|
436
|
+
// re-encode identically here and the duplicate is caught regardless of the
|
|
437
|
+
// requireDeterministic mode.
|
|
438
|
+
var seen = new Set();
|
|
414
439
|
for (var j = 0; j < mlen; j++) {
|
|
415
440
|
var keyStart = state.pos;
|
|
416
441
|
var key = _decodeItem(state, depth + 1);
|
|
417
|
-
var
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
seen.push(keyBytes);
|
|
442
|
+
var keyId;
|
|
443
|
+
try { keyId = encode(key).toString("latin1"); }
|
|
444
|
+
catch (_e) { keyId = "raw:" + state.buf.toString("latin1", keyStart, state.pos); }
|
|
445
|
+
boundedMap.requireAbsentMember(seen, keyId, _throwDuplicateKey);
|
|
446
|
+
seen.add(keyId);
|
|
424
447
|
var val = _decodeItem(state, depth + 1);
|
|
425
448
|
m.set(key, val);
|
|
426
449
|
}
|
package/lib/cdn-cache-control.js
CHANGED
|
@@ -376,9 +376,13 @@ function parse(headerValue) {
|
|
|
376
376
|
}
|
|
377
377
|
continue;
|
|
378
378
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
379
|
+
// RFC 9111 delta-seconds is 1*DIGIT — Number() would also accept hex
|
|
380
|
+
// ("0x10"), exponential ("1e3"), and surrounding whitespace, which are
|
|
381
|
+
// not valid cache-directive values. Round-trip parseInt to require pure
|
|
382
|
+
// decimal digits.
|
|
383
|
+
var n = parseInt(val, 10);
|
|
384
|
+
if (Number.isFinite(n) && n >= 0 && String(n) === val) {
|
|
385
|
+
out[_camel(key)] = n;
|
|
382
386
|
}
|
|
383
387
|
continue;
|
|
384
388
|
}
|
package/lib/cert.js
CHANGED
|
@@ -125,7 +125,9 @@ function _createSealedDiskStorage(opts) {
|
|
|
125
125
|
async readSealed(relPath) {
|
|
126
126
|
var p = nodePath.join(rootDir, relPath + ".sealed");
|
|
127
127
|
if (!nodeFs.existsSync(p)) return null;
|
|
128
|
-
|
|
128
|
+
// Cap + fd-bound (sealed cert/key envelope is well under 256 KiB). NO
|
|
129
|
+
// refuseSymlink: the sealed store may be operator-mounted.
|
|
130
|
+
var sealed = atomicFile.fdSafeReadSync(p, { maxBytes: C.BYTES.kib(256) });
|
|
129
131
|
var plain = vaultStore.unseal(sealed);
|
|
130
132
|
return Buffer.isBuffer(plain) ? plain : Buffer.from(plain);
|
|
131
133
|
},
|
|
@@ -139,7 +141,7 @@ function _createSealedDiskStorage(opts) {
|
|
|
139
141
|
async readMeta(certName) {
|
|
140
142
|
var p = nodePath.join(_certDir(certName), "meta.json");
|
|
141
143
|
if (!nodeFs.existsSync(p)) return null;
|
|
142
|
-
try { return safeJson.parse(
|
|
144
|
+
try { return safeJson.parse(atomicFile.fdSafeReadSync(p, { maxBytes: C.BYTES.kib(16), encoding: "utf8" }), { maxBytes: C.BYTES.kib(16) }); }
|
|
143
145
|
catch (e) {
|
|
144
146
|
// meta.json is a derived index (expiry + fingerprint), not a
|
|
145
147
|
// source of truth — the sealed cert is. A corrupt meta must not
|
|
@@ -429,7 +431,7 @@ function create(opts) {
|
|
|
429
431
|
function _loadOrGenerateAccountKey() {
|
|
430
432
|
// Read sealed account JWK; generate + persist if absent.
|
|
431
433
|
var sealedBuf = nodeFs.existsSync(nodePath.join(storage.rootDir, "account/jwk.json.sealed"))
|
|
432
|
-
?
|
|
434
|
+
? atomicFile.fdSafeReadSync(nodePath.join(storage.rootDir, "account/jwk.json.sealed"), { maxBytes: C.BYTES.kib(64) })
|
|
433
435
|
: null;
|
|
434
436
|
if (sealedBuf) {
|
|
435
437
|
var jwk;
|
package/lib/cli.js
CHANGED
|
@@ -39,6 +39,7 @@ var os = require("node:os");
|
|
|
39
39
|
var nodePath = require("node:path");
|
|
40
40
|
var apiSnapshot = require("./api-snapshot");
|
|
41
41
|
var argParser = require("./arg-parser");
|
|
42
|
+
var atomicFile = require("./atomic-file");
|
|
42
43
|
var auditChain = require("./audit-chain");
|
|
43
44
|
var auditTools = require("./audit-tools");
|
|
44
45
|
var backup = require("./backup");
|
|
@@ -1484,7 +1485,10 @@ async function _runMtls(args, ctx) {
|
|
|
1484
1485
|
validityDays: daysP,
|
|
1485
1486
|
});
|
|
1486
1487
|
if (outPath) {
|
|
1487
|
-
|
|
1488
|
+
// Atomic, symlink-refusing write — a bare writeFileSync follows a
|
|
1489
|
+
// symlink an attacker pre-planted at the operator-supplied --out
|
|
1490
|
+
// path (CWE-59) and could expose the client key bundle through it.
|
|
1491
|
+
atomicFile.writeSync(outPath, p12.p12, { fileMode: 0o600 });
|
|
1488
1492
|
report.write("p12 written: " + outPath);
|
|
1489
1493
|
} else {
|
|
1490
1494
|
// No --out: stream the bytes to stdout for piping. Operators
|
package/lib/cloud-events.js
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
var nodeCrypto = require("node:crypto");
|
|
38
|
+
var numericBounds = require("./numeric-bounds");
|
|
38
39
|
var validateOpts = require("./validate-opts");
|
|
39
40
|
var rfc3339 = require("./rfc3339");
|
|
40
41
|
var safeJson = require("./safe-json");
|
|
@@ -265,7 +266,7 @@ function parse(envelope) {
|
|
|
265
266
|
for (var j = 0; j < keys.length; j += 1) {
|
|
266
267
|
var key = keys[j];
|
|
267
268
|
if (REQUIRED_ATTRS.indexOf(key) !== -1) continue;
|
|
268
|
-
if (KNOWN_OPTIONAL_ATTRS
|
|
269
|
+
if (Object.prototype.hasOwnProperty.call(KNOWN_OPTIONAL_ATTRS, key)) continue;
|
|
269
270
|
extensions[key] = envelope[key];
|
|
270
271
|
}
|
|
271
272
|
|
|
@@ -309,7 +310,7 @@ function _extIssue(name, v) {
|
|
|
309
310
|
if (v === null) return null;
|
|
310
311
|
if (typeof v === "string" || typeof v === "boolean") return null;
|
|
311
312
|
if (typeof v === "number") {
|
|
312
|
-
if (!
|
|
313
|
+
if (!numericBounds.isFiniteInt(v)) return "extension '" + name + "' must be an integer (CloudEvents has no float type)";
|
|
313
314
|
if (v < INT_MIN || v > INT_MAX) return "extension '" + name + "' integer out of 32-bit range";
|
|
314
315
|
return null;
|
|
315
316
|
}
|
package/lib/cluster-storage.js
CHANGED
|
@@ -327,9 +327,13 @@ var _activeTx = null;
|
|
|
327
327
|
// synchronous this runs atomically to completion with no interleaving.
|
|
328
328
|
function _localExec(sql, params) {
|
|
329
329
|
var stmt = _localDb().prepare(sql);
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
|
|
330
|
+
// Choose .all() vs .run() by whether the statement returns rows. A leading-
|
|
331
|
+
// keyword check (`^\s*SELECT`) mis-routes a WITH (CTE) read — "WITH c AS (...)
|
|
332
|
+
// SELECT ..." does not start with SELECT — to .run(), which on node:sqlite
|
|
333
|
+
// silently returns only a changes count and DROPS the rows. The shared
|
|
334
|
+
// CTE/EXPLAIN-aware classifier resolves the effective verb (and a top-level
|
|
335
|
+
// RETURNING) so every row-returning statement reaches the caller via .all().
|
|
336
|
+
if (externalDb._statementReturnsRows(sql)) {
|
|
333
337
|
var rows = stmt.all.apply(stmt, params || []);
|
|
334
338
|
return { rows: rows, rowCount: rows.length };
|
|
335
339
|
}
|