@blamejs/core 0.15.28 → 0.15.29
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/asyncapi.js +13 -13
- package/lib/auth/sd-jwt-vc.js +5 -5
- package/lib/dsr.js +16 -4
- package/lib/openapi.js +13 -13
- package/lib/safe-json.js +56 -0
- package/lib/sandbox.js +18 -6
- package/lib/vex.js +6 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.15.x
|
|
10
10
|
|
|
11
|
+
- v0.15.29 (2026-06-25) — **JSON parsing of operator and untrusted input is routed through `b.safeJson` everywhere, so a `"__proto__"` member is stripped and the parse is size-bounded — `b.openapi.parse`, `b.asyncapi.parse`, and `b.sandbox.run` were using a raw `JSON.parse`.** b.openapi.parse and b.asyncapi.parse parsed an operator-supplied document string with a raw JSON.parse, which re-creates a "__proto__" member as an own key and imposes no size bound; b.sandbox.run parsed the untrusted sandboxed code's serialized result the same way (proto member kept, depth unbounded). All three now route through b.safeJson, which strips the prototype-pollution keys and applies the depth / key / size caps. A new b.safeJson.parseStringOrObject primitive captures the recurring "accept a document as a JSON string OR a pre-built object" shape (a string is parsed safely, an object passes through) that openapi and asyncapi had each hand-rolled, with each consumer's typed error class and a generous document size cap passed as data. Valid input parses identically to before; a malicious or oversized document is now defused rather than accepted. **Added:** *b.safeJson.parseStringOrObject(input, opts?)* — Accepts either a JSON string — parsed through b.safeJson.parse, so the prototype-pollution-key strip and depth / key / size caps apply — or an already-decoded plain object (returned unchanged). For the recurring "operator hands me a document as a JSON string or a pre-built object" surface (b.openapi / b.asyncapi). The consumer's typed error class, error codes, label, and size cap are passed as options, so a raw JSON.parse on operator input cannot be hand-rolled per consumer. **Security:** *Operator and untrusted JSON parses strip __proto__ and bound size (no raw JSON.parse)* — A raw JSON.parse keeps a "__proto__" object member as an own key (a prototype-pollution gadget for any downstream merge) and parses unbounded input. b.openapi.parse and b.asyncapi.parse used it on operator document strings, and b.sandbox.run used it on the untrusted sandboxed code's JSON-serialized result. All now route through b.safeJson — the prototype-pollution keys are stripped and the depth / key / size caps apply (a 16 MiB document cap for openapi/asyncapi, the existing result-bytes cap for the sandbox). Internal JSON parses (the sealed DSR store payloads, the VEX canonical re-format) route through the same primitive too, so the guarantee cannot be bypassed one call site at a time. Valid input is unaffected; a "__proto__"-laden or oversized payload is defused.
|
|
12
|
+
|
|
11
13
|
- v0.15.28 (2026-06-25) — **`b.network.dns` and `b.network.tls` errors now carry a usable terminal-vs-transient signal on `err.permanent`, so a caller's retry loop re-attempts only the failures a retry can fix.** DnsError was declared always-transient and NetworkTlsError always-permanent, so a consumer driving its own retry loop got the wrong signal in both directions: it would retry a permanent DNS failure (a bad host, bad options, an unsupported query, an NXDOMAIN-style no-result, or a caller-shape/config error such as an unconfigured transport or an invalid resolver list) forever, and it would refuse to retry a transient TLS-over-network failure (an ECH connection failure, a handshake timeout, DNS momentarily unavailable). err.permanent now reflects each error's actual transience, derived from its code and failing closed (an unknown code is permanent, so a hopeless target is not retried indefinitely): for DnsError only a failed network round-trip (a lookup timeout, a resolve/reverse query that failed on the wire, a failed DoH/DoT exchange, a DDR discovery that found nothing) is transient — config, input, and environment errors raised before any network work are permanent; for NetworkTlsError only the network-layer ECH failures are transient. TlsTrustError remains always-permanent by design — a trust-verification failure (bad CA, fingerprint mismatch, OCSP not-good, CT violation, an unreachable OCSP responder) must never be silently retried past the trust decision. **Fixed:** *DnsError / NetworkTlsError expose a correct terminal/transient signal; TlsTrustError stays terminal* — b.network.dns's DnsError was always-transient and b.network.tls's NetworkTlsError always-permanent, so err.permanent misled a consumer's retry loop in both directions — retrying a permanent DNS error (bad host / options / unsupported type / no-result) indefinitely, and never retrying a transient TLS network error (ECH connect failure, handshake timeout, DNS unavailable). err.permanent now reflects each error's actual transience by code and fails closed (unknown codes are permanent): DnsError is transient only for a failed network round-trip (dns/lookup-timeout, dns/resolve-failed, dns/reverse-failed, dns/doh-failed, dns/dot-failed, dns/dot-handshake-failed, dns/ddr-not-discovered, dns/system-failed), and permanent otherwise — including the caller-shape / environment errors raised before any network work (dns/transport-unavailable for an unconfigured transport, dns/dnr-no-resolvers for an empty/invalid resolver list, dns/setservers-failed for an invalid address, dns/no-system-resolvers when none are configured). NetworkTlsError is transient only for the network-layer ECH failures (tls/ech-connect-failed, tls/ech-timeout, tls/ech-dns-unavailable), permanent otherwise. TlsTrustError remains always-permanent so a trust-verification failure is never silently retried.
|
|
12
14
|
|
|
13
15
|
- v0.15.27 (2026-06-25) — **Internal test-harness reliability only — the published library is byte-for-byte identical to 0.15.26.** The legacy single-layer smoke files used fixed-duration setTimeout sleeps to wait for asynchronous conditions (a job processed, a lease TTL lapsing, an audit flush completing). On a contended CI runner a fixed sleep is both flake-prone (too short under load) and slow (it always burns the full budget). Those condition-waits are converted to the harness's polling helpers — waitUntil for observable predicates, passiveObserve for deliberate real-time elapses, withTestTimeout for hang guards — which exit early on fast platforms and give contended platforms the full budget. No shipped framework code changed; this release is byte-for-byte identical to 0.15.26 for operators. **Fixed:** *Smoke layer files poll for conditions instead of fixed setTimeout sleeps* — The single-layer smoke files waited on asynchronous conditions with fixed-duration setTimeout sleeps, which flake under SMOKE_PARALLEL load and always burn their full budget. The condition-waits are converted to the polling helpers (waitUntil / passiveObserve / withTestTimeout) so they exit early on fast platforms and stay robust on contended ones; non-wait timers (abort triggers, child/socket watchdogs, simulated-latency mocks) are unchanged. This is test-harness reliability only — no shipped framework behavior changed.
|
package/lib/asyncapi.js
CHANGED
|
@@ -40,6 +40,8 @@ var openapiSecurity = require("./openapi-security");
|
|
|
40
40
|
var openapiYaml = require("./openapi-yaml");
|
|
41
41
|
var bindingsMod = require("./asyncapi-bindings");
|
|
42
42
|
var traitsMod = require("./asyncapi-traits");
|
|
43
|
+
var safeJson = require("./safe-json");
|
|
44
|
+
var C = require("./constants");
|
|
43
45
|
var { defineClass } = require("./framework-error");
|
|
44
46
|
var AsyncApiError = defineClass("AsyncApiError", { alwaysPermanent: true });
|
|
45
47
|
|
|
@@ -470,19 +472,17 @@ function _validateServerEntry(entry, label) {
|
|
|
470
472
|
* bad.errors[0]; // → 'operations.pub.channel: $ref "#/channels/missing" does not resolve to a declared channel'
|
|
471
473
|
*/
|
|
472
474
|
function parse(jsonStringOrObject) {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
"asyncapi.parse: input must be a JSON string or a plain object");
|
|
485
|
-
}
|
|
475
|
+
// A JSON string is parsed through safeJson (proto-pollution-key strip + depth
|
|
476
|
+
// / size caps — a raw JSON.parse here kept a "__proto__" member and was
|
|
477
|
+
// unbounded on an operator-supplied document); a pre-built object passes
|
|
478
|
+
// through. The 16 MiB cap is generous for any real AsyncAPI document.
|
|
479
|
+
var doc = safeJson.parseStringOrObject(jsonStringOrObject, {
|
|
480
|
+
maxBytes: C.BYTES.mib(16),
|
|
481
|
+
errorClass: AsyncApiError,
|
|
482
|
+
jsonCode: "asyncapi/bad-json",
|
|
483
|
+
inputCode: "asyncapi/bad-input",
|
|
484
|
+
label: "asyncapi.parse",
|
|
485
|
+
});
|
|
486
486
|
var errors = [];
|
|
487
487
|
if (typeof doc.asyncapi !== "string") {
|
|
488
488
|
errors.push("missing or non-string `asyncapi` version field (must be 3.0.x)");
|
package/lib/auth/sd-jwt-vc.js
CHANGED
|
@@ -165,8 +165,8 @@ function _verifyJwt(token, publicKey, algorithm) {
|
|
|
165
165
|
var headerStr = _b64uDecodeStr(parts[0]);
|
|
166
166
|
var payloadStr = _b64uDecodeStr(parts[1]);
|
|
167
167
|
return {
|
|
168
|
-
header: safeJson.parse(headerStr, { maxBytes: 64 * 1024 }), // allow:
|
|
169
|
-
payload: safeJson.parse(payloadStr, { maxBytes: 1024 * 1024 }), // allow:
|
|
168
|
+
header: safeJson.parse(headerStr, { maxBytes: 64 * 1024 }), // allow:raw-byte-literal — JWT header cap (64 KB)
|
|
169
|
+
payload: safeJson.parse(payloadStr, { maxBytes: 1024 * 1024 }), // allow:raw-byte-literal — JWT payload cap (1 MB)
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -330,7 +330,7 @@ function present(opts) {
|
|
|
330
330
|
if (_jwtParts.length === 3) {
|
|
331
331
|
try {
|
|
332
332
|
_issuerPayload = safeJson.parse(_b64uDecodeStr(_jwtParts[1]),
|
|
333
|
-
{ maxBytes: 64 * 1024 }); // allow:
|
|
333
|
+
{ maxBytes: 64 * 1024 }); // allow:raw-byte-literal — JWT payload cap (64 KB)
|
|
334
334
|
} catch (_e) { _issuerPayload = null; }
|
|
335
335
|
}
|
|
336
336
|
var _sdAlg = (_issuerPayload && typeof _issuerPayload._sd_alg === "string")
|
|
@@ -448,7 +448,7 @@ async function verify(presentation, opts) {
|
|
|
448
448
|
"verify: JWT must have 3 dot-separated parts");
|
|
449
449
|
}
|
|
450
450
|
var headerObj;
|
|
451
|
-
try { headerObj = safeJson.parse(_b64uDecodeStr(jwtParts[0]), { maxBytes: 64 * 1024 }); } // allow:
|
|
451
|
+
try { headerObj = safeJson.parse(_b64uDecodeStr(jwtParts[0]), { maxBytes: 64 * 1024 }); } // allow:raw-byte-literal — JWT header cap (64 KB)
|
|
452
452
|
catch (e) {
|
|
453
453
|
throw new AuthError("auth-sd-jwt-vc/bad-header",
|
|
454
454
|
"verify: malformed JWT header: " + e.message);
|
|
@@ -622,7 +622,7 @@ async function verify(presentation, opts) {
|
|
|
622
622
|
}
|
|
623
623
|
// Verify KB-JWT signature
|
|
624
624
|
var kbHeaderObj;
|
|
625
|
-
try { kbHeaderObj = safeJson.parse(_b64uDecodeStr(maybeKbJwt.split(".")[0]), { maxBytes: 4096 }); }
|
|
625
|
+
try { kbHeaderObj = safeJson.parse(_b64uDecodeStr(maybeKbJwt.split(".")[0]), { maxBytes: 4096 }); }
|
|
626
626
|
catch (e) {
|
|
627
627
|
throw new AuthError("auth-sd-jwt-vc/bad-kb-header",
|
|
628
628
|
"verify: malformed KB-JWT header: " + e.message);
|
package/lib/dsr.js
CHANGED
|
@@ -114,6 +114,7 @@ var bCrypto = require("./crypto");
|
|
|
114
114
|
var lazyRequire = require("./lazy-require");
|
|
115
115
|
var validateOpts = require("./validate-opts");
|
|
116
116
|
var safeSql = require("./safe-sql");
|
|
117
|
+
var safeJson = require("./safe-json");
|
|
117
118
|
var boundedMap = require("./bounded-map");
|
|
118
119
|
var { defineClass } = require("./framework-error");
|
|
119
120
|
|
|
@@ -1142,12 +1143,23 @@ function dbTicketStore(opts) {
|
|
|
1142
1143
|
// the derived lookup hashes are computed from the plaintext; vault-less
|
|
1143
1144
|
// it stores plaintext (matching the agent-* fallback).
|
|
1144
1145
|
function _sealColumns(id, ticket) {
|
|
1146
|
+
// The payload column is read back through safeJson.parse, whose hard
|
|
1147
|
+
// ceiling (safeJson.ABSOLUTE_MAX_BYTES) caps what any read can accept.
|
|
1148
|
+
// Refuse a ticket whose serialized form exceeds that same ceiling on
|
|
1149
|
+
// write so the store never holds a payload it cannot read back later
|
|
1150
|
+
// (write cap == read cap, measured the same way: UTF-8 byte length).
|
|
1151
|
+
var serializedPayload = JSON.stringify(ticket);
|
|
1152
|
+
if (Buffer.byteLength(serializedPayload, "utf8") > safeJson.ABSOLUTE_MAX_BYTES) {
|
|
1153
|
+
throw new DsrError("dsr/ticket-too-large",
|
|
1154
|
+
"_sealColumns: ticket " + id + " payload exceeds the " +
|
|
1155
|
+
safeJson.ABSOLUTE_MAX_BYTES + "-byte store limit");
|
|
1156
|
+
}
|
|
1145
1157
|
var row = {
|
|
1146
1158
|
id: id,
|
|
1147
1159
|
subject_id: (ticket.subject && ticket.subject.subjectId) || null,
|
|
1148
1160
|
subject_email: (ticket.subject && ticket.subject.email) || null,
|
|
1149
1161
|
subject_phone: (ticket.subject && ticket.subject.phone) || null,
|
|
1150
|
-
payload:
|
|
1162
|
+
payload: serializedPayload,
|
|
1151
1163
|
};
|
|
1152
1164
|
var out = {
|
|
1153
1165
|
$sid: row.subject_id,
|
|
@@ -1252,7 +1264,7 @@ function dbTicketStore(opts) {
|
|
|
1252
1264
|
var rows = db.prepare("SELECT id, payload FROM " + qTable + " WHERE id = $id")
|
|
1253
1265
|
.all({ $id: id });
|
|
1254
1266
|
if (!rows || rows.length === 0) return null;
|
|
1255
|
-
return
|
|
1267
|
+
return safeJson.parse(_unsealPayload(rows[0].payload, rows[0].id), { maxBytes: safeJson.ABSOLUTE_MAX_BYTES });
|
|
1256
1268
|
},
|
|
1257
1269
|
list: async function (filter) {
|
|
1258
1270
|
filter = filter || {};
|
|
@@ -1267,7 +1279,7 @@ function dbTicketStore(opts) {
|
|
|
1267
1279
|
if (conds.length > 0) sql += " WHERE " + conds.join(" AND ");
|
|
1268
1280
|
sql += " ORDER BY submitted_at DESC";
|
|
1269
1281
|
var rows = db.prepare(sql).all(params);
|
|
1270
|
-
return rows.map(function (r) { return
|
|
1282
|
+
return rows.map(function (r) { return safeJson.parse(_unsealPayload(r.payload, r.id), { maxBytes: safeJson.ABSOLUTE_MAX_BYTES }); });
|
|
1271
1283
|
},
|
|
1272
1284
|
update: async function (id, ticket) {
|
|
1273
1285
|
var cols = _sealColumns(id, ticket);
|
|
@@ -1314,7 +1326,7 @@ function dbTicketStore(opts) {
|
|
|
1314
1326
|
var del = db.prepare("DELETE FROM " + qTable + " WHERE id = $id");
|
|
1315
1327
|
for (var i = 0; i < rows.length; i++) {
|
|
1316
1328
|
try {
|
|
1317
|
-
var t =
|
|
1329
|
+
var t = safeJson.parse(_unsealPayload(rows[i].payload, rows[i].id), { maxBytes: safeJson.ABSOLUTE_MAX_BYTES });
|
|
1318
1330
|
if (t.retentionUntil && t.retentionUntil < asOf) {
|
|
1319
1331
|
del.run({ $id: rows[i].id });
|
|
1320
1332
|
purged += 1;
|
package/lib/openapi.js
CHANGED
|
@@ -47,6 +47,8 @@ var schemaWalk = require("./openapi-schema-walk");
|
|
|
47
47
|
var pathsBuilderMod = require("./openapi-paths-builder");
|
|
48
48
|
var openapiSecurity = require("./openapi-security");
|
|
49
49
|
var openapiYaml = require("./openapi-yaml");
|
|
50
|
+
var safeJson = require("./safe-json");
|
|
51
|
+
var C = require("./constants");
|
|
50
52
|
var { defineClass } = require("./framework-error");
|
|
51
53
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
52
54
|
|
|
@@ -520,19 +522,17 @@ function _validateItemOperations(item, label, errors, securitySchemes) {
|
|
|
520
522
|
* bad.errors[0]; // → 'path "users" must start with \'/\''
|
|
521
523
|
*/
|
|
522
524
|
function parse(jsonStringOrObject) {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
"openapi.parse: input must be a JSON string or a plain object");
|
|
535
|
-
}
|
|
525
|
+
// A JSON string is parsed through safeJson (proto-pollution-key strip + depth
|
|
526
|
+
// / size caps — a raw JSON.parse here kept a "__proto__" member and was
|
|
527
|
+
// unbounded on an operator-supplied document); a pre-built object passes
|
|
528
|
+
// through. The 16 MiB cap is generous for any real OpenAPI document.
|
|
529
|
+
var doc = safeJson.parseStringOrObject(jsonStringOrObject, {
|
|
530
|
+
maxBytes: C.BYTES.mib(16),
|
|
531
|
+
errorClass: OpenApiError,
|
|
532
|
+
jsonCode: "openapi/bad-json",
|
|
533
|
+
inputCode: "openapi/bad-input",
|
|
534
|
+
label: "openapi.parse",
|
|
535
|
+
});
|
|
536
536
|
var errors = [];
|
|
537
537
|
if (typeof doc.openapi !== "string") {
|
|
538
538
|
errors.push("missing or non-string `openapi` version field (must be 3.1.x or 3.2.x)");
|
package/lib/safe-json.js
CHANGED
|
@@ -240,6 +240,61 @@ function parseOrDefault(input, fallback, opts) {
|
|
|
240
240
|
catch (_e) { return fallback; }
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* @primitive b.safeJson.parseStringOrObject
|
|
245
|
+
* @signature b.safeJson.parseStringOrObject(input, opts?)
|
|
246
|
+
* @since 0.15.29
|
|
247
|
+
* @status stable
|
|
248
|
+
* @related b.safeJson.parse
|
|
249
|
+
*
|
|
250
|
+
* Accept EITHER a JSON string — parsed through `parse`, so the
|
|
251
|
+
* proto-pollution-key strip, depth/key caps, and size cap all apply — OR an
|
|
252
|
+
* already-decoded plain object (returned unchanged). This is the recurring
|
|
253
|
+
* "operator hands me a document as a JSON string or a pre-built object" surface
|
|
254
|
+
* (b.openapi / b.asyncapi). Routing it here means a raw `JSON.parse` on operator
|
|
255
|
+
* input — which keeps a `"__proto__"` member as an own key and imposes no size
|
|
256
|
+
* bound — cannot be hand-rolled per consumer. The divergence each consumer needs
|
|
257
|
+
* (its typed error class + codes + a generous document size cap) is carried as
|
|
258
|
+
* data, so there is no per-consumer branch.
|
|
259
|
+
*
|
|
260
|
+
* @opts
|
|
261
|
+
* maxBytes: number, // forwarded to parse (default 1 MiB; capped 64 MiB)
|
|
262
|
+
* maxDepth: number, // forwarded to parse
|
|
263
|
+
* maxKeys: number, // forwarded to parse
|
|
264
|
+
* errorClass: function, // typed error class to throw (else SafeJsonError)
|
|
265
|
+
* jsonCode: string, // error code for invalid JSON (used with errorClass)
|
|
266
|
+
* inputCode: string, // error code for a non-string/non-object input
|
|
267
|
+
* label: string, // message prefix (default "safeJson.parseStringOrObject")
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* var doc = b.safeJson.parseStringOrObject(input, {
|
|
271
|
+
* maxBytes: C.BYTES.mib(16), errorClass: OpenApiError,
|
|
272
|
+
* jsonCode: "openapi/bad-json", inputCode: "openapi/bad-input",
|
|
273
|
+
* label: "openapi.parse",
|
|
274
|
+
* });
|
|
275
|
+
*/
|
|
276
|
+
function parseStringOrObject(input, opts) {
|
|
277
|
+
opts = opts || {};
|
|
278
|
+
var label = opts.label || "safeJson.parseStringOrObject";
|
|
279
|
+
if (typeof input === "string") {
|
|
280
|
+
try { return parse(input, opts); }
|
|
281
|
+
catch (e) {
|
|
282
|
+
if (typeof opts.errorClass === "function") {
|
|
283
|
+
throw new opts.errorClass(opts.jsonCode, label + ": invalid JSON — " + (e && e.message));
|
|
284
|
+
}
|
|
285
|
+
throw e;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (input !== null && typeof input === "object" &&
|
|
289
|
+
!Buffer.isBuffer(input) && !(input instanceof Uint8Array)) {
|
|
290
|
+
return input;
|
|
291
|
+
}
|
|
292
|
+
if (typeof opts.errorClass === "function") {
|
|
293
|
+
throw new opts.errorClass(opts.inputCode, label + ": input must be a JSON string or a plain object");
|
|
294
|
+
}
|
|
295
|
+
throw new SafeJsonError(label + ": input must be a JSON string or a plain object", "json/wrong-input-type");
|
|
296
|
+
}
|
|
297
|
+
|
|
243
298
|
function _stripProtoKeys(key, value) {
|
|
244
299
|
if (pick.isPoisonedKey(key)) return undefined;
|
|
245
300
|
return value;
|
|
@@ -997,6 +1052,7 @@ function isJsonObject(value) {
|
|
|
997
1052
|
module.exports = {
|
|
998
1053
|
parse: parse,
|
|
999
1054
|
parseOrDefault: parseOrDefault,
|
|
1055
|
+
parseStringOrObject: parseStringOrObject,
|
|
1000
1056
|
isJsonObject: isJsonObject,
|
|
1001
1057
|
stringify: stringify,
|
|
1002
1058
|
stringifyForScript: stringifyForScript,
|
package/lib/sandbox.js
CHANGED
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
* - Timeout (default: 250ms, max: 10s) terminates the worker.
|
|
32
32
|
* - Heap caps (maxOldGenerationSizeMb / maxYoungGenerationSizeMb)
|
|
33
33
|
* derived from maxBytes; v8 kills the worker on overflow.
|
|
34
|
-
* - Result size cap = maxBytes / 4
|
|
34
|
+
* - Result size cap = min(maxBytes / 4, 64 MiB) - the host re-parses
|
|
35
|
+
* the untrusted result through safeJson, whose hard ceiling bounds
|
|
36
|
+
* it at 64 MiB regardless of a larger maxBytes.
|
|
35
37
|
*
|
|
36
38
|
* Allowed-globals list:
|
|
37
39
|
* The allowed opt names which extra globals operator source may
|
|
@@ -63,7 +65,7 @@
|
|
|
63
65
|
* - sandbox/bad-input - input is not JSON-serializable
|
|
64
66
|
* - sandbox/input-too-large - JSON.stringify(input).length > maxBytes
|
|
65
67
|
* - sandbox/timeout - worker exceeded timeoutMs
|
|
66
|
-
* - sandbox/oversized-result - worker output > maxBytes / 4
|
|
68
|
+
* - sandbox/oversized-result - worker output > min(maxBytes / 4, 64 MiB)
|
|
67
69
|
* - sandbox/parse-error - source did not parse inside the worker
|
|
68
70
|
* - sandbox/runtime-error - operator transform threw
|
|
69
71
|
* - sandbox/spawn-failed - worker thread failed to spawn
|
|
@@ -83,6 +85,7 @@ var lazyRequire = require("./lazy-require");
|
|
|
83
85
|
var validateOpts = require("./validate-opts");
|
|
84
86
|
var numericBounds = require("./numeric-bounds");
|
|
85
87
|
var safeBuffer = require("./safe-buffer");
|
|
88
|
+
var safeJson = require("./safe-json");
|
|
86
89
|
var C = require("./constants");
|
|
87
90
|
var { SandboxError } = require("./framework-error");
|
|
88
91
|
|
|
@@ -236,9 +239,14 @@ function run(opts) {
|
|
|
236
239
|
stackSizeMb: stackMib,
|
|
237
240
|
};
|
|
238
241
|
|
|
239
|
-
// Reserve 1/4 of maxBytes as the per-result hard cap
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
+
// Reserve 1/4 of maxBytes as the per-result hard cap, clamped to the
|
|
243
|
+
// host-side JSON parse ceiling so the worker's output cap and the host's
|
|
244
|
+
// safeJson.parse cap agree exactly. Without the clamp a sandbox with
|
|
245
|
+
// maxBytes over 256 MiB would let the worker emit a result the host then
|
|
246
|
+
// refuses (safeJson hard-caps at ABSOLUTE_MAX_BYTES) — the worker refuses
|
|
247
|
+
// it too, so an oversized untrusted result fails the same way on both
|
|
248
|
+
// sides rather than passing the worker and crossing to a rejecting host.
|
|
249
|
+
var maxResultBytes = Math.min(Math.floor(maxBytes / 4), safeJson.ABSOLUTE_MAX_BYTES);
|
|
242
250
|
|
|
243
251
|
return new Promise(function (resolve, reject) {
|
|
244
252
|
var startedAt = Date.now();
|
|
@@ -312,7 +320,11 @@ function run(opts) {
|
|
|
312
320
|
var peakBytes = (typeof msg.peakBytes === "number") ? msg.peakBytes : 0;
|
|
313
321
|
if (msg.ok) {
|
|
314
322
|
var parsed;
|
|
315
|
-
|
|
323
|
+
// resultJson is the JSON.stringify of UNTRUSTED sandboxed code's return
|
|
324
|
+
// value: parse it through safeJson so a "__proto__" member is stripped
|
|
325
|
+
// and a pathologically deep/large result is bounded (maxResultBytes), not
|
|
326
|
+
// a raw JSON.parse that re-creates the key and is depth-unbounded.
|
|
327
|
+
try { parsed = (msg.resultJson === undefined) ? undefined : safeJson.parse(msg.resultJson, { maxBytes: maxResultBytes }); }
|
|
316
328
|
catch (eParse) {
|
|
317
329
|
_emitAudit("sandbox.run.refused", "failure", {
|
|
318
330
|
reason: "sandbox/bad-result-json", runtimeMs: runtimeMs, peakBytes: peakBytes, sourceBytes: sourceBytes,
|
package/lib/vex.js
CHANGED
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
*/
|
|
49
49
|
|
|
50
50
|
var canonicalJson = require("./canonical-json");
|
|
51
|
+
var safeJson = require("./safe-json");
|
|
52
|
+
var C = require("./constants");
|
|
51
53
|
var validateOpts = require("./validate-opts");
|
|
52
54
|
var { defineClass } = require("./framework-error");
|
|
53
55
|
|
|
@@ -633,7 +635,10 @@ function serialize(doc) {
|
|
|
633
635
|
// of truth for sort/scrub behaviour across the framework (rule
|
|
634
636
|
// §canonicalize).
|
|
635
637
|
var canonical = canonicalJson.stringify(doc);
|
|
636
|
-
|
|
638
|
+
// Re-indent the canonical bytes for human-diffable output. allowProto keeps
|
|
639
|
+
// the canonicalizer's exact key set (this is a faithful re-format of trusted
|
|
640
|
+
// framework output, not a defense boundary); the size cap bounds the parse.
|
|
641
|
+
return JSON.stringify(safeJson.parse(canonical, { allowProto: true, maxBytes: C.BYTES.mib(16) }), null, 2);
|
|
637
642
|
}
|
|
638
643
|
|
|
639
644
|
module.exports = {
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:9d6b0acf-4b88-47bc-b14b-ba2342410a0e",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-06-
|
|
8
|
+
"timestamp": "2026-06-26T09:20:08.812Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.15.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.15.29",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.15.
|
|
25
|
+
"version": "0.15.29",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.15.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.15.29",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.15.
|
|
57
|
+
"ref": "@blamejs/core@0.15.29",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|