@blamejs/core 0.16.28 → 0.16.30

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
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.16.x
10
10
 
11
+ - v0.16.30 (2026-07-13) — **Reject a malformed IPv6 address whose '::' compresses no groups (and a leading-zero embedded-IPv4 tail) in the shared IP validator and the CIDR guard, and complete third-party license attribution for the vendored elliptic-curve library and the Public Suffix List.** The shared IPv6 text parser accepted an address where a '::' sat next to a full eight explicit groups (for example 1:2:3:4:5:6:7:8::, ::1:2:3:4:5:6:7:8, or 1:2:3:4:5:6:7::8). RFC 4291 §2.2 defines '::' as an abbreviation for one or more all-zero groups, so those forms compress nothing and are not valid IPv6 text -- Node's built-in net.isIP rejects every one of them. The parser computed the number of groups to insert but only rejected a negative count, so a zero-insert '::' passed. Because each accepted spelling still normalizes to the same address as its canonical form, no allow/deny decision was bypassed, but the validator disagreed with net.isIP and with any strict peer parser on a whole class of inputs. b.mail (SPF/DMARC/RBL/greylist/HELO) and b.guardCidr both parse IPv6 through the affected code, so both now reject these forms. The embedded IPv4 tail of an IPv4-mapped address is also now validated with the framework's strict dotted-quad check, so a leading-zero, octal-ambiguous octet such as ::ffff:01.2.3.4 is refused rather than silently reinterpreted. Separately, the third-party NOTICE file was missing two vendored components that ship in the package -- the @noble/curves elliptic-curve library (MIT, used by the OPRF primitive) and the Mozilla Public Suffix List (MPL-2.0) -- both are now attributed, and a repository supply-chain policy records why the framework's inherent network, configuration, vendored-bundle, and data-file characteristics are expected. **Changed:** *Complete third-party license attribution for the vendored @noble/curves library and the Public Suffix List, and refresh the vendored Public Suffix List* — The NOTICE file, which attributes every third-party component vendored under lib/vendor/, was missing two that ship in the package: the @noble/curves elliptic-curve library (MIT, used by the OPRF primitive) and the Mozilla Public Suffix List (MPL-2.0, used for organizational-domain derivation in DMARC alignment, BIMI scoping, and cookie-scope confinement). Both are now attributed; the Public Suffix List entry records that it is vendored verbatim under MPL-2.0 with its canonical source URL, satisfying MPL-2.0 §3.2. A stale vendored-library version in the same file was also corrected. The vendored Public Suffix List itself was refreshed to the current upstream snapshot so organizational-domain derivation reflects the latest registry delegations. A repository supply-chain policy also documents why the framework's inherent network egress, configuration reads, vendored minified bundles, and permissively- or open-data-licensed data files are expected characteristics rather than findings. **Security:** *b.mail and b.guardCidr reject a malformed IPv6 address whose '::' compresses zero groups, matching RFC 4291 and net.isIP* — The shared IPv6 text parser inserted 8 - (left groups) - (right groups) zero groups for a '::' and rejected only a negative result, so an address with a full eight explicit groups adjacent to a '::' -- 1:2:3:4:5:6:7:8::, ::1:2:3:4:5:6:7:8, 1:2:3:4:5:6:7::8, 1:2:3:4::5:6:7:8 -- was accepted even though RFC 4291 §2.2 requires '::' to stand for at least one all-zero group and net.isIP rejects all of them. The parser now requires a '::' to insert at least one group. Every affected spelling normalized to the same address as its canonical form, so no CIDR or SPF/DMARC allow/deny decision was bypassed; the fix removes a parser divergence from the platform and peer parsers on a whole class of inputs. The same zero-group check was applied to the separate IPv6 parser inside b.guardCidr, and the embedded IPv4 tail of an IPv4-mapped IPv6 address is now validated with the framework's strict dotted-quad check so a leading-zero / octal-ambiguous octet (for example ::ffff:01.2.3.4) is refused instead of reinterpreted. Legitimate compressed and IPv4-mapped addresses are unchanged.
12
+
13
+ - v0.16.29 (2026-07-13) — **Reject a backup manifest's absolute, drive-letter, or NTFS-stream path at validation and resolve every path through the framework path-safety primitive on both backup and restore, and stop a bound-key auth middleware from hanging when a required peer certificate is absent.** Two defects surfaced while covering the backup/restore path handling and the bound-key auth middleware. A backup manifest's file paths were checked only for a leading separator and .., so a Windows drive-letter path such as C:\Windows\evil, or a colon-bearing NTFS alternate-data-stream marker such as db.enc:evil, passed b.backupManifest.validate, and the backup and restore steps joined the caller- or manifest-declared relativePath and encryptedPath directly, without the framework's path-safety primitive -- so a path built from untrusted input could read a file outside dataDir on backup, or aim a restored file (or a blob read) outside the staging directory on restore. validate now rejects a colon anywhere (both the drive-letter prefix and the alternate-data-stream marker) alongside .. and a leading separator, and both b.backupBundle.create and b.restoreBundle resolve every manifest-declared path through b.safePath (which refuses traversal, absolute, drive-letter, UNC, NTFS alternate-data-stream, and reserved-name paths), so each sink is contained even if a path slips past the first-line check. Separately, b.middleware.requireBoundKey dereferenced req.peerCert.raw in its peer-certificate pinning branch when a fingerprint had been pre-attached by upstream mTLS but the certificate object was absent, throwing an uncaught TypeError that rejected the middleware promise -- the request hung with no response instead of a clean fail-closed denial; the branch now guards the certificate and denies. **Fixed:** *b.middleware.requireBoundKey fails closed instead of hanging when a required peer certificate is absent* — In the peer-certificate pinning branch, the middleware read req.peerCert.raw without checking that req.peerCert was present. When an upstream mTLS layer had pre-attached a peer fingerprint (req.peerFingerprint) but no certificate object, that read threw an uncaught TypeError, which rejected the middleware's promise -- the request received no response and hung, rather than a clean fail-closed 401/403. The branch now checks for the certificate before dereferencing it and denies when it is missing, so a peer-cert-pinned key without a usable certificate is refused, not stalled. **Security:** *Backup and restore resolve every filesystem path through b.safePath, and b.backupManifest.validate rejects an absolute, drive-letter, or NTFS-stream path* — A backup manifest's per-file relativePath and encryptedPath were validated only against .. and a leading / or \, so a Windows drive-absolute path (C:\...) or an NTFS alternate-data-stream marker (db.enc:evil) passed b.backupManifest.validate, and the backup, restore, and storage-adapter steps built their filesystem paths with a plain path join of the caller- or manifest-supplied values -- meaning a path built from untrusted input could read a file outside dataDir on backup, aim a decrypted-file write or an encrypted-blob read outside the intended directory on restore, or escape the storage root through the filesystem adapter. validate() now rejects a colon anywhere (covering both the drive-letter prefix and the alternate-data-stream marker) alongside .. and a leading separator, and every filesystem sink -- b.backupBundle.create (the source read), b.restoreBundle (the encrypted-blob read and the restore destination), and the b.backup.bundleAdapterStorage.fsAdapter key resolver -- resolves its path through b.safePath, which refuses traversal, absolute, drive-letter, UNC, NTFS alternate-data-stream, and Windows reserved-name components and contains the result under its base. (The object-store storage adapter builds an object key, not a filesystem path, and keeps its traversal/NUL check -- a colon is a legal object-key character.) Legitimate relative paths back up and restore unchanged.
14
+
11
15
  - v0.16.28 (2026-07-13) — **Fix a Sieve filter bypass where an explicit :comparator silently disabled a header/address/envelope test, plus a JSONPath normalized-path round-trip fault and a rejected daemon cwd option.** Three defects surfaced while covering the Sieve interpreter, JSONPath engine, and daemon supervisor. In b.mail.sieve, an explicit :comparator on a header, address, or envelope test silently disabled the whole test: the Sieve parser did not bind the comparator name to its tag, so the comparator string was consumed as the first positional argument (the header/address name) and every real argument shifted by one -- the test looked up a non-existent header, never matched, and the message fell through to implicit keep, so a discard / fileinto / redirect that should have fired instead delivered the message. The parser now binds :comparator to its tag (RFC 5228 §2.7.3), so the comparator is applied and the arguments stay aligned. b.jsonPath.paths emitted a normalized path containing raw control characters (only ' and \ were escaped), which the query parser then rejected, so a path returned by paths() did not round-trip through query(); control characters are now escaped per RFC 9535 §2.7. And b.daemon.start rejected its own documented cwd option with an unknown-option error; cwd is now accepted and forwarded as the detached child's working directory. **Fixed:** *b.jsonPath.paths emits a normalized path that round-trips through b.jsonPath.query* — A normalized path returned by paths() escaped only the single-quote and backslash characters inside a member name, leaving raw control characters (newline, tab, and the rest of U+0000 through U+001F) in the output. The query parser rejects an unescaped control character in a name selector, so such a path could not be fed back into query(). Control characters in a normalized-path name are now escaped as their short form (\b \t \n \f \r) or \uXXXX, per RFC 9535 §2.7, so paths() output round-trips. · *b.daemon.start accepts and applies the documented cwd option* — The start() options validator did not list cwd among the accepted options, so passing the documented cwd (the working directory for the detached child) was rejected with a daemon/bad-opts unknown-option error and the feature was unusable. cwd is now an accepted optional string and is forwarded to the detached child spawn as its working directory. **Security:** *b.mail.sieve applies an explicit :comparator instead of silently disabling the test* — A Sieve script that used an explicit :comparator on a header, address, or envelope test -- for example header :comparator "i;octet" :is "Subject" "..." -- silently stopped filtering. The parser did not attach the comparator's value string to the :comparator tag, so it was consumed as the first positional argument (the header or address name) and shifted the real name and key list by one position. The test then queried a non-existent header, never matched, and evaluation fell through to implicit keep, so a discard, fileinto, or reject the operator intended was not applied and the message was delivered anyway -- a filter bypass for any rule with an explicit comparator. The parser now binds :comparator to its tag per RFC 5228 §2.7.3, so the chosen comparator (i;octet exact vs the default i;ascii-casemap case-insensitive) is applied and the positional arguments stay aligned; a :comparator with no following comparator-name string is refused at parse time.
12
16
 
13
17
  - v0.16.27 (2026-07-12) — **Fix b.pubsub pattern subscriptions silently dropping every message for a single-wildcard pattern such as orders.*.created.** b.pubsub.subscribePattern with a * wildcard between dotted segments never matched any channel, so a pattern subscriber received nothing. The documented flagship pattern orders.*.created silently dropped orders.eu.created, and the same held for a.*.c, a.*.b.*.c, and any pattern whose wildcard sits between literal segments -- the source @example itself did not work. The segment matcher required a literal that follows a * to fit entirely before the next dot, but a trailing literal like .created contains a dot and legitimately begins before that boundary, so the match was rejected. The matcher now lets a post-wildcard literal begin at or before the next dot and matches it verbatim, while a single-segment * still never spans a dot -- so orders.*.created matches orders.eu.created but not orders.eu.fr.created or orders.created, and a pattern subscriber never receives a message from an extra path segment. Surfaced while covering the pub/sub matcher's branches. **Fixed:** *b.pubsub.subscribePattern matches a single-segment wildcard between literal segments* — A pattern with a * between dotted segments (the documented orders.*.created, or any a.*.c / a.*.b.*.c) matched no channel at all, so the pattern subscriber's handler never fired. The matcher rejected the literal that follows a wildcard unless it fit entirely before the next dot, but a trailing literal such as .created contains a dot and starts before that boundary, so a legitimate match was dropped. The literal after a wildcard is now allowed to begin at or before the next dot and is matched verbatim; the wildcard still matches exactly one non-dot segment, so orders.*.created matches orders.eu.created but not orders.eu.fr.created (two segments) or orders.created (none) -- a pattern subscription never widens to an extra path segment. Behaviour is now consistent with the documented example.
package/NOTICE CHANGED
@@ -25,6 +25,16 @@ Used for: XChaCha20-Poly1305 authenticated encryption (lib/crypto.js,
25
25
  lib/vault-wrap.js, via lib/vendor/noble-ciphers.cjs).
26
26
  Thank you to Paul Miller for the audited noble-ciphers suite.
27
27
  --------------------------------------------------------------------------------
28
+ Component: @noble/curves
29
+ Version: 2.2.0
30
+ Source: https://github.com/paulmillr/noble-curves
31
+ License: MIT
32
+ Copyright: Copyright (c) 2022 Paul Miller (https://paulmillr.com)
33
+ Used for: RFC 9497 Oblivious Pseudo-Random Function (OPRF / VOPRF / POPRF)
34
+ over ristretto255 / P-256 / P-384 / P-521 (lib/crypto-oprf.js,
35
+ via lib/vendor/noble-curves.cjs). Thank you to Paul Miller for the
36
+ audited noble-curves suite.
37
+ --------------------------------------------------------------------------------
28
38
  Component: @noble/post-quantum
29
39
  Version: 0.6.1
30
40
  Source: https://github.com/paulmillr/noble-post-quantum
@@ -41,7 +51,7 @@ Used for: FIPS 203 ML-KEM (ml_kem_512 / ml_kem_768 / ml_kem_1024),
41
51
  reference implementation.
42
52
  --------------------------------------------------------------------------------
43
53
  Component: @simplewebauthn/server
44
- Version: 13.3.0
54
+ Version: 13.3.2
45
55
  Source: https://github.com/MasterKale/SimpleWebAuthn
46
56
  License: MIT
47
57
  Copyright: Copyright (c) Matthew Miller
@@ -79,3 +89,18 @@ Used for: Top-10000 most-common (breach-derived) passwords. Loaded by
79
89
  project maintainers for keeping a curated, freely-redistributable
80
90
  baseline.
81
91
  --------------------------------------------------------------------------------
92
+ Component: publicsuffix-list (Mozilla Public Suffix List)
93
+ Version: master snapshot (bundled 2026-07-13)
94
+ Source: https://publicsuffix.org/list/public_suffix_list.dat
95
+ License: MPL-2.0
96
+ Copyright: Copyright (c) Mozilla Foundation and Public Suffix List contributors
97
+ Used for: Canonical catalog of effective top-level domains. Loaded at module
98
+ init by b.publicSuffix to derive organizational domains for
99
+ DMARCbis (psd= / np=) alignment, BIMI issuer scoping, cookie-scope
100
+ confinement, and same-site policy (lib/public-suffix.js, via
101
+ lib/vendor/public-suffix-list.dat). This is data, not a code
102
+ bundle. The list is vendored verbatim and unmodified and remains
103
+ licensed under the Mozilla Public License 2.0; its complete source
104
+ is available at the canonical URL above, satisfying MPL-2.0 §3.2.
105
+ The full MPL-2.0 text is at https://www.mozilla.org/MPL/2.0/.
106
+ --------------------------------------------------------------------------------
@@ -51,6 +51,7 @@
51
51
  var nodeFs = require("node:fs");
52
52
  var nodePath = require("node:path");
53
53
  var atomicFile = require("../atomic-file");
54
+ var safePath = require("../safe-path");
54
55
  var bCrypto = require("./crypto");
55
56
  var backupManifest = require("./manifest");
56
57
  var validateOpts = require("../validate-opts");
@@ -122,11 +123,20 @@ async function create(opts) {
122
123
  throw new BackupBundleError("backup-bundle/bad-include",
123
124
  "create: files[" + i + "] requires { relativePath: string }");
124
125
  }
125
- if (entry.relativePath.indexOf("..") !== -1 || /^[/\\]/.test(entry.relativePath)) {
126
+ if (entry.relativePath.indexOf("..") !== -1 || /^[/\\]/.test(entry.relativePath) ||
127
+ entry.relativePath.indexOf(":") !== -1) {
128
+ // A colon anywhere is refused alongside '..' and a leading separator: it
129
+ // covers both a Windows drive prefix (C:\..., absolute) and an NTFS
130
+ // alternate-data-stream marker (db.enc:evil). This friendly pre-screen
131
+ // mirrors b.backupManifest.validate; safePath.resolve below is the
132
+ // authoritative sink for the residual classes (UNC, reserved name, etc.).
126
133
  throw new BackupBundleError("backup-bundle/bad-include",
127
- "create: files[" + i + "].relativePath must be a relative path (got '" + entry.relativePath + "')");
134
+ "create: files[" + i + "].relativePath must be a relative path without '..', a leading separator, or a colon (got '" + entry.relativePath + "')");
128
135
  }
129
- var srcPath = nodePath.join(dataDir, entry.relativePath);
136
+ // Resolve the source path THROUGH dataDir with safePath so a relativePath
137
+ // built from untrusted input can't read a file outside dataDir even if it
138
+ // slips the first-line check (UNC, reserved name, encoded separator, bidi).
139
+ var srcPath = safePath.resolve(dataDir, entry.relativePath);
130
140
  if (!nodeFs.existsSync(srcPath)) {
131
141
  if (entry.required) {
132
142
  throw new BackupBundleError("backup-bundle/missing-required",
@@ -55,6 +55,7 @@ var os = require("node:os");
55
55
  var nodePath = require("node:path");
56
56
  var bCrypto = require("../crypto");
57
57
  var atomicFile = require("../atomic-file");
58
+ var safePath = require("../safe-path");
58
59
  var C = require("../constants");
59
60
  var backupBundle = require("./bundle");
60
61
  var frameworkFiles = require("../framework-files");
@@ -2197,13 +2198,23 @@ bundleAdapterStorage.fsAdapter = function (fsOpts) {
2197
2198
  atomicFile.ensureDir(root);
2198
2199
 
2199
2200
  function _keyPath(key) {
2200
- // Refuse keys with traversal segments defense in depth even
2201
- // though the storage layer also checks.
2202
- if (key.indexOf("..") !== -1 || key.indexOf("\0") !== -1) {
2201
+ // Gate the key under Windows path semantics REGARDLESS of the host, then
2202
+ // resolve the real on-disk path with the host's semantics. An fsAdapter
2203
+ // store is portable a bundle written on one OS is often read on another —
2204
+ // and win32 splits on both `/` and `\`, so a key like `..\evil` (a harmless
2205
+ // literal filename under POSIX, but a climb out of root when the same key is
2206
+ // later interpreted on Windows) is refused everywhere. The gate is a strict
2207
+ // superset of the host's, also refusing an absolute / drive-letter / UNC
2208
+ // prefix, an NTFS alternate-data-stream marker, a Windows reserved name, and
2209
+ // NUL / control bytes; defense in depth even though the storage layer also
2210
+ // checks. A hand-rolled '..' + NUL substring check (the prior shape) missed
2211
+ // the drive / ADS / reserved-name class the shared primitive handles, and a
2212
+ // host-only resolve missed the cross-platform backslash-traversal class.
2213
+ if (safePath.resolveOrNull(root, key, { platform: "win32" }) === null) {
2203
2214
  throw new BackupError("backup/bad-key",
2204
- "fsAdapter: key contains invalid characters: " + JSON.stringify(key));
2215
+ "fsAdapter: key contains invalid or unsafe path characters: " + JSON.stringify(key));
2205
2216
  }
2206
- return nodePath.join(root, key);
2217
+ return safePath.resolve(root, key);
2207
2218
  }
2208
2219
 
2209
2220
  return {
@@ -121,14 +121,19 @@ function _validateFileEntry(f, idx, errors) {
121
121
  }
122
122
  if (typeof f.relativePath !== "string" || f.relativePath.length === 0) {
123
123
  errors.push("files[" + idx + "].relativePath: required non-empty string");
124
- } else if (f.relativePath.indexOf("..") !== -1 || /^[/\\]/.test(f.relativePath)) {
125
- // No traversal — when restored, relativePath joins under dataDir
126
- errors.push("files[" + idx + "].relativePath: must be a relative path without '..' or leading separator");
124
+ } else if (f.relativePath.indexOf("..") !== -1 || /^[/\\]/.test(f.relativePath) || f.relativePath.indexOf(":") !== -1) {
125
+ // No traversal — when restored, relativePath joins under dataDir. A colon
126
+ // anywhere is refused alongside '..' and a leading separator: it covers
127
+ // both a Windows drive prefix (C:\..., absolute — path.resolve would honor
128
+ // it and escape dataDir) and an NTFS alternate-data-stream marker
129
+ // (db.enc:evil), so validate() fails closed before restore, matching the
130
+ // safePath sink b.restoreBundle resolves both paths through.
131
+ errors.push("files[" + idx + "].relativePath: must be a relative path without '..', a leading separator, or a colon (drive letter / NTFS data-stream marker)");
127
132
  }
128
133
  if (typeof f.encryptedPath !== "string" || f.encryptedPath.length === 0) {
129
134
  errors.push("files[" + idx + "].encryptedPath: required non-empty string");
130
- } else if (f.encryptedPath.indexOf("..") !== -1 || /^[/\\]/.test(f.encryptedPath)) {
131
- errors.push("files[" + idx + "].encryptedPath: must be a relative path without '..' or leading separator");
135
+ } else if (f.encryptedPath.indexOf("..") !== -1 || /^[/\\]/.test(f.encryptedPath) || f.encryptedPath.indexOf(":") !== -1) {
136
+ errors.push("files[" + idx + "].encryptedPath: must be a relative path without '..', a leading separator, or a colon (drive letter / NTFS data-stream marker)");
132
137
  }
133
138
  if (typeof f.size !== "number" || !Number.isInteger(f.size) || f.size < 0) {
134
139
  errors.push("files[" + idx + "].size: required non-negative integer");
package/lib/guard-cidr.js CHANGED
@@ -182,6 +182,11 @@ function _parseIpv6(s) {
182
182
  if (left === null || right === null) return null;
183
183
  var pad = IPV6_GROUPS - left.length - right.length;
184
184
  if (pad < 0) return null;
185
+ // RFC 4291 §2.2 — "::" must compress at least one all-zero group. Eight
186
+ // explicit groups adjacent to a "::" (pad === 0) compress nothing and are
187
+ // malformed IPv6 text (net.isIP rejects them), so reject rather than accept
188
+ // a non-canonical spelling into CIDR matching.
189
+ if (pad === 0) return null;
185
190
  var zeros = [];
186
191
  for (var z = 0; z < pad; z += 1) zeros.push("0000"); // IPv6 zero group
187
192
  groups = left.concat(zeros).concat(right);
package/lib/ip-utils.js CHANGED
@@ -30,8 +30,12 @@ function expandIpv6Hex(ip) {
30
30
  // RFC 4291 §2.5.5.2 IPv4-mapped / dual-stack: accept ".d.d.d.d" tail.
31
31
  var dual = ip.match(/^(.*?):(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/); // allow:regex-no-length-cap — dotted-quad has fixed shape; LHS bounded by IPv6 group cap below
32
32
  if (dual) {
33
+ // The embedded IPv4 tail must be a strict RFC 791 dotted-quad. Reuse the
34
+ // canonical isIPv4 (rejects octets > 255 AND leading-zero / octal-ambiguous
35
+ // octets like "01") so this mapped-form parse doesn't diverge from net.isIP
36
+ // or from the standalone isIPv4 validator on the same dotted-quad.
37
+ if (!isIPv4(dual[2])) return null;
33
38
  var v4 = dual[2].split(".").map(Number);
34
- if (v4.some(function (o) { return !(o >= 0 && o <= 255); })) return null; // IPv4 octet range
35
39
  var hi = (v4[0] << 8) | v4[1]; // 16-bit group pack
36
40
  var lo = (v4[2] << 8) | v4[3]; // 16-bit group pack
37
41
  ip = dual[1] + ":" + hi.toString(16) + ":" + lo.toString(16);
@@ -43,6 +47,12 @@ function expandIpv6Hex(ip) {
43
47
  if (dblColon.length === 1 && leftGroups.length !== 8) return null; // RFC 4291 IPv6 group count
44
48
  var fillCount = 8 - leftGroups.length - rightGroups.length; // RFC 4291 IPv6 group count
45
49
  if (fillCount < 0) return null;
50
+ // RFC 4291 §2.2 — "::" abbreviates ONE OR MORE groups of all-zero 16-bit
51
+ // fields, so when a "::" is present it must insert at least one group. Eight
52
+ // explicit groups plus a "::" (e.g. 1:2:3:4:5:6:7:8:: or 1:2:3:4:5:6:7::8)
53
+ // compresses zero groups and is not valid IPv6 text; net.isIP rejects these,
54
+ // so reject them here too rather than diverge from the kernel/peer parser.
55
+ if (dblColon.length === 2 && fillCount === 0) return null;
46
56
  var fill = [];
47
57
  for (var f = 0; f < fillCount; f += 1) fill.push("0");
48
58
  var groups = leftGroups.concat(fill).concat(rightGroups);
@@ -289,7 +289,7 @@ function create(opts) {
289
289
  keyId: record.id || null,
290
290
  });
291
291
  }
292
- } else if (!bCrypto().isCertRevoked(req.peerCert.raw, pinned)) {
292
+ } else if (!(req.peerCert && req.peerCert.raw) || !bCrypto().isCertRevoked(req.peerCert.raw, pinned)) {
293
293
  // isCertRevoked returns true on MATCH against the deny-list
294
294
  // shape; we use it here as a fingerprint-set membership test
295
295
  // because it does the same constant-time hex/colon comparison
@@ -50,6 +50,7 @@
50
50
  var nodeFs = require("node:fs");
51
51
  var nodePath = require("node:path");
52
52
  var atomicFile = require("./atomic-file");
53
+ var safePath = require("./safe-path");
53
54
  var C = require("./constants");
54
55
  var backupCrypto = require("./backup/crypto");
55
56
  var backupManifest = require("./backup/manifest");
@@ -226,7 +227,11 @@ async function extract(opts) {
226
227
  continue;
227
228
  }
228
229
 
229
- var blobPath = nodePath.join(bundleDir, entry.encryptedPath);
230
+ // Resolve the manifest-declared encrypted-blob path THROUGH the base with
231
+ // safePath -- a tampered manifest could otherwise carry a traversal /
232
+ // absolute / drive-letter / NTFS-ADS encryptedPath and read a file outside
233
+ // the bundle. manifest.validate is a first-line check; this is the sink.
234
+ var blobPath = safePath.resolve(bundleDir, entry.encryptedPath);
230
235
  // Cap the read to the manifest's declared encryptedSize so an oversize-on-
231
236
  // disk blob is refused BEFORE it is read into memory (was: read fully, then
232
237
  // compare → an OOM window for a huge swapped blob). A valid blob is exactly
@@ -282,7 +287,10 @@ async function extract(opts) {
282
287
  " — bundle is corrupted or manifest tampered");
283
288
  }
284
289
 
285
- var destPath = nodePath.join(stagingDir, entry.relativePath);
290
+ // Resolve the restore destination THROUGH the staging base with safePath
291
+ // so a tampered relativePath (traversal / absolute / drive-letter / ADS)
292
+ // cannot escape stagingDir when the decrypted file is written.
293
+ var destPath = safePath.resolve(stagingDir, entry.relativePath);
286
294
  atomicFile.ensureDir(nodePath.dirname(destPath));
287
295
  atomicFile.writeSync(destPath, plaintext, { fileMode: 0o600 });
288
296
 
@@ -18,7 +18,7 @@
18
18
  "hashes": {
19
19
  "server": "sha256:5d539dfc9ef47121d4c09bd7256d76448a1f5ac47ee09ac44c78ff6a062af9ab"
20
20
  },
21
- "refreshedAt": "2026-07-10T14:22:17.414Z"
21
+ "refreshedAt": "2026-07-13T22:58:54.983Z"
22
22
  },
23
23
  "@noble/curves": {
24
24
  "version": "2.2.0",
@@ -40,7 +40,7 @@
40
40
  "hashes": {
41
41
  "server": "sha256:ebf254d5eb56aef8705a1c4af9603f47987b4870a9bb5e657e06907b701e2731"
42
42
  },
43
- "refreshedAt": "2026-07-10T14:22:17.414Z"
43
+ "refreshedAt": "2026-07-13T22:58:54.983Z"
44
44
  },
45
45
  "@noble/post-quantum": {
46
46
  "version": "0.6.1",
@@ -71,7 +71,7 @@
71
71
  "hashes": {
72
72
  "server": "sha256:f9190309daadca4c2e2cc2b76beaa6b96e463429cc3c390bd9f0ceaf7b588c68"
73
73
  },
74
- "refreshedAt": "2026-07-10T14:22:17.414Z"
74
+ "refreshedAt": "2026-07-13T22:58:54.983Z"
75
75
  },
76
76
  "@simplewebauthn/server": {
77
77
  "version": "13.3.2",
@@ -94,7 +94,7 @@
94
94
  "hashes": {
95
95
  "server": "sha256:49411d893f5e9b0e2fcaa564b4ec7921f73a9a06229b5e53d49c1453ea1a365c"
96
96
  },
97
- "refreshedAt": "2026-07-10T14:22:17.414Z"
97
+ "refreshedAt": "2026-07-13T22:58:54.983Z"
98
98
  },
99
99
  "SecLists-common-passwords-top-10000": {
100
100
  "version": "10k-most-common (master)",
@@ -114,7 +114,7 @@
114
114
  },
115
115
  "runtime_artifact": "lib/vendor/common-passwords-top-10000.data.js",
116
116
  "integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
117
- "refreshedAt": "2026-07-10T14:22:17.414Z"
117
+ "refreshedAt": "2026-07-13T22:58:54.983Z"
118
118
  },
119
119
  "bimi-trust-anchors": {
120
120
  "version": "operator-managed",
@@ -139,7 +139,7 @@
139
139
  },
140
140
  "runtime_artifact": "lib/vendor/bimi-trust-anchors.data.js",
141
141
  "integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
142
- "refreshedAt": "2026-07-10T14:22:17.414Z"
142
+ "refreshedAt": "2026-07-13T22:58:54.983Z"
143
143
  },
144
144
  "publicsuffix-list": {
145
145
  "version": "master",
@@ -152,14 +152,14 @@
152
152
  "data_js": "lib/vendor/public-suffix-list.data.js"
153
153
  },
154
154
  "bundler": "curl https://publicsuffix.org/list/public_suffix_list.dat",
155
- "bundledAt": "2026-06-24T00:00:00Z",
155
+ "bundledAt": "2026-07-13T00:00:00Z",
156
156
  "hashes": {
157
- "server": "sha256:b86d21bda9c627f0fdbb0182a16a441992e53dc74eb3e866e7885660df863dbd",
158
- "data_js": "sha256:72fb435e03a25bece67f744601e75e3fb7972f264336ddf7cfcb13ddccedb73e"
157
+ "server": "sha256:b6270a246d7bffbe85ad1fb2908abad74828ac5b95bad81cd0cde236955ce444",
158
+ "data_js": "sha256:13b7fa0a62d7b86e5a46468d5d252addd4e54f20143ef3377a5f042b36abf22e"
159
159
  },
160
160
  "runtime_artifact": "lib/vendor/public-suffix-list.data.js",
161
161
  "integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
162
- "refreshedAt": "2026-07-10T14:22:17.414Z"
162
+ "refreshedAt": "2026-07-13T22:58:54.983Z"
163
163
  },
164
164
  "peculiar-pki": {
165
165
  "version": "2.0.0+pkijs-3.4.0",
@@ -190,7 +190,7 @@
190
190
  "hashes": {
191
191
  "server": "sha256:9bbc191afaaa2b1e5757f00480457c08134cdc2c55d541df18d9155bba9cbf77"
192
192
  },
193
- "refreshedAt": "2026-07-10T14:22:17.414Z"
193
+ "refreshedAt": "2026-07-13T22:58:54.983Z"
194
194
  }
195
195
  }
196
196
  }
@@ -5,8 +5,8 @@
5
5
  // Please pull this list from, and only from https://publicsuffix.org/list/public_suffix_list.dat,
6
6
  // rather than any other VCS sites. Pulling from any other URL is not guaranteed to be supported.
7
7
 
8
- // VERSION: 2026-07-09_11-59-23_UTC
9
- // COMMIT: b9a86cf0cd115f1e60b5815533f3fcfd2f9e8f4b
8
+ // VERSION: 2026-07-13_21-14-34_UTC
9
+ // COMMIT: 8eb9e60139cb2c62ccec664554adae3767dc1374
10
10
 
11
11
  // Instructions on pulling and using this list can be found at https://publicsuffix.org/list/.
12
12
 
@@ -1176,12 +1176,16 @@ gov.gd
1176
1176
  // Confirmed by registry <info@nic.ge> 2024-11-20
1177
1177
  ge
1178
1178
  com.ge
1179
+ cyb.ge
1179
1180
  edu.ge
1180
1181
  gov.ge
1182
+ llc.ge
1181
1183
  net.ge
1184
+ online.ge
1182
1185
  org.ge
1183
1186
  pvt.ge
1184
1187
  school.ge
1188
+ tnx.ge
1185
1189
 
1186
1190
  // gf : https://www.iana.org/domains/root/db/gf.html
1187
1191
  gf
@@ -1464,11 +1468,14 @@ tv.im
1464
1468
  // Please note, that nic.in is not an official eTLD, but used by most
1465
1469
  // government institutions.
1466
1470
  // Confirmed by Gaurav Kansal <gaurav.kansal@nic.in> 2025-11-06
1471
+ // Added aero.in, alumni.in, school.in and ub.in by Gaurav Kansal <gaurav.kansal@nic.in> 2026-06-25
1467
1472
  in
1468
1473
  5g.in
1469
1474
  6g.in
1470
1475
  ac.in
1476
+ aero.in
1471
1477
  ai.in
1478
+ alumni.in
1472
1479
  am.in
1473
1480
  bank.in
1474
1481
  bihar.in
@@ -1503,8 +1510,10 @@ pg.in
1503
1510
  post.in
1504
1511
  pro.in
1505
1512
  res.in
1513
+ school.in
1506
1514
  travel.in
1507
1515
  tv.in
1516
+ ub.in
1508
1517
  uk.in
1509
1518
  up.in
1510
1519
  us.in
@@ -4336,14 +4345,18 @@ no
4336
4345
  fhs.no
4337
4346
  folkebibl.no
4338
4347
  fylkesbibl.no
4348
+ gielda.no
4349
+ herad.no
4339
4350
  idrett.no
4351
+ kommune.no
4340
4352
  museum.no
4341
4353
  priv.no
4354
+ suohkan.no
4355
+ tjielte.no
4356
+ uenorge.no
4342
4357
  vgs.no
4343
4358
  // Norid category second-level domains managed by parties other than Norid : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-d/
4344
4359
  dep.no
4345
- herad.no
4346
- kommune.no
4347
4360
  mil.no
4348
4361
  stat.no
4349
4362
  // Norid geographical second level domains : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-b/
@@ -4484,7 +4497,7 @@ askøy.no
4484
4497
  askvoll.no
4485
4498
  asnes.no
4486
4499
  åsnes.no
4487
- audnedaln.no
4500
+ audnedal.no
4488
4501
  aukra.no
4489
4502
  aure.no
4490
4503
  aurland.no
@@ -4595,7 +4608,6 @@ forsand.no
4595
4608
  fosnes.no
4596
4609
  fræna.no
4597
4610
  frana.no
4598
- frei.no
4599
4611
  frogn.no
4600
4612
  froland.no
4601
4613
  frosta.no
@@ -4646,6 +4658,7 @@ halden.no
4646
4658
  halsa.no
4647
4659
  hamar.no
4648
4660
  hamaroy.no
4661
+ hamarøy.no
4649
4662
  hammarfeasta.no
4650
4663
  hámmárfeasta.no
4651
4664
  hammerfest.no
@@ -4700,6 +4713,7 @@ karasjohka.no
4700
4713
  kárášjohka.no
4701
4714
  karasjok.no
4702
4715
  karlsoy.no
4716
+ karlsøy.no
4703
4717
  karmoy.no
4704
4718
  karmøy.no
4705
4719
  kautokeino.no
@@ -4888,6 +4902,7 @@ ralingen.no
4888
4902
  rana.no
4889
4903
  randaberg.no
4890
4904
  rauma.no
4905
+ re.no
4891
4906
  rendalen.no
4892
4907
  rennebu.no
4893
4908
  rennesoy.no
@@ -5045,6 +5060,7 @@ tysvær.no
5045
5060
  tysvar.no
5046
5061
  ullensaker.no
5047
5062
  ullensvang.no
5063
+ ulstein.no
5048
5064
  ulvik.no
5049
5065
  unjarga.no
5050
5066
  unjárga.no
@@ -14231,6 +14247,12 @@ iopsys.se
14231
14247
  // Submitted by Matthew Hardeman <mhardeman@ipifony.com>
14232
14248
  ipifony.net
14233
14249
 
14250
+ // IPv64.net : https://ipv64.net/
14251
+ // Submitted by Dennis Schröder <info@ipv64.net>
14252
+ home64.de
14253
+ ipv64.de
14254
+ ipv64.net
14255
+
14234
14256
  // ir.md : https://nic.ir.md
14235
14257
  // Submitted by Ali Soizi <info@nic.ir.md>
14236
14258
  ir.md
@@ -15636,9 +15658,10 @@ dedibox.fr
15636
15658
  schokokeks.net
15637
15659
 
15638
15660
  // Scottish Government : https://www.gov.scot
15639
- // Submitted by Martin Ellis <martin.ellis@gov.scot>
15661
+ // Submitted by Martin Ellis <digital-publishing@gov.scot>
15640
15662
  gov.scot
15641
15663
  service.gov.scot
15664
+ mygov.scot
15642
15665
 
15643
15666
  // Scry Security : http://www.scrysec.com
15644
15667
  // Submitted by Shante Adam <shante@skyhat.io>