@blamejs/core 0.16.40 → 0.17.1

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 CHANGED
@@ -6,6 +6,12 @@ Pre-1.0 the surface is intentionally evolving — every release may
6
6
  change something operators depend on. Read each entry before
7
7
  upgrading across more than a few patches at a time.
8
8
 
9
+ ## v0.17.x
10
+
11
+ - v0.17.1 (2026-07-17) — **Profile, posture, and capability name lookups across the content-safety and mail-protocol guards reject a prototype-member name instead of running under it, the mTLS CA can generate a CRL after a fingerprint-only revocation, and a hostile MIME filename no longer crashes attachment extraction.** A second family of guards resolved a profile, posture, or capability name against a plain-object lookup table with a truthiness or in-operator guard, so a name that is an inherited Object.prototype member (constructor, __proto__, toString) slipped the guard and the gate ran under the inherited member instead of refusing the unknown name. The shared profile resolver (b.gateContract.makeProfileResolver, which the guard family composes), the iCal and vCard content guards, the dark-patterns posture check, the JMAP capability allowlist, and the IMAP/POP3/ManageSieve command guards now all resolve names through an own-property check. Separately, b.mtlsCa.generateCrl crashed with a null-serial error once any certificate had been revoked by fingerprint (the mode the require-mTLS gate pins on), dropping every serial-keyed revocation from the published CRL; and b.safeMime.extractAttachments threw an uncaught URIError on a hostile Content-Disposition filename with a malformed percent-escape, crashing the caller. **Fixed:** *mTLS CA generates a CRL after a fingerprint-only revocation* — b.mtlsCa.revoke accepts a certificate fingerprint (the value the require-mTLS gate pins on and generateClientCert surfaces for exactly this use), which is stored without a serial number. b.mtlsCa.generateCrl then mapped the whole revocation registry into CRL entries and handed the null serial to the CRL encoder, which threw -- aborting CRL generation entirely. Because one fingerprint-only revocation broke every subsequent CRL build, all serial-keyed revocations were silently dropped from the published CRL and it could never be regenerated, going stale for external CRL-based revocation checking. generateCrl now projects out fingerprint-only entries (which a standard X.509 CRL cannot represent) before encoding and reports how many were omitted; fingerprint-only revocations remain enforced through the mTLS gate, and the CRL publishes every serial-keyed revocation. · *A hostile MIME filename no longer crashes attachment extraction* — b.safeMime.extractAttachments decoded an RFC 2231 / RFC 5987 extended filename parameter (filename*=charset''percent-encoded) with an unguarded percent-decode, so a malformed escape -- a truncated %, a non-hex %ZZ, or a sequence that decodes to invalid UTF-8 -- threw an uncaught URIError that escaped the parser's typed-error contract and crashed the caller (for example a mail store extracting attachments). A one-line hostile Content-Disposition header was a trivial denial of service. The decode now degrades to the still-encoded filename on failure, matching the framework's handling of every other percent-decode site, so a hostile filename yields a best-effort name that downstream filename guards still vet rather than a crash. **Security:** *Guard profile, posture, and capability lookups reject a prototype-member name* — A group of content-safety and mail-protocol guards resolved a caller- or request-supplied profile / posture / capability name against a plain-object lookup table using a truthiness check (var caps = TABLE[name]; if (!caps)) or the prototype-chain-aware in operator. A name that names an inherited Object.prototype member -- constructor, __proto__, toString -- is truthy (or present via the prototype chain), so it slipped the guard and the gate ran under the inherited member instead of refusing the unknown name. The shared resolver b.gateContract.makeProfileResolver (composed by the idempotency-key, mail-compose, message-id and other guards) now guards both its posture and profile lookups with hasOwnProperty; b.safeIcal, b.safeVcard, b.darkPatterns, b.guardJmap (capability allowlist and profile/posture), and the b.guardImapCommand / b.guardPop3Command / b.guardManageSieveCommand command guards do the same. An unknown or prototype-member name is now rejected with the guard's typed error; supported names are unaffected. This extends the same own-property hardening applied to the crypto algorithm-table lookups in 0.17.0 to the guard-family profile resolvers.
12
+
13
+ - v0.17.0 (2026-07-17) — **Algorithm and key-type lookups across the crypto verifiers reject a prototype-member name instead of accepting it, the ZIP reader honors the operator's decompression-ratio policy, and signed S3/GCS query parameters transmit the space encoding they were signed with.** A family of verifiers resolved an algorithm, hash, or key-type name against a plain-object lookup table with a truthiness or `in`-operator guard, so a name that is an inherited Object.prototype member (constructor, __proto__, toString, valueOf) slipped the guard and resolved to the inherited member: b.jwk thumbprinted an attacker-crafted key to a predictable digest, b.vc and b.contentCredentials emitted a real signature under a bogus algorithm, and b.sdJwtVc accepted an attacker-controlled hash name from an unsigned issuer payload. All of them now reject a name that is not an own property of the table. Separately, b.archive's ZIP reader silently capped the decompression ratio at its composed default and ignored the operator's configured bomb policy, refusing legitimate highly-compressible entries; and b.storage's SigV4 (and the GCS V4 presigner) signed a query space as %20 but transmitted it as +, so a signed parameter carrying a space was rejected as a signature mismatch. **Fixed:** *ZIP extraction honors the operator's decompression-ratio policy* — b.archive's random-access ZIP reader composed b.safeDecompress for each DEFLATE entry without forwarding the reader's own expansion-ratio cap, so the decompression inherited safeDecompress's stricter default ratio and silently overrode the operator's configured bomb policy. A ZIP entry that legitimately compressed better than the default ratio (logs, JSON, telemetry, zero-padded or sparse binaries) was refused even when the operator's policy permitted it. The reader now forwards its configured maximum expansion ratio into the decode, so the actual-bytes ratio re-check uses the same cap the declared-size gate already enforced. · *Signed S3 and GCS query parameters transmit the space encoding they were signed with* — b.storage's SigV4 request signing (S3, R2, MinIO, and the S3-compatible backends) and the GCS V4 presigner sign the canonical query string, which encodes a space as %20 per the AWS/GCS specification, but transmitted the request via the WHATWG URL serializer, which encodes a space as +. A signed query parameter carrying a literal space -- a response-content-disposition filename or a list prefix -- was therefore signed over %20 but sent as +, so the storage server re-canonicalized to different bytes and rejected the request with a signature mismatch. The signer now aligns the wire query to the signed canonical encoding (rewriting a bare + back to %20) at each signing sink, so the transmitted query is byte-identical to what the signature commits to. **Security:** *Crypto algorithm and key-type lookups reject a prototype-member name* — Several verifiers resolved a caller- or attacker-supplied algorithm / hash / key-type name against a plain-object lookup table using a truthiness check (`var v = TABLE[name]; if (!v) reject`) or the prototype-chain-aware `in` operator. A name that names an inherited Object.prototype member -- constructor, __proto__, toString, valueOf, toLocaleString -- is truthy (or present via the prototype chain), so it slipped the guard and resolved to the inherited member. The observed consequences: b.jwk.thumbprint / canonicalize accepted an unsupported kty and thumbprinted the key to a single predictable digest; b.vc.issue (JOSE) and b.contentCredentials.signCose emitted a real signature under a bogus algorithm id; and b.sdJwtVc.present accepted an attacker-controlled _sd_alg read from the unsigned issuer payload. b.tsa and the SD-JWT hash-disclosure path returned a raw runtime error rather than a typed rejection. Every one of these table lookups now checks Object.prototype.hasOwnProperty before indexing, so a name that is not an own member of the table is rejected with the primitive's typed error. Supported algorithm and key-type names are unaffected.
14
+
9
15
  ## v0.16.x
10
16
 
11
17
  - v0.16.40 (2026-07-16) — **Four request-path fixes: the router no longer lets the Host header steer which route runs, per-route rate limits can no longer be evaded with a rotating query string, and the message-format and URI-template expanders no longer leak inherited object properties.** b.router derived the dispatch path from the client-controlled Host header, so a path-like Host value bled into the parsed pathname and could steer a request to a different route than its request line named -- a path-ACL bypass in front-proxied deployments. b.middleware.rateLimit in per-route scope fell back to the full request URL (query string included) when the router had not populated the path, letting a rotating throwaway query parameter mint a fresh bucket per request. And b.i18n message-format and b.uriTemplate resolved a template-derived variable name with a bare property read, so a name like {toString} or {constructor} returned an inherited Object.prototype member (or a prototype-polluted value) instead of treating the variable as absent. **Security:** *The Host header can no longer influence which route is dispatched* — b.router derived the request path it matches routes against (req.pathname) by parsing the Host header concatenated with the request URL. WHATWG URL parsing folds any path-like characters in the Host value into the parsed pathname, so a request whose request line was /x but whose Host header was trusted/admin dispatched as /admin/x while req.url stayed /x. Because req.pathname is what the route matcher and every path-scoped guard (auth, CSRF, mTLS) compare against, a client could steer which handler runs -- and a front proxy or WAF that authorizes on the visible request path is bypassed. The router now derives the path and query from the request URL alone, resolved against a fixed internal authority, so the Host header cannot perturb routing; a request target that is not origin-form (a single leading slash) -- an absolute-form, authority-form, asterisk-form, or network-path-reference target -- is rejected with 400 rather than coerced into a routable path. The request's real scheme and host remain available to the consumers (canonical URL, CORS, host allowlist) that legitimately need them. · *Per-route rate limits key on the path only, not the query string* — b.middleware.rateLimit with scope: "per-route" composed its bucket key from req.pathname, falling back to the full request URL when pathname was absent (a raw Node request handed to the middleware without the router populating it). The fallback included the query string, which is not part of route identity, so an attacker could rotate a throwaway parameter (?nonce=1, ?nonce=2, ...) to mint a fresh bucket per request from the same client and evade the limit. The per-route key now strips the query in the fallback, matching the path-normalization the other request-scoped guards already apply. · *Message-format and URI-template variable lookups are own-property only* — b.i18n message-format ({arg}, {n, plural, ...}, {s, select, ...}) and b.uriTemplate ({var} expansion) resolved a variable name parsed from the template with a bare property read of the caller's variables object. A template naming an inherited member -- {toString}, {constructor}, {valueOf}, {__proto__} -- therefore returned the Object.prototype member (a function's source, or a prototype-polluted value) instead of treating the variable as absent, leaking it into rendered output or an expanded URI. Both now resolve variables through an own-property check, so an inherited or polluted name renders empty (message-format argument) or is omitted per RFC 6570 (URI template), matching the own-property discipline the HTML template engine and the simple interpolator already enforce.
@@ -606,6 +606,14 @@ async function _decompressEntry(adapter, entry, dataStart, bombPolicy) {
606
606
  algorithm: "deflate-raw",
607
607
  maxOutputBytes: maxOutput,
608
608
  maxCompressedBytes: entry.compressedSize,
609
+ // Honor the reader's own expansion-ratio cap. Without this, the
610
+ // composition inherits safeDecompress's DEFAULT_MAX_RATIO (50),
611
+ // which silently overrides bombPolicy.maxExpansionRatio and refuses
612
+ // every entry compressing better than 50:1 even when the operator's
613
+ // policy (default 100) permits it. The declared-size ratio was
614
+ // already validated in _enforceBombPolicy; this re-check on the
615
+ // actual inflated bytes uses the SAME configured cap.
616
+ maxRatio: bombPolicy.maxExpansionRatio,
609
617
  });
610
618
  if (decompressed.length !== entry.uncompressedSize) {
611
619
  throw new ArchiveReadError("archive-read/inflate-size-mismatch",
@@ -119,11 +119,11 @@ function _b64uDecodeBuf(s) {
119
119
  }
120
120
 
121
121
  function _hashDisclosure(disclosureStr, hashAlg) {
122
- var nodeAlg = SUPPORTED_HASH_ALGS[hashAlg];
123
- if (!nodeAlg) {
122
+ if (!Object.prototype.hasOwnProperty.call(SUPPORTED_HASH_ALGS, hashAlg)) {
124
123
  throw new AuthError("auth-sd-jwt-vc/bad-hash",
125
124
  "Unsupported hash algorithm: " + hashAlg);
126
125
  }
126
+ var nodeAlg = SUPPORTED_HASH_ALGS[hashAlg];
127
127
  var h = nodeCrypto.createHash(nodeAlg);
128
128
  h.update(disclosureStr, "ascii");
129
129
  return h.digest().toString("base64url");
@@ -338,12 +338,12 @@ function present(opts) {
338
338
  }
339
339
  var _sdAlg = (_issuerPayload && typeof _issuerPayload._sd_alg === "string")
340
340
  ? _issuerPayload._sd_alg : "sha-256";
341
- var _sdNodeHash = SUPPORTED_HASH_ALGS[_sdAlg];
342
- if (!_sdNodeHash) {
341
+ if (!Object.prototype.hasOwnProperty.call(SUPPORTED_HASH_ALGS, _sdAlg)) {
343
342
  throw new AuthError("auth-sd-jwt-vc/bad-hash",
344
343
  "present: issuer credential declares _sd_alg \"" + _sdAlg +
345
344
  "\" which this framework version does not support");
346
345
  }
346
+ var _sdNodeHash = SUPPORTED_HASH_ALGS[_sdAlg];
347
347
 
348
348
  var disclosedNames = Array.isArray(opts.disclosedClaimNames)
349
349
  ? opts.disclosedClaimNames.slice() : [];
@@ -595,7 +595,7 @@ function signCose(manifest, opts) {
595
595
  validateOpts.requireNonEmptyString(opts.privateKeyPem,
596
596
  "contentCredentials.signCose: privateKeyPem", ContentCredentialsError, "BAD_KEY");
597
597
  var algName = (opts.alg || "ml-dsa-87").toLowerCase();
598
- if (!(algName in COSE_ALGS)) {
598
+ if (!Object.prototype.hasOwnProperty.call(COSE_ALGS, algName)) {
599
599
  throw ContentCredentialsError.factory("content-credentials/bad-alg",
600
600
  "contentCredentials.signCose: alg '" + algName +
601
601
  "' not in COSE alg registry. Known: " + Object.keys(COSE_ALGS).join(", "));
@@ -266,11 +266,14 @@ function assertParity(signup, cancel, opts) {
266
266
  "darkPatterns.assertParity: resourceId differs between flows");
267
267
  }
268
268
  var postureName = opts.posture || "ftc-2024";
269
- var posture = POSTURES[postureName];
270
- if (!posture) {
269
+ // Own-property lookup: postureName is operator input, so a bare
270
+ // `POSTURES[postureName]` with a truthiness guard would let a prototype key
271
+ // ("constructor") resolve to an inherited member and run under it (fail-open).
272
+ if (!Object.prototype.hasOwnProperty.call(POSTURES, postureName)) {
271
273
  throw errorClass.factory("dark-patterns/bad-posture",
272
274
  "darkPatterns.assertParity: unknown posture " + postureName);
273
275
  }
276
+ var posture = POSTURES[postureName];
274
277
 
275
278
  var toleranceClicks = typeof opts.toleranceClicks === "number"
276
279
  ? opts.toleranceClicks : posture.toleranceClicks;
@@ -1190,12 +1190,20 @@ function makeProfileResolver(cfg) {
1190
1190
  var label = _guardLabelFromError(ErrorClass);
1191
1191
  return function resolveProfile(opts) {
1192
1192
  opts = opts || {};
1193
- if (opts.posture && postures && postures[opts.posture]) {
1193
+ // Own-property lookups only: opts.posture / opts.profile are attacker/
1194
+ // operator input, so a bare bracket read would let a prototype key
1195
+ // ("constructor" / "__proto__" / "toString") resolve to an inherited
1196
+ // truthy member — the posture read would return it as the profile name,
1197
+ // and `!profiles[p]` would treat it as a known profile and skip the
1198
+ // bad-profile refusal, running the gate under a bogus profile (fail-open).
1199
+ if (opts.posture && postures &&
1200
+ Object.prototype.hasOwnProperty.call(postures, opts.posture) &&
1201
+ postures[opts.posture]) {
1194
1202
  var pn = postures[opts.posture];
1195
1203
  return byObject ? profiles[pn] : pn;
1196
1204
  }
1197
1205
  var p = opts.profile || dft;
1198
- if (!profiles[p]) {
1206
+ if (!Object.prototype.hasOwnProperty.call(profiles, p)) {
1199
1207
  throw ErrorClass.factory(codePrefix + "/bad-profile",
1200
1208
  label + ": unknown profile '" + p + "' (use " +
1201
1209
  Object.keys(profiles).join(" / ") + ")");
@@ -198,10 +198,18 @@ var LITERAL_SMUGGLE_RE = /\{[0-9]+\+?\}(?!\s*$)/;
198
198
  function validate(line, opts) {
199
199
  opts = opts || {};
200
200
  var profileName = typeof opts.profile === "string" ? opts.profile : DEFAULT_PROFILE;
201
- if (opts.posture && COMPLIANCE_POSTURES[opts.posture]) {
201
+ // hasOwnProperty on both lookups: opts.profile / opts.posture are caller
202
+ // input, and a bare bracket read resolves an inherited Object.prototype
203
+ // key (constructor / __proto__ / toString / ...) to a truthy value —
204
+ // PROFILES["constructor"] is the Object function, so `if (!caps)` never
205
+ // fires and every byte / literal cap compares against `undefined` (fails
206
+ // open). An unknown posture is ignored (falls through to profile/default);
207
+ // an unknown profile throws bad-profile.
208
+ if (opts.posture && Object.prototype.hasOwnProperty.call(COMPLIANCE_POSTURES, opts.posture)) {
202
209
  profileName = COMPLIANCE_POSTURES[opts.posture];
203
210
  }
204
- var caps = PROFILES[profileName];
211
+ var caps = Object.prototype.hasOwnProperty.call(PROFILES, profileName)
212
+ ? PROFILES[profileName] : undefined;
205
213
  if (!caps) {
206
214
  throw new GuardImapCommandError("guard-imap-command/bad-profile",
207
215
  "guardImapCommand.validate: unknown profile '" + profileName + "'");
package/lib/guard-jmap.js CHANGED
@@ -133,10 +133,18 @@ var CORE_CAPABILITIES = Object.freeze({
133
133
  function validate(rawBody, opts) {
134
134
  opts = opts || {};
135
135
  var profileName = typeof opts.profile === "string" ? opts.profile : DEFAULT_PROFILE;
136
- if (opts.posture && COMPLIANCE_POSTURES[opts.posture]) {
136
+ // hasOwnProperty on both lookups: opts.profile / opts.posture are caller
137
+ // input, and a bare bracket read resolves an inherited Object.prototype
138
+ // key (constructor / __proto__ / toString / ...) to a truthy value —
139
+ // PROFILES["constructor"] is the Object function, so `if (!caps)` never
140
+ // fires and every size / count cap compares against `undefined` (fails
141
+ // open). An unknown posture is ignored (falls through to profile/default)
142
+ // as before; an unknown profile throws bad-profile.
143
+ if (opts.posture && Object.prototype.hasOwnProperty.call(COMPLIANCE_POSTURES, opts.posture)) {
137
144
  profileName = COMPLIANCE_POSTURES[opts.posture];
138
145
  }
139
- var caps = PROFILES[profileName];
146
+ var caps = Object.prototype.hasOwnProperty.call(PROFILES, profileName)
147
+ ? PROFILES[profileName] : undefined;
140
148
  if (!caps) {
141
149
  throw new GuardJmapError("guard-jmap/bad-profile",
142
150
  "guardJmap.validate: unknown profile '" + profileName + "'");
@@ -198,7 +206,15 @@ function validate(rawBody, opts) {
198
206
  throw new GuardJmapError("urn:ietf:params:jmap:error:invalidArguments",
199
207
  "guardJmap.validate: `using[" + ui + "]` must be a string capability URI");
200
208
  }
201
- if (!Object.prototype.hasOwnProperty.call(CORE_CAPABILITIES, cap) && !serverCaps[cap]) {
209
+ // hasOwnProperty on both tables: `cap` is attacker-supplied (from the
210
+ // request body). A bare `serverCaps[cap]` bracket read resolves an
211
+ // inherited Object.prototype member (constructor / __proto__ / toString
212
+ // / ...) to a truthy value, letting a prototype-key capability pass the
213
+ // allowlist the server never advertised. Own-key AND truthy preserves
214
+ // the operator's "capability: false disables it" semantics.
215
+ var advertised = Object.prototype.hasOwnProperty.call(CORE_CAPABILITIES, cap) ||
216
+ (Object.prototype.hasOwnProperty.call(serverCaps, cap) && serverCaps[cap]);
217
+ if (!advertised) {
202
218
  throw new GuardJmapError("urn:ietf:params:jmap:error:unknownCapability",
203
219
  "guardJmap.validate: capability '" + cap + "' not advertised by this server");
204
220
  }
@@ -202,10 +202,18 @@ var LITERAL_RE = /^\{([0-9]{1,10})(\+?)\}$/;
202
202
  function validate(line, opts) {
203
203
  opts = opts || {};
204
204
  var profileName = typeof opts.profile === "string" ? opts.profile : DEFAULT_PROFILE;
205
- if (opts.posture && COMPLIANCE_POSTURES[opts.posture]) {
205
+ // hasOwnProperty on both lookups: opts.profile / opts.posture are caller
206
+ // input, and a bare bracket read resolves an inherited Object.prototype
207
+ // key (constructor / __proto__ / toString / ...) to a truthy value —
208
+ // PROFILES["constructor"] is the Object function, so `if (!caps)` never
209
+ // fires and every line / script / script-name cap compares against
210
+ // `undefined` (fails open). An unknown posture is ignored (falls through
211
+ // to profile/default); an unknown profile throws bad-profile.
212
+ if (opts.posture && Object.prototype.hasOwnProperty.call(COMPLIANCE_POSTURES, opts.posture)) {
206
213
  profileName = COMPLIANCE_POSTURES[opts.posture];
207
214
  }
208
- var caps = PROFILES[profileName];
215
+ var caps = Object.prototype.hasOwnProperty.call(PROFILES, profileName)
216
+ ? PROFILES[profileName] : undefined;
209
217
  if (!caps) {
210
218
  throw new GuardManageSieveCommandError("guard-managesieve-command/bad-profile",
211
219
  "guardManageSieveCommand.validate: unknown profile '" + profileName + "'");
@@ -155,10 +155,18 @@ var MSG_NUM_RE = /^[1-9][0-9]{0,9}$/;
155
155
  function validate(line, opts) {
156
156
  opts = opts || {};
157
157
  var profileName = typeof opts.profile === "string" ? opts.profile : DEFAULT_PROFILE;
158
- if (opts.posture && COMPLIANCE_POSTURES[opts.posture]) {
158
+ // hasOwnProperty on both lookups: opts.profile / opts.posture are caller
159
+ // input, and a bare bracket read resolves an inherited Object.prototype
160
+ // key (constructor / __proto__ / toString / ...) to a truthy value —
161
+ // PROFILES["constructor"] is the Object function, so `if (!caps)` never
162
+ // fires and every line / username / password cap compares against
163
+ // `undefined` (fails open). An unknown posture is ignored (falls through
164
+ // to profile/default); an unknown profile throws bad-profile.
165
+ if (opts.posture && Object.prototype.hasOwnProperty.call(COMPLIANCE_POSTURES, opts.posture)) {
159
166
  profileName = COMPLIANCE_POSTURES[opts.posture];
160
167
  }
161
- var caps = PROFILES[profileName];
168
+ var caps = Object.prototype.hasOwnProperty.call(PROFILES, profileName)
169
+ ? PROFILES[profileName] : undefined;
162
170
  if (!caps) {
163
171
  throw new GuardPop3CommandError("guard-pop3-command/bad-profile",
164
172
  "guardPop3Command.validate: unknown profile '" + profileName + "'");
package/lib/jwk.js CHANGED
@@ -58,8 +58,17 @@ function _requiredMembers(jwk) {
58
58
  if (typeof jwk.kty !== "string" || jwk.kty.length === 0) {
59
59
  throw new JwkError("jwk/bad-jwk", "jwk: 'kty' is required");
60
60
  }
61
+ // Own-property lookup only: `kty` is attacker-controlled, and a plain-object
62
+ // bracket read resolves inherited Object.prototype members (`__proto__`,
63
+ // `toString`, `valueOf`, `toLocaleString`, …). Those are truthy, so an
64
+ // unguarded `if (!names)` would treat them as supported key types — the
65
+ // zero-`length` prototype methods then iterate no required members and
66
+ // thumbprint the empty object, collapsing distinct inputs onto one
67
+ // predictable digest instead of the intended unsupported-kty refusal.
68
+ if (!Object.prototype.hasOwnProperty.call(REQUIRED, jwk.kty)) {
69
+ throw new JwkError("jwk/unsupported-kty", "jwk: unsupported kty '" + jwk.kty + "'");
70
+ }
61
71
  var names = REQUIRED[jwk.kty];
62
- if (!names) throw new JwkError("jwk/unsupported-kty", "jwk: unsupported kty '" + jwk.kty + "'");
63
72
  var out = {};
64
73
  for (var i = 0; i < names.length; i++) {
65
74
  var n = names[i];
@@ -115,8 +124,16 @@ function canonicalize(jwk) {
115
124
  */
116
125
  function thumbprint(jwk, opts) {
117
126
  opts = opts || {};
118
- var hash = HASHES[opts.hash || "sha256"];
119
- if (!hash) throw new JwkError("jwk/bad-hash", "jwk.thumbprint: hash must be sha256, sha384, or sha512");
127
+ // Own-property lookup only — `opts.hash` is caller-controlled; a bare
128
+ // bracket read of the HASHES table resolves inherited members (`toString`,
129
+ // `constructor`, …) to truthy functions that slip past `!hash` and reach
130
+ // createHash as a non-string, leaking a raw ERR_INVALID_ARG_TYPE instead of
131
+ // the typed jwk/bad-hash refusal the primitive documents.
132
+ var hashName = opts.hash || "sha256";
133
+ if (!Object.prototype.hasOwnProperty.call(HASHES, hashName)) {
134
+ throw new JwkError("jwk/bad-hash", "jwk.thumbprint: hash must be sha256, sha384, or sha512");
135
+ }
136
+ var hash = HASHES[hashName];
120
137
  var canon = canonicalize(jwk);
121
138
  return nodeCrypto.createHash(hash).update(canon, "utf8").digest("base64url");
122
139
  }
package/lib/mtls-ca.js CHANGED
@@ -607,7 +607,21 @@ function create(opts) {
607
607
  "framework's bundled CA engine, which supports it");
608
608
  }
609
609
  var ca = await initCA();
610
- var revocations = revocationStore.list();
610
+ var allRevocations = revocationStore.list();
611
+ // A standard X.509 CRL (RFC 5280 §5.1) is keyed by certificate serial
612
+ // number. revoke({ fingerprint }) — a first-class revocation mode, and the
613
+ // value the require-mtls gate pins on — stores no serial (serialNumber is
614
+ // null). Such an entry cannot be represented in a CRL, so project it out
615
+ // here rather than handing a null serial to the CRL encoder: the encoder
616
+ // throws on it, which would break CRL generation for the ENTIRE registry,
617
+ // dropping the serial-keyed certs that CAN be published from every fresh
618
+ // CRL (a fail-open for those certs' published revocation). Fingerprint-only
619
+ // revocations stay enforced through isRevoked()/the mTLS gate, which is
620
+ // fingerprint-aware; the count that could not be represented is surfaced.
621
+ var revocations = allRevocations.filter(function (r) {
622
+ return r && r.serialNumber != null;
623
+ });
624
+ var fingerprintOnlyOmitted = allRevocations.length - revocations.length;
611
625
  var nowMs = Date.now();
612
626
  var thisUpdate = opts3.thisUpdate || new Date(nowMs);
613
627
  var nextUpdate = opts3.nextUpdate ||
@@ -627,7 +641,9 @@ function create(opts) {
627
641
  atomicFile.writeSync(paths.crl, crlPem, { mode: 0o644 });
628
642
  }
629
643
  return { crlPem: crlPem, thisUpdate: thisUpdate, nextUpdate: nextUpdate,
630
- entryCount: revocations.length, path: paths.crl };
644
+ entryCount: revocations.length,
645
+ fingerprintOnlyOmitted: fingerprintOnlyOmitted,
646
+ path: paths.crl };
631
647
  }
632
648
 
633
649
  return {
@@ -362,6 +362,13 @@ function create(config) {
362
362
 
363
363
  url.searchParams.set("X-Goog-Signature", signature);
364
364
 
365
+ // Final query mutation done — align the wire space encoding to the signed
366
+ // canonical query (GCS V4 signs "%20" but url.toString() would serialize a
367
+ // space as "+", so a spaced response-header override / prefix would be
368
+ // rejected as a signature mismatch). Must precede url.toString(); do not
369
+ // touch url.searchParams afterward (it re-serializes spaces back to "+").
370
+ sigv4.alignWireQueryToSigV4(url);
371
+
365
372
  var clientHeaders = {};
366
373
  if (opts.contentType) clientHeaders["Content-Type"] = opts.contentType;
367
374
 
@@ -120,6 +120,29 @@ function canonicalQueryString(searchParams) {
120
120
  }).join("&");
121
121
  }
122
122
 
123
+ // Reconcile the wire query with the signed canonical query. The WHATWG URL
124
+ // serializes its query via application/x-www-form-urlencoded, which encodes a
125
+ // space as "+"; SigV4 signs canonicalQueryString, which encodes a space as
126
+ // "%20" (AWS: "encode the space character as %20 and not +"). Left
127
+ // unreconciled, a signed query parameter carrying a literal space (a
128
+ // response-content-disposition filename, a list prefix) is transmitted
129
+ // (url.search / url.toString) as "+" while the signature commits to "%20", so
130
+ // the server re-canonicalizes to different bytes and rejects the request with
131
+ // SignatureDoesNotMatch. A bare "+" in url.search is ALWAYS an encoded space —
132
+ // URLSearchParams serializes a literal "+" as "%2B" and parses "+" back to a
133
+ // space, so canonicalQueryString already signed that position as "%20";
134
+ // rewriting "+" to "%20" makes the transmitted query byte-identical to the
135
+ // signed canonical query. Bare subresource tokens (?uploads / ?lifecycle) carry
136
+ // no "+", so they pass through untouched. Mutates url.search in place (the
137
+ // string setter preserves "%20"); callers MUST NOT touch url.searchParams
138
+ // afterward — that re-serializes spaces back to "+".
139
+ function _alignWireQueryToSigV4(url) {
140
+ var search = url.search;
141
+ if (search.indexOf("+") !== -1) {
142
+ url.search = search.replace(/\+/g, "%20"); // allow:regex-no-length-cap — single-char global replace, input bounded by maxUrlLength
143
+ }
144
+ }
145
+
123
146
  function canonicalHeaders(headers) {
124
147
  var pairs = [];
125
148
  for (var k in headers) {
@@ -233,6 +256,10 @@ function signRequest(opts) {
233
256
  ", Signature=" + signature;
234
257
  headers["Authorization"] = auth;
235
258
 
259
+ // The URL just signed is the one the caller transmits — make its wire query
260
+ // byte-identical to the canonical query the signature commits to.
261
+ _alignWireQueryToSigV4(url);
262
+
236
263
  return { headers: headers, signature: signature, canonicalRequest: canon, stringToSign: sts };
237
264
  }
238
265
 
@@ -810,6 +837,9 @@ function create(config) {
810
837
  var signature = nodeCrypto.createHmac("sha256", signingKey).update(sts).digest("hex");
811
838
 
812
839
  url.searchParams.set("X-Amz-Signature", signature);
840
+ // Final query mutation done — align the wire space encoding to the signed
841
+ // "%20" form before serializing the URL the client will use.
842
+ _alignWireQueryToSigV4(url);
813
843
 
814
844
  var clientHeaders = {};
815
845
  if (opts.contentType) clientHeaders["Content-Type"] = opts.contentType;
@@ -994,6 +1024,7 @@ module.exports = {
994
1024
  deriveSigningKey: deriveSigningKey,
995
1025
  canonicalQueryString: canonicalQueryString,
996
1026
  canonicalHeaders: canonicalHeaders,
1027
+ alignWireQueryToSigV4: _alignWireQueryToSigV4,
997
1028
  awsUriEncode: awsUriEncode,
998
1029
  sha256Hex: sha256Hex,
999
1030
  formatAmzDate: _formatAmzDate,
package/lib/safe-ical.js CHANGED
@@ -281,14 +281,18 @@ function _resolveCaps(opts) {
281
281
  if (typeof opts.profile === "string") {
282
282
  name = opts.profile;
283
283
  } else if (typeof opts.compliancePosture === "string") {
284
- name = COMPLIANCE_POSTURES[opts.compliancePosture] || "strict";
284
+ name = Object.prototype.hasOwnProperty.call(COMPLIANCE_POSTURES, opts.compliancePosture)
285
+ ? COMPLIANCE_POSTURES[opts.compliancePosture] : "strict";
285
286
  }
286
- var caps = PROFILES[name];
287
- if (!caps) {
287
+ // Own-property lookup: `name` derives from attacker/operator opts.profile, so
288
+ // a bare `if (!PROFILES[name])` would let a prototype key ("constructor") pass
289
+ // as a known profile and run under an inherited member (fail-open).
290
+ if (!Object.prototype.hasOwnProperty.call(PROFILES, name)) {
288
291
  throw new SafeIcalError("safe-ical/bad-opt",
289
292
  "safeIcal.parse: unknown profile '" + name +
290
293
  "' (expected strict|balanced|permissive)");
291
294
  }
295
+ var caps = PROFILES[name];
292
296
  return caps;
293
297
  }
294
298
 
package/lib/safe-mime.js CHANGED
@@ -750,7 +750,18 @@ function _filenameFromHeaders(headers) {
750
750
  }
751
751
  if (/^[A-Za-z0-9_-]+'[A-Za-z0-9_-]*'/.test(raw)) {
752
752
  var enc = raw.split("'");
753
- return decodeURIComponent(enc[2]);
753
+ // RFC 2231 / 5987 ext-value percent-decode. A hostile
754
+ // `filename*=UTF-8''%` (truncated %-escape, non-hex digits, or a
755
+ // %-escape sequence that isn't valid UTF-8) makes decodeURIComponent
756
+ // throw a raw URIError, which would escape the SafeMimeError contract
757
+ // and crash the attachment walk. Degrade to the still-encoded segment
758
+ // — best-effort, and downstream filename guards handle the raw form —
759
+ // matching every other decodeURIComponent site in the framework.
760
+ try {
761
+ return decodeURIComponent(enc[2]);
762
+ } catch (_e) {
763
+ return enc[2];
764
+ }
754
765
  }
755
766
  return raw;
756
767
  }
package/lib/safe-vcard.js CHANGED
@@ -225,14 +225,18 @@ function _resolveCaps(opts) {
225
225
  if (typeof opts.profile === "string") {
226
226
  name = opts.profile;
227
227
  } else if (typeof opts.compliancePosture === "string") {
228
- name = COMPLIANCE_POSTURES[opts.compliancePosture] || "strict";
228
+ name = Object.prototype.hasOwnProperty.call(COMPLIANCE_POSTURES, opts.compliancePosture)
229
+ ? COMPLIANCE_POSTURES[opts.compliancePosture] : "strict";
229
230
  }
230
- var caps = PROFILES[name];
231
- if (!caps) {
231
+ // Own-property lookup: `name` derives from attacker/operator opts.profile, so
232
+ // a bare `if (!PROFILES[name])` would let a prototype key ("constructor") pass
233
+ // as a known profile and run under an inherited member (fail-open).
234
+ if (!Object.prototype.hasOwnProperty.call(PROFILES, name)) {
232
235
  throw new SafeVcardError("safe-vcard/bad-opt",
233
236
  "safeVcard.parse: unknown profile '" + name +
234
237
  "' (expected strict|balanced|permissive)");
235
238
  }
239
+ var caps = PROFILES[name];
236
240
  return caps;
237
241
  }
238
242
 
package/lib/tsa.js CHANGED
@@ -122,11 +122,11 @@ function _normHex(h) {
122
122
  // Resolve the message imprint: hash the data, or use a pre-computed hash.
123
123
  function _imprint(data, opts, fnName) {
124
124
  var hashName = opts.hashAlg || "SHA-512";
125
- var h = IMPRINT_HASHES[hashName];
126
- if (!h) {
125
+ if (!Object.prototype.hasOwnProperty.call(IMPRINT_HASHES, hashName)) {
127
126
  throw new TsaError("tsa/bad-hash-alg",
128
127
  fnName + ": hashAlg must be one of " + Object.keys(IMPRINT_HASHES).join(" / "));
129
128
  }
129
+ var h = IMPRINT_HASHES[hashName];
130
130
  var digest;
131
131
  if (opts.hashed) {
132
132
  digest = _bytes(data, "hash");
package/lib/vc.js CHANGED
@@ -247,10 +247,10 @@ function _sign(doc, opts, joseTyp, coseTyp, coseContentType, fnName) {
247
247
  });
248
248
  }
249
249
  if (opts.securing === "jose") {
250
- var params = JOSE_ALGS[opts.alg];
251
- if (!params) {
250
+ if (!Object.prototype.hasOwnProperty.call(JOSE_ALGS, opts.alg)) {
252
251
  throw new VcError("vc/bad-alg", fnName + ": JOSE securing requires alg ES256/384/512 or EdDSA (got " + opts.alg + ")");
253
252
  }
253
+ var params = JOSE_ALGS[opts.alg];
254
254
  var key = _toKey(opts.privateKey, "private");
255
255
  var header = { alg: opts.alg, typ: joseTyp };
256
256
  if (typeof opts.kid === "string") header.kid = opts.kid;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.16.40",
3
+ "version": "0.17.1",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:450ccb8a-2242-4a61-a57f-516b6c8e1325",
5
+ "serialNumber": "urn:uuid:04849b6a-ae7a-43f3-939a-e67a488f6a49",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-17T03:08:43.327Z",
8
+ "timestamp": "2026-07-17T08:07:44.038Z",
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.40",
22
+ "bom-ref": "@blamejs/core@0.17.1",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.16.40",
25
+ "version": "0.17.1",
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.40",
29
+ "purl": "pkg:npm/%40blamejs/core@0.17.1",
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.40",
57
+ "ref": "@blamejs/core@0.17.1",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]