@blamejs/core 0.16.17 → 0.16.18
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/archive-tar.js +12 -2
- package/lib/auth/fido-mds3.js +58 -47
- package/lib/guard-graphql.js +30 -11
- package/lib/self-update.js +20 -0
- 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.16.x
|
|
10
10
|
|
|
11
|
+
- v0.16.18 (2026-07-12) — **Refuse a stale FIDO metadata BLOB on the operator-fetch path, harden the GraphQL DoS-shape guard against escaped strings and malformed queries, parse POSIX leading-space octal in tar headers, and fix the FIDO certified-level, self-update 304, and swap-maxBytes paths — surfaced by covering previously-untested error and adversarial branches.** Covering untested error and adversarial branches across four security-relevant primitives surfaced seven defects, fixed at the root. The most serious: b.auth.fidoMds3.fetch() reimplemented the metadata-BLOB validation inline and omitted the stale-BLOB refusal the internal path enforced — so a signed-but-expired BLOB (nextUpdate in the past) was accepted and cached, letting an attacker who serves an ancient correctly-signed BLOB freeze an operator's revoked/compromised-authenticator list at a time of their choosing. Both paths now route through one _verifyBlobWithRoots source of truth, so no check can be present on one and missing on the other. The GraphQL query-shape guard (the pre-schema depth/alias DoS walker) miscounted on a string literal containing an escaped quote and on a brace-unbalanced malformed query, weakening the depth/alias limits it exists to enforce. The FIDO certified-level resolver lexically compared status reports and let an undated later report (a decertification or downgrade) lose to an earlier dated one, so a now-decertified authenticator could still read as certified. A tar reader misparsed POSIX leading-space-padded octal header fields to 0 (a size misparse desynced the block walker and rejected archives other tars extract). self-update's documented 304 If-None-Match fast-path was dead (it threw on every unchanged conditional poll), and selfUpdate.swap read an opts.maxBytes it never declared, so it could refuse a large binary that selfUpdate.verify accepted. **Fixed:** *b.auth.fidoMds3 certified-level honors an undated later decertification or downgrade* — _certifiedLevel compared status reports lexically and coerced a missing effectiveDate to an empty string, which sorts before any real date — so a later report with no effectiveDate (a NOT_FIDO_CERTIFIED decertification, or a downgrade/upgrade) always lost to an earlier dated grant, and certifiedLevel froze at the stale, higher historical value. A step-up policy reading certifiedLevel >= N could therefore accept a now-decertified authenticator. Reports without a comparable date now fall back to array order (append order = chronological), so a later decertification/downgrade wins. · *b.archive.read.tar parses POSIX leading-space-padded octal header fields* — A tar numeric header field (mode / uid / gid / size / mtime / checksum) that is left-padded with ASCII spaces — POSIX-legal, and emitted by star / BSD / Java / Perl tars — was misparsed to 0 because the octal reader treated a leading space as a field terminator. A misparsed size desynced the 512-byte block walker (it read the file body as the next header) and rejected an archive other tars extract cleanly. The reader now skips leading-space padding before reading octal digits. · *b.selfUpdate.poll returns a no-update result on a 304 Not Modified* — The documented If-None-Match / ETag fast-path was unreachable: poll delegated HTTP status handling to the HTTP client, which rejects every non-2xx (304 included) as an error before poll could inspect the status. A conditional poll that correctly received a 304 threw selfupdate/poll-failed instead of returning { available: false, statusCode: 304 }. The 304 (and the non-2xx) branch is now reached, so ETag-conditional polling works as documented. · *b.selfUpdate.swap accepts the maxBytes it re-reads under* — swap re-reads the newly-installed bytes to re-hash them (closing the verify→swap window) under an opts.maxBytes cap, but its option schema never declared maxBytes — so a caller who passed it was refused with selfupdate/bad-opts and swap always used the fixed 1 GiB default, which could refuse a large binary that selfUpdate.verify (which does declare maxBytes) accepted. swap now declares maxBytes (validated like verify's), so the two caps match; rollback, which re-reads nothing, still does not accept it. **Security:** *b.auth.fidoMds3.fetch refuses a stale (expired) metadata BLOB* — The operator-facing fetch path (which trusts caCertificate roots) reimplemented the metadata-BLOB parse and verify inline and omitted the stale-BLOB refusal that the default-roots path enforced, so a BLOB whose nextUpdate is already in the past was accepted and cached even though FIDO MDS3 §3.1.7 says it must not be trusted. An attacker serving an ancient, correctly-signed BLOB could pin an operator to a revoked/compromised-authenticator list frozen at that time. The JWS + x5c-chain verify, payload-shape checks, and stale-BLOB refusal now live in one _verifyBlobWithRoots helper that both the operator-fetch and default-roots paths route through, so the checks can never drift apart again. · *b.guardGraphql hardens the pre-schema DoS-shape walker against escaped strings and malformed queries* — The query-shape walker that enforces depth and alias limits before the query reaches a schema miscounted in two ways: it treated an escaped quote inside a valid GraphQL string literal as the string's terminator (desyncing its in-string state), and it popped its depth-indexed alias counter on every closing brace even for a brace-unbalanced malformed query (underflowing the counter). Both weakened the depth/alias caps the guard exists to enforce against a DoS-shaped query. The walker now tracks string escapes correctly and guards the alias-counter stack against imbalance.
|
|
12
|
+
|
|
11
13
|
- v0.16.17 (2026-07-12) — **Reject INI float literals that overflow to Infinity, return a verdict (not an exception) when a reverse-DNS lookup faults, wrap an async redirect-hook rejection like a sync throw, and refuse a fractional --max-rows — four defects surfaced by covering previously-untested error branches.** Covering previously-untested error and adversarial branches across four primitives surfaced four genuine defects, each fixed at the root. b.parsers.ini.parse coerced a value like `x = 1e999` straight to ±Infinity — its integer and hex branches already reject out-of-range numbers, but the float branch had no finiteness guard, so an overflowing float slipped through and could poison a downstream size cap or timeout; it is now rejected with ini/value-out-of-range. b.mail.iprev.verify threw an unhandled exception when the forward-confirm DNS lookup returned an error code outside the handful it enumerated (EREFUSED / ENOTIMP / …), even though its reverse-lookup path and every sibling (SPF/DKIM/DMARC/ARC) return a verdict for such faults; it now returns a temperror verdict, and — like those siblings — accepts an operator opts.dnsLookup resolver so the confirm path is resolvable offline. b.httpClient wrapped a synchronous onRedirect hook throw into a REDIRECT_ABORTED error but let an async hook rejection escape unwrapped; both now abort the redirect identically. And the blamejs audit verify-chain --max-rows flag accepted a fractional value (2.5), which truncated the chain walk mid-row and reported a nonsensical fractional count; it now requires a whole positive integer, matching its own error message. **Fixed:** *b.parsers.ini.parse rejects an overflowing float instead of coercing to Infinity* — A float literal that exceeds the representable range (e.g. `x = 1e999`) coerced to ±Infinity. The integer and hex coercion branches already reject out-of-range numbers via Number.isSafeInteger, but the float branch returned Number(raw) with no finiteness check — so an Infinity could flow into a downstream size cap or timeout, a denial-of-service vector. The float branch now rejects a non-finite result with ini/value-out-of-range; a large-but-finite float (1e308) and underflow (1e-999 → 0) still parse. · *b.mail.iprev.verify returns a temperror verdict on an un-enumerated reverse/forward DNS fault* — The forward-confirm DNS lookup's error handler enumerated a few transient codes and threw for anything else, so a resolver returning EREFUSED / ENOTIMP / EBADRESP produced an unhandled exception from the public API rather than a verdict. The reverse-lookup path and every sibling result type (SPF / DKIM / DMARC / ARC) return a verdict for a DNS-derived fault; the forward path now does too (temperror). iprev.verify also gains an operator opts.dnsLookup resolver, matching the dnsLookup contract the other types already honor, so the forward-confirm path is resolvable offline. · *b.httpClient aborts a redirect on an async onRedirect hook rejection* — A synchronous throw from the onRedirect hook was wrapped into a REDIRECT_ABORTED error, but an async hook that rejected let the rejection escape unwrapped — inconsistent handling for the same operator control point. An async onRedirect rejection now aborts the redirect with REDIRECT_ABORTED, identical to the synchronous throw. · *blamejs audit verify-chain --max-rows requires a whole positive integer* — The --max-rows flag validated only that the value was finite and >= 1, so a fractional value (2.5) was accepted and passed to the chain walk, where it truncated the verification mid-row and reported a fractional rowsVerified count — despite the flag's own error message promising a positive integer. It now rejects a non-integer value, matching the sibling --steps flag.
|
|
12
14
|
|
|
13
15
|
- v0.16.16 (2026-07-12) — **Build the data-subject-request ticket store's SQL through the shared b.sql query builder instead of hand-assembled statements, and add a static check that keeps db-handle primitives composing b.sql.** A maintainability change with no behavior difference for operators. The b.dsr ticket store built its reads and writes by concatenating table and column names into SQL strings passed to db.prepare, re-implementing the identifier quoting and sealed-field handling that b.sql — the same builder b.db.from() uses — already provides. That hand-rolled shape is how b.tenant.quota's storage query drifted from the query builder and accrued a run of parity defects fixed in 0.16.15 (reserved-word names, schema-qualified names, sealed-column filtering). The store's DML now composes b.sql (its schema DDL, which is not a b.sql concern, stays as direct statements), so its SQL cannot diverge from the builder. A new codebase-patterns check flags any db-handle primitive that passes an inline SELECT / INSERT / UPDATE / DELETE string literal to db.prepare / runSql, directing it to compose b.sql instead, so this class of drift cannot recur. **Changed:** *b.dsr ticket store composes b.sql for its reads and writes* — The data-subject-request ticket store (insert / get / list / update / delete / purge and the legacy re-seal backfill) now builds its DML with the b.sql query builder — sql.select / insert / update / delete(table, { dialect, quoteName }).…toSql() — and prepares the resulting statement, rather than concatenating identifiers into SQL strings by hand. This removes a hand-rolled identifier-quoting surface that could drift from what b.db.from() accepts. Schema provisioning (CREATE TABLE / INDEX, ALTER, PRAGMA) is not a b.sql concern and remains as direct statements. One behavior change: on a store backed by a vault, a ticket payload is AEAD-sealed and base64-encoded (~4/3 expansion) before it is bound, and the bound cell must fit the query builder's 64 MiB per-value ceiling — so the payload is now capped at an expansion-safe plaintext size (~48 MiB) and a larger ticket is refused with dsr/ticket-too-large (route large access/portability exports through chunked storage rather than one giant sealed cell). Plaintext stores keep the full 64 MiB limit. When a vault is first enabled on a table that already holds an over-cap legacy plaintext row, the one-time re-seal backfill still migrates that row's subject columns and derived hashes — so it stays findable by subject lookup and erasable by the data-subject erasure purge — and leaves only the over-cap payload plaintext (still under the read ceiling, DB-encrypted at rest, and removed when the row is erased), rather than failing provisioning with a query-builder error. **Detectors:** *Static check: db-handle primitives must compose b.sql for DML* — A new codebase-patterns check flags any primitive holding a db handle that runs DML by passing an inline SELECT / INSERT / UPDATE / DELETE string literal to db.prepare / runSql — the shape that lets a query drift from b.sql's identifier quoting and sealed-field rewrite (the b.tenant.quota storage defect class). It directs authors to build the query with b.sql and prepare the resulting string. DDL and PRAGMA (not b.sql verbs) and queries already built through a b.sql variable are out of scope.
|
package/lib/archive-tar.js
CHANGED
|
@@ -175,10 +175,20 @@ function _writeString(buf, value, offset, width) {
|
|
|
175
175
|
|
|
176
176
|
function _readOctal(buf, offset, width) {
|
|
177
177
|
// Read an octal-encoded field. Terminator may be space or NUL.
|
|
178
|
+
//
|
|
179
|
+
// POSIX permits numeric fields to be LEFT-padded with spaces (as well as
|
|
180
|
+
// zeros) — star, BSD tar, and several Java/Perl tar libraries emit them,
|
|
181
|
+
// and GNU tar / libarchive skip them. Treating a leading space as a
|
|
182
|
+
// terminator silently misreads such a field as 0 (a parser-differential:
|
|
183
|
+
// the reader would disagree with every other tar on the same bytes, and a
|
|
184
|
+
// misread size desyncs the block walker). Skip leading-space padding first;
|
|
185
|
+
// a space or NUL AFTER the digits still terminates the field.
|
|
186
|
+
var i = 0;
|
|
187
|
+
while (i < width && buf[offset + i] === 0x20) i += 1; // ASCII space (0x20) leading padding per POSIX numeric field
|
|
178
188
|
var s = "";
|
|
179
|
-
for (
|
|
189
|
+
for (; i < width; i += 1) {
|
|
180
190
|
var c = buf[offset + i];
|
|
181
|
-
if (c === 0x20 || c === 0) break; // ASCII space (0x20) + NUL (0x00) field terminators
|
|
191
|
+
if (c === 0x20 || c === 0) break; // ASCII space (0x20) + NUL (0x00) field terminators after digits
|
|
182
192
|
if (c < 0x30 || c > 0x37) { // ASCII '0' (0x30) .. '7' (0x37) octal digits
|
|
183
193
|
throw new TarError("archive-tar/bad-octal",
|
|
184
194
|
"non-octal byte 0x" + c.toString(16) + " at offset " + (offset + i)); // radix=16 for diagnostic hex format
|
package/lib/auth/fido-mds3.js
CHANGED
|
@@ -344,12 +344,16 @@ function _parseNextUpdate(s) {
|
|
|
344
344
|
return d;
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
|
|
347
|
+
// Single source of truth for the BLOB-trust decision: JWS + x5c-chain verify
|
|
348
|
+
// against the supplied trust roots, payload-shape validation, AND the
|
|
349
|
+
// stale-BLOB refusal. fetch() (operator-supplied roots via caCertificate) and
|
|
350
|
+
// _verifyAndParseBlob (default vendored roots) BOTH route here so no check can
|
|
351
|
+
// ever be present on one path but missing on the other. The stale-refusal used
|
|
352
|
+
// to live only in this helper's caller-inlined twin, so the operator-facing
|
|
353
|
+
// fetch path silently accepted (and cached) a signed-but-expired BLOB; keeping
|
|
354
|
+
// one body closes that class.
|
|
355
|
+
function _verifyBlobWithRoots(token, rootPems) {
|
|
351
356
|
var jws = _parseJws(token);
|
|
352
|
-
var rootPems = _resolveRoots(undefined);
|
|
353
357
|
var chain = _validateChain(jws.header.x5c, rootPems);
|
|
354
358
|
_verifyJws(jws, chain[0]);
|
|
355
359
|
var payload = jws.payload;
|
|
@@ -366,19 +370,19 @@ function _verifyAndParseBlob(token) {
|
|
|
366
370
|
throw new FidoMds3Error("fido-mds3/bad-payload",
|
|
367
371
|
"BLOB payload 'nextUpdate' missing or not YYYY-MM-DD: " + payload.nextUpdate);
|
|
368
372
|
}
|
|
369
|
-
// Stale-BLOB refusal
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
373
|
+
// Stale-BLOB refusal (FIDO MDS3 section 3.1.7): clients SHOULD refresh by
|
|
374
|
+
// nextUpdate; a BLOB whose nextUpdate is already in the past is not safe to
|
|
375
|
+
// trust even though its cert chain still validates. Flooring the staleness to
|
|
376
|
+
// MIN_CACHE_TTL_MS in _ttlFromNextUpdate is not enough on its own -- the BLOB
|
|
377
|
+
// itself must be refused, or an attacker serving an ancient signed-but-
|
|
378
|
+
// expired BLOB keeps operators pinned to a revoked-authenticator list frozen
|
|
379
|
+
// at that time. Refuse at parse time so neither fetch nor a cache lookup ever
|
|
380
|
+
// honors it.
|
|
377
381
|
if (nextUpdate.getTime() < Date.now()) {
|
|
378
382
|
throw new FidoMds3Error("fido-mds3/blob-stale",
|
|
379
383
|
"BLOB payload nextUpdate \"" + payload.nextUpdate +
|
|
380
|
-
"\" is in the past
|
|
381
|
-
"(FIDO MDS3
|
|
384
|
+
"\" is in the past -- refusing to trust a stale metadata BLOB " +
|
|
385
|
+
"(FIDO MDS3 section 3.1.7)");
|
|
382
386
|
}
|
|
383
387
|
return {
|
|
384
388
|
entries: payload.entries,
|
|
@@ -388,6 +392,13 @@ function _verifyAndParseBlob(token) {
|
|
|
388
392
|
};
|
|
389
393
|
}
|
|
390
394
|
|
|
395
|
+
// Internal verify-blob helper used by tests to exercise the verifier against
|
|
396
|
+
// the DEFAULT vendored MDS3 trust roots without standing up a real HTTPS
|
|
397
|
+
// endpoint. Operator-facing surface goes through fetch().
|
|
398
|
+
function _verifyAndParseBlob(token) {
|
|
399
|
+
return _verifyBlobWithRoots(token, _resolveRoots(undefined));
|
|
400
|
+
}
|
|
401
|
+
|
|
391
402
|
// ---- public surface ----
|
|
392
403
|
|
|
393
404
|
/**
|
|
@@ -481,39 +492,22 @@ async function fetch(opts) { // allow:raw-outbound-http-framework-internal —
|
|
|
481
492
|
}
|
|
482
493
|
var token = rsp.body.toString("ascii").trim();
|
|
483
494
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
if (typeof payload.no !== "number" || !isFinite(payload.no)) {
|
|
493
|
-
throw new FidoMds3Error("fido-mds3/bad-payload",
|
|
494
|
-
"BLOB payload missing or non-numeric 'no'");
|
|
495
|
-
}
|
|
496
|
-
var nextUpdate = _parseNextUpdate(payload.nextUpdate);
|
|
497
|
-
if (!nextUpdate) {
|
|
498
|
-
throw new FidoMds3Error("fido-mds3/bad-payload",
|
|
499
|
-
"BLOB payload 'nextUpdate' missing or not YYYY-MM-DD: " + payload.nextUpdate);
|
|
500
|
-
}
|
|
501
|
-
var record = {
|
|
502
|
-
entries: payload.entries,
|
|
503
|
-
no: payload.no,
|
|
504
|
-
nextUpdate: nextUpdate,
|
|
505
|
-
url: url,
|
|
506
|
-
legalHeader: payload.legalHeader,
|
|
507
|
-
};
|
|
495
|
+
// Route the trust decision through the shared verifier so this operator-
|
|
496
|
+
// facing path enforces the identical checks (chain verify, payload shape,
|
|
497
|
+
// AND the stale-BLOB refusal) as the internal helper -- fetch previously
|
|
498
|
+
// reimplemented the parse inline and omitted the stale check, silently
|
|
499
|
+
// accepting and caching an ancient signed-but-expired BLOB.
|
|
500
|
+
var record = _verifyBlobWithRoots(token, rootPems);
|
|
501
|
+
record.url = url;
|
|
508
502
|
// Re-assert TTL based on the BLOB's nextUpdate (overrides the
|
|
509
503
|
// wrap-call's safe-minimum seed).
|
|
510
|
-
try { await c.set(cacheKey, record, _ttlFromNextUpdate(nextUpdate)); }
|
|
504
|
+
try { await c.set(cacheKey, record, _ttlFromNextUpdate(record.nextUpdate)); }
|
|
511
505
|
catch (_e) { /* cache.set best-effort */ }
|
|
512
506
|
try { audit().safeEmit({
|
|
513
507
|
action: "auth.fido_mds3.fetch",
|
|
514
508
|
outcome: "success",
|
|
515
|
-
metadata: { url: url, no:
|
|
516
|
-
nextUpdate:
|
|
509
|
+
metadata: { url: url, no: record.no, entries: record.entries.length,
|
|
510
|
+
nextUpdate: record.nextUpdate.toISOString().slice(0, 10) },
|
|
517
511
|
}); } catch (_e) { /* audit best-effort */ }
|
|
518
512
|
return record;
|
|
519
513
|
}, MIN_CACHE_TTL_MS);
|
|
@@ -577,18 +571,35 @@ function lookupAaguid(blob, aaguid) {
|
|
|
577
571
|
function _certifiedLevel(statusReports) {
|
|
578
572
|
if (!Array.isArray(statusReports)) return { level: 0, plus: false };
|
|
579
573
|
var latest = null;
|
|
580
|
-
var latestDate = null;
|
|
574
|
+
var latestDate = null; // null == no comparable date (missing / malformed)
|
|
581
575
|
for (var i = 0; i < statusReports.length; i++) {
|
|
582
576
|
var sr = statusReports[i];
|
|
583
577
|
// Only certification-status reports move the level: a level grant, or its
|
|
584
578
|
// explicit revocation (NOT_FIDO_CERTIFIED). Other statuses (UPDATE_AVAILABLE,
|
|
585
|
-
// REVOKED,
|
|
586
|
-
// status length is bounded before the regex test below
|
|
579
|
+
// REVOKED, etc.) are handled elsewhere and must not be read as a level. The
|
|
580
|
+
// status length is bounded before the regex test below -- FIDO status tokens
|
|
587
581
|
// are short enums; bounding input before any .test() is the convention.
|
|
588
582
|
if (!sr || typeof sr.status !== "string" || sr.status.length > 64) continue;
|
|
589
583
|
if (!CERT_LEVEL_RE.test(sr.status) && sr.status !== "NOT_FIDO_CERTIFIED") continue;
|
|
590
|
-
|
|
591
|
-
|
|
584
|
+
// Only a well-formed calendar date is comparable. Reuse _parseNextUpdate
|
|
585
|
+
// (which already validates YYYY-MM-DD AND rejects impossible dates such as
|
|
586
|
+
// 2026-02-31) and compare epoch ms. A missing / malformed effectiveDate is
|
|
587
|
+
// "no comparable date" (null) and falls back to array order (append order,
|
|
588
|
+
// which the spec defines as chronological): the report later in the array
|
|
589
|
+
// is the more recent one. Coercing a missing date to "" and running it
|
|
590
|
+
// through a lexical compare made an undated report lose to EVERY earlier
|
|
591
|
+
// dated report, so a later decertification / downgrade / upgrade whose
|
|
592
|
+
// effectiveDate was absent never superseded the earlier grant -- freezing
|
|
593
|
+
// certifiedLevel at a stale value (an authenticator-assurance bypass: a
|
|
594
|
+
// decertified or downgraded authenticator kept reporting its prior level to
|
|
595
|
+
// step-up / risk policy).
|
|
596
|
+
var parsed = typeof sr.effectiveDate === "string" ? _parseNextUpdate(sr.effectiveDate) : null;
|
|
597
|
+
var d = parsed ? parsed.getTime() : null;
|
|
598
|
+
if (latest === null) { latest = sr; latestDate = d; continue; }
|
|
599
|
+
// sr is later in array order than the current latest. A missing/malformed
|
|
600
|
+
// date on EITHER side means array order decides -> sr (later) wins. Both
|
|
601
|
+
// dated -> the newer-or-equal date wins (sr, being later, takes ties).
|
|
602
|
+
if (d === null || latestDate === null || d >= latestDate) {
|
|
592
603
|
latest = sr; latestDate = d;
|
|
593
604
|
}
|
|
594
605
|
}
|
package/lib/guard-graphql.js
CHANGED
|
@@ -165,7 +165,8 @@ function _measureQueryShape(query) {
|
|
|
165
165
|
var depth = 0;
|
|
166
166
|
var inString = false;
|
|
167
167
|
var inComment = false;
|
|
168
|
-
var
|
|
168
|
+
var escapeNext = false; // inside a string, the previous char was an unescaped backslash
|
|
169
|
+
var aliasStack = [0]; // per-selection-set alias counter (top of stack = current set)
|
|
169
170
|
for (var i = 0; i < query.length; i += 1) {
|
|
170
171
|
var c = query.charAt(i);
|
|
171
172
|
if (inComment) {
|
|
@@ -173,7 +174,18 @@ function _measureQueryShape(query) {
|
|
|
173
174
|
continue;
|
|
174
175
|
}
|
|
175
176
|
if (inString) {
|
|
176
|
-
|
|
177
|
+
// Escaped-quote handling must track the backslash RUN, not just the
|
|
178
|
+
// single preceding char. `"\\"` is a complete string (its backslash is
|
|
179
|
+
// itself escaped), so a naive `charAt(i - 1) !== "\\"` test reads the
|
|
180
|
+
// closing quote as escaped, leaves the walker stuck in-string, and
|
|
181
|
+
// blinds it to every following brace / colon — silently under-counting
|
|
182
|
+
// depth and alias breadth so a depth-bomb or alias-bomb rides through as
|
|
183
|
+
// shape-clean (a fail-open DoS-measurement bypass on valid GraphQL).
|
|
184
|
+
// Toggle on each backslash: a closing quote ends the string only after
|
|
185
|
+
// an EVEN run (escapeNext false).
|
|
186
|
+
if (escapeNext) { escapeNext = false; continue; }
|
|
187
|
+
if (c === "\\") { escapeNext = true; continue; }
|
|
188
|
+
if (c === '"') inString = false;
|
|
177
189
|
continue;
|
|
178
190
|
}
|
|
179
191
|
if (c === '"') { inString = true; continue; }
|
|
@@ -181,28 +193,35 @@ function _measureQueryShape(query) {
|
|
|
181
193
|
if (c === "{") {
|
|
182
194
|
depth += 1;
|
|
183
195
|
if (depth > maxDepth) maxDepth = depth;
|
|
184
|
-
|
|
196
|
+
aliasStack.push(0);
|
|
185
197
|
} else if (c === "}") {
|
|
186
198
|
// Capture the current selection-set's alias count before popping
|
|
187
199
|
// — otherwise we lose the per-block max when the block closes.
|
|
188
|
-
var current =
|
|
200
|
+
var current = aliasStack[aliasStack.length - 1] || 0;
|
|
189
201
|
if (current > maxAliases) maxAliases = current;
|
|
190
202
|
depth -= 1;
|
|
191
|
-
|
|
203
|
+
// Never pop the base counter: an unbalanced leading `}` would otherwise
|
|
204
|
+
// desync the stack from `depth`, and a subsequent `depth`-indexed
|
|
205
|
+
// increment lands on an absent slot (`undefined + 1 === NaN`), poisoning
|
|
206
|
+
// every later comparison so an alias-bomb reads as clean.
|
|
207
|
+
if (aliasStack.length > 1) aliasStack.pop();
|
|
192
208
|
if (depth < 0) depth = 0;
|
|
193
209
|
} else if (c === ":") {
|
|
194
|
-
// Alias indicator — `alias: field
|
|
195
|
-
//
|
|
210
|
+
// Alias / argument indicator — `alias: field` / `field(arg: value)`.
|
|
211
|
+
// Increment the CURRENT selection-set's counter (top of stack) when the
|
|
212
|
+
// char before `:` looks like an identifier. Using the stack top rather
|
|
213
|
+
// than a `depth` index keeps counting correct even when a malformed
|
|
214
|
+
// brace run has decoupled `depth` from the stack length.
|
|
196
215
|
var prev = i > 0 ? query.charAt(i - 1) : "";
|
|
197
|
-
if (/[A-Za-z0-9_]/.test(prev) &&
|
|
198
|
-
|
|
216
|
+
if (/[A-Za-z0-9_]/.test(prev) && aliasStack.length > 1) {
|
|
217
|
+
aliasStack[aliasStack.length - 1] += 1;
|
|
199
218
|
}
|
|
200
219
|
}
|
|
201
220
|
}
|
|
202
221
|
// Final sweep covers any unclosed selection-sets (operator-supplied
|
|
203
222
|
// syntactically-invalid queries).
|
|
204
|
-
for (var ai = 0; ai <
|
|
205
|
-
if (
|
|
223
|
+
for (var ai = 0; ai < aliasStack.length; ai += 1) {
|
|
224
|
+
if (aliasStack[ai] > maxAliases) maxAliases = aliasStack[ai];
|
|
206
225
|
}
|
|
207
226
|
return { maxDepth: maxDepth, maxAliases: maxAliases };
|
|
208
227
|
}
|
package/lib/self-update.js
CHANGED
|
@@ -378,6 +378,15 @@ async function poll(opts) {
|
|
|
378
378
|
allowedHosts: opts.allowedHosts,
|
|
379
379
|
allowedProtocols: opts.allowedProtocols,
|
|
380
380
|
allowInternal: opts.allowInternal,
|
|
381
|
+
// poll() owns status handling — the branches below distinguish a 304
|
|
382
|
+
// If-None-Match "fast no-update" hit from a real non-2xx refusal and a
|
|
383
|
+
// 2xx feed to parse. Without always-resolve, httpClient.request rejects
|
|
384
|
+
// EVERY non-2xx (304 included) as HTTP_ERROR before poll can inspect
|
|
385
|
+
// res.statusCode, which made the documented conditional-poll fast-path
|
|
386
|
+
// and the selfupdate/poll-non-2xx branch dead code — a conditional poll
|
|
387
|
+
// that correctly received a 304 threw selfupdate/poll-failed instead of
|
|
388
|
+
// reporting "no update".
|
|
389
|
+
responseMode: "always-resolve",
|
|
381
390
|
errorClass: SelfUpdateError,
|
|
382
391
|
});
|
|
383
392
|
} catch (e) {
|
|
@@ -630,6 +639,15 @@ function _validateSwapOpts(opts, label) {
|
|
|
630
639
|
"selfUpdate.swap: opts.hashAlgo must be one of " + ALLOWED_HASH_ALGS.join(", "));
|
|
631
640
|
}
|
|
632
641
|
};
|
|
642
|
+
// swap re-reads the from-bytes to re-hash them (closing the verify->swap
|
|
643
|
+
// window); its cap must be declarable so it matches the maxBytes an
|
|
644
|
+
// operator passed to selfUpdate.verify for the same asset — otherwise swap
|
|
645
|
+
// would refuse a large binary that verify accepted. Optional; defaults to
|
|
646
|
+
// the same C.BYTES.gib(1) cap the body applies.
|
|
647
|
+
schema.maxBytes = function (value) {
|
|
648
|
+
numericBounds.requirePositiveFiniteIntIfPresent(value,
|
|
649
|
+
"selfUpdate.swap: opts.maxBytes", SelfUpdateError, "selfupdate/bad-max-bytes");
|
|
650
|
+
};
|
|
633
651
|
}
|
|
634
652
|
schema.to = { rule: "required-string", code: "selfupdate/bad-to",
|
|
635
653
|
label: "selfUpdate." + label + ": opts.to" };
|
|
@@ -696,6 +714,8 @@ async function _safeRollback(backupTo, to, hadOriginal) {
|
|
|
696
714
|
* backupTo: string, // required — backup path for the existing `to`
|
|
697
715
|
* expectedHash: string, // required — the hash selfUpdate.verify returned
|
|
698
716
|
* hashAlgo: string, // sha3-512 (default) | sha-256 | sha-512 | shake256
|
|
717
|
+
* maxBytes: number, // from-bytes re-hash cap (default 1 GiB) — set to
|
|
718
|
+
* // the same value passed to selfUpdate.verify
|
|
699
719
|
*
|
|
700
720
|
* @example
|
|
701
721
|
* var v = await b.selfUpdate.verify({ assetPath, signaturePath, pubkeyPem });
|
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:1786ee2a-259a-4966-b764-11772b484854",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-12T08:00:39.980Z",
|
|
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.16.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.16.18",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.16.
|
|
25
|
+
"version": "0.16.18",
|
|
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.16.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.16.18",
|
|
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.16.
|
|
57
|
+
"ref": "@blamejs/core@0.16.18",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|