@blamejs/core 0.17.21 → 0.17.22
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/network-tls.js +69 -7
- 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.17.x
|
|
10
10
|
|
|
11
|
+
- v0.17.22 (2026-07-25) — **`b.network.tls.checkServerIdentity9525` parses subjectAltName quote-aware, closing a hostname-verification bypass.** The strict RFC 9525 identity verifier parsed Node's subjectAltName string with a naive comma split, ignoring the JSON-quoted encoding Node emits for a SAN value that itself contains separators (the CVE-2021-44531/44532 remediation). A certificate whose single dNSName is a quoted blob such as `DNS:"x, DNS:victim.com, y"` was broken into pieces by the split, smuggling a clean `victim.com` token the certificate never asserts -- so checkServerIdentity9525 ACCEPTED a host that Node's own tls.checkServerIdentity REJECTS (ERR_TLS_CERT_ALTNAME_INVALID). The verifier now splits SAN entries on commas outside quotes and JSON-decodes quoted spans, matching Node's splitEscapedAltNames, so it asserts only the certificate's actual dNSName / IP values and is at least as strict as the function it advertises replacing. This path gates peer identity in b.crypto.spkiPinVerifier. **Security:** *checkServerIdentity9525 no longer accepts a hostname smuggled through a quoted subjectAltName* — Node renders a subjectAltName value that contains commas or control characters as a JSON-quoted string, so a single dNSName whose value is `x, DNS:victim.com, y` appears in cert.subjectaltname as `DNS:"x, DNS:victim.com, y"`. The verifier's SAN parser split that raw string on every comma, turning one entry into several and extracting a `DNS:victim.com` token the certificate does not actually present -- a hostname-verification bypass, since a strict RFC 9525 drop-in must be no weaker than the Node function it replaces (which rejects this cert). The parser now mirrors Node's splitEscapedAltNames: it splits only on commas outside quotes and JSON-decodes quoted spans, so each parsed dNSName is exactly one GeneralName and a smuggled name can no longer match. Malformed SAN quoting fails closed (asserts no identifiers). Operators using b.network.tls.checkServerIdentity9525 (directly or via b.crypto.spkiPinVerifier) should upgrade.
|
|
12
|
+
|
|
11
13
|
- v0.17.21 (2026-07-25) — **`blamejs erase` treats only `--confirm true` as confirmation, so `--confirm false` no longer triggers the irreversible erase.** Three fixes to the blamejs command line. The `erase` subcommand (a cryptographic single-row erase) gated its irreversible action on a bare-truthiness check, so `--confirm false` -- a string -- counted as confirmation and the erase proceeded; the gate now accepts only `true` / "true", matching `audit purge`, and refuses anything else. A stray `-v` / `--version` token alongside a subcommand no longer short-circuits to the version print: `blamejs migrate up --db x -v` previously returned 0 after printing the version without running the migration, handing automation a false pass; the version flag is now honored only when it is the whole invocation. And `blamejs dev` now survives a crash of the watched child -- it stays up to hot-restart on the next file change instead of draining the event loop and exiting 0 -- and awaits the child's full termination on SIGINT/SIGTERM so a shutdown cannot orphan it. **Fixed:** *blamejs erase refuses --confirm false instead of performing the irreversible erase* — The erase subcommand confirmed its irreversible action with a bare-truthiness check, so the string `--confirm false` was treated as confirmation and the erase ran. Confirmation is now satisfied only by `--confirm true` (or the bare `--confirm`), matching the check `audit purge` already used; any other value -- including `false` -- is refused with a non-zero exit and no erase. A shared confirmation helper backs every destructive subcommand so the acknowledgement contract cannot drift between them. · *A stray -v / --version no longer turns a subcommand into a silent no-op* — The top-level version flag was evaluated before subcommand dispatch and returned 0 whenever `-v` / `--version` appeared anywhere in the arguments, so a consequential command such as `migrate up`, `seed run`, or `erase` alongside a stray `-v` printed the version and exited 0 without doing anything -- a false success for any script checking the exit code. The version flag is now honored only when the invocation carries no subcommand; otherwise the subcommand runs (or fails) as written. · *blamejs dev survives a watched-child crash and shuts the child down cleanly* — The dev supervisor held the event loop open with an unref'd timer, so when the watched child process crashed the supervisor drained the loop and exited 0 -- defeating crash-resilience (it should stay up and hot-restart on the next file change) and reporting success even though the app had crashed. It now holds a referenced heartbeat so a child crash leaves the supervisor running, and on SIGINT/SIGTERM it awaits the child's full termination (graceful signal, then escalation) before exiting, so a shutdown can no longer race ahead and orphan the child.
|
|
12
14
|
|
|
13
15
|
- v0.17.20 (2026-07-24) — **`b.forms.validate` enforces required checkboxes server-side and rejects malformed field bounds, closing two validation gaps.** b.forms.validate previously let an unchecked required checkbox pass: the renderer emits the HTML `required` attribute for it, but the server-side validator skipped the check, so a client that omitted the box -- or any non-browser caller -- bypassed a constraint the form advertised. It now rejects an unchecked required checkbox, keeping backend validation in lock-step with what the form displays. Separately, a field whose numeric bound (min/max/minlength/maxlength) was not a finite number went silently unenforced -- a NaN comparison is always false -- so the bound became a no-op; validate now throws on such a spec at the entry point, the same way it already rejects a non-precompiled regex pattern. Two smaller fixes: a field rendered without an explicit type now emits type="text" (matching the widget it dispatches to) rather than an empty type attribute, and b.externalDb reports EXPLAIN statements as row-returning so a query plan's rows are not dropped by the local-execution row/no-row chooser. **Changed:** *b.forms.validate option reference corrected* — The validate() reference described length bounds as minLength/maxLength, but the option keys are the HTML-attribute spellings minlength/maxlength (the same keys the renderer reads); the reference and its example are corrected, and the example now uses a numeric min bound on its number field. · *Vendored Public Suffix List refreshed to the current snapshot* — The bundled Public Suffix List that backs domain classification (registrable-domain and public-suffix checks in URL, cookie, and SSRF handling) is updated to the latest upstream publicsuffix.org snapshot, so newly delegated public suffixes and private-domain entries are recognized. **Fixed:** *Required checkboxes are enforced server-side in b.forms.validate* — A checkbox marked required is rendered with the HTML `required` attribute, but validate() treated an unchecked box (which coerces to false) as satisfying the field, so an unchecked required checkbox passed. Any caller that skipped the box -- a scripted client, or a browser with the constraint stripped -- bypassed a documented requirement. validate() now returns an error for an unchecked required checkbox (honouring the field's errorMessages.required), so the backend enforces the same constraint the frontend shows. A form that relied on the previous pass-through for an unchecked required checkbox will now surface a validation error, which is the intended behaviour. · *A malformed numeric field bound is rejected instead of silently ignored* — When a field's min, max, minlength, or maxlength was not a finite number (a non-numeric string, NaN, or Infinity), its comparison could never fire -- a NaN comparison is always false -- so the bound was quietly unenforced while appearing to constrain the field. validate() now throws at the entry point when a defined bound is non-finite, matching how it already requires pattern to be a pre-compiled RegExp. Numeric-string bounds (e.g. min: "1") remain accepted. · *A typeless field renders type="text" instead of an empty type attribute* — b.forms.render dispatches a field with no explicit type to a text input, but the emitted markup carried type="" rather than type="text". The rendered type attribute now matches the widget dispatched, so the markup is self-consistent. · *b.externalDb reports EXPLAIN statements as row-returning* — statementReturnsRows classified a plain EXPLAIN (and an EXPLAIN ANALYZE wrapping a write) as producing no row set, so the local-execution path could route it to a no-row call and drop the query plan. EXPLAIN always returns plan rows to the caller; it is now reported as row-returning whenever its prefix resolves, while an unparseable EXPLAIN prefix stays fail-closed. This is distinct from the cross-border residency read/write classification, which is unchanged.
|
package/lib/network-tls.js
CHANGED
|
@@ -13,6 +13,7 @@ var atomicFile = require("./atomic-file");
|
|
|
13
13
|
var bCrypto = require("./crypto");
|
|
14
14
|
var C = require("./constants");
|
|
15
15
|
var safeBuffer = require("./safe-buffer");
|
|
16
|
+
var safeJson = require("./safe-json");
|
|
16
17
|
var validateOpts = require("./validate-opts");
|
|
17
18
|
var lazyRequire = require("./lazy-require");
|
|
18
19
|
var safeAsync = require("./safe-async");
|
|
@@ -3071,6 +3072,49 @@ function _matchDnsNamePattern(pattern, host) {
|
|
|
3071
3072
|
return true;
|
|
3072
3073
|
}
|
|
3073
3074
|
|
|
3075
|
+
// Node renders a SAN value that contains separators/control chars as a
|
|
3076
|
+
// JSON-quoted string (the CVE-2021-44531/44532 remediation), e.g. a single
|
|
3077
|
+
// dNSName whose value is `x, DNS:y.com` appears as `DNS:"x, DNS:y.com"`. A
|
|
3078
|
+
// naive split(",") would break that one entry into several and smuggle a
|
|
3079
|
+
// clean `DNS:y.com` token the certificate never asserted — a hostname
|
|
3080
|
+
// verification bypass. Split on commas OUTSIDE quotes and JSON-decode quoted
|
|
3081
|
+
// spans, mirroring Node's own splitEscapedAltNames, so each entry is exactly
|
|
3082
|
+
// one GeneralName.
|
|
3083
|
+
var _SAN_QUOTED_RE = /^"(?:[^"\\]|\\.)*"/;
|
|
3084
|
+
function _splitEscapedAltNames(altNames) {
|
|
3085
|
+
var result = [];
|
|
3086
|
+
var current = "";
|
|
3087
|
+
var offset = 0;
|
|
3088
|
+
while (offset !== altNames.length) {
|
|
3089
|
+
var nextSep = altNames.indexOf(",", offset);
|
|
3090
|
+
var nextQuote = altNames.indexOf('"', offset);
|
|
3091
|
+
if (nextQuote !== -1 && (nextSep === -1 || nextQuote < nextSep)) {
|
|
3092
|
+
current += altNames.substring(offset, nextQuote);
|
|
3093
|
+
var match = _SAN_QUOTED_RE.exec(altNames.substring(nextQuote));
|
|
3094
|
+
if (!match) {
|
|
3095
|
+
// Malformed quoting — refuse to parse rather than guess.
|
|
3096
|
+
throw new TlsTrustError("tls/altname-format",
|
|
3097
|
+
"malformed quoted subjectAltName entry");
|
|
3098
|
+
}
|
|
3099
|
+
// match[0] is a JSON string literal (the `_SAN_QUOTED_RE` shape); route
|
|
3100
|
+
// through the framework JSON primitive rather than raw JSON.parse.
|
|
3101
|
+
current += safeJson.parse(match[0]);
|
|
3102
|
+
offset = nextQuote + match[0].length;
|
|
3103
|
+
} else if (nextSep !== -1) {
|
|
3104
|
+
current += altNames.substring(offset, nextSep);
|
|
3105
|
+
result.push(current);
|
|
3106
|
+
current = "";
|
|
3107
|
+
offset = nextSep + 1;
|
|
3108
|
+
if (altNames.charAt(offset) === " ") offset += 1; // skip the ", " gap
|
|
3109
|
+
} else {
|
|
3110
|
+
current += altNames.substring(offset);
|
|
3111
|
+
offset = altNames.length;
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
result.push(current);
|
|
3115
|
+
return result;
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3074
3118
|
function _parseSanString(rawSubjectAltName) {
|
|
3075
3119
|
// Node exposes the SAN as a comma-separated string of typed entries:
|
|
3076
3120
|
// "DNS:foo.example.com, DNS:*.example.com, IP Address:198.51.100.1,
|
|
@@ -3081,13 +3125,27 @@ function _parseSanString(rawSubjectAltName) {
|
|
|
3081
3125
|
if (typeof rawSubjectAltName !== "string" || rawSubjectAltName.length === 0) {
|
|
3082
3126
|
return { dns: dns, ips: ips };
|
|
3083
3127
|
}
|
|
3084
|
-
var entries
|
|
3128
|
+
var entries;
|
|
3129
|
+
try {
|
|
3130
|
+
entries = _splitEscapedAltNames(rawSubjectAltName);
|
|
3131
|
+
} catch (_e) {
|
|
3132
|
+
// Malformed SAN quoting → assert no identifiers (fail closed).
|
|
3133
|
+
return { dns: dns, ips: ips };
|
|
3134
|
+
}
|
|
3085
3135
|
for (var i = 0; i < entries.length; i += 1) {
|
|
3086
|
-
var
|
|
3087
|
-
var colon =
|
|
3136
|
+
var entry = entries[i];
|
|
3137
|
+
var colon = entry.indexOf(":");
|
|
3088
3138
|
if (colon === -1) continue;
|
|
3089
|
-
|
|
3090
|
-
|
|
3139
|
+
// The GeneralName TYPE prefix is emitted by _splitEscapedAltNames exactly
|
|
3140
|
+
// (no surrounding whitespace), and the value MUST be preserved EXACTLY —
|
|
3141
|
+
// NOT trimmed. Trimming would strip leading/trailing control whitespace a
|
|
3142
|
+
// decoded quoted value can carry: a dNSName that is a newline followed by
|
|
3143
|
+
// "victim.com" decodes to "\nvictim.com", and a trim would reduce it to a
|
|
3144
|
+
// bare "victim.com" that matches a name the certificate never asserts — a
|
|
3145
|
+
// bypass. Node compares the decoded value verbatim, so an identifier with
|
|
3146
|
+
// such characters simply fails the exact match and is rejected.
|
|
3147
|
+
var kind = entry.slice(0, colon);
|
|
3148
|
+
var val = entry.slice(colon + 1);
|
|
3091
3149
|
if (kind === "DNS") {
|
|
3092
3150
|
dns.push(val);
|
|
3093
3151
|
} else if (kind === "IP Address" || kind === "IP") {
|
|
@@ -3101,9 +3159,13 @@ function _parseSanString(rawSubjectAltName) {
|
|
|
3101
3159
|
|
|
3102
3160
|
function _normalizeIpForCompare(ip) {
|
|
3103
3161
|
// Lower-case + strip embedded brackets so "[::1]" / "::1" / "::0001"
|
|
3104
|
-
// all compare equal.
|
|
3162
|
+
// all compare equal. Do NOT trim surrounding whitespace: a valid IP never
|
|
3163
|
+
// carries any, and trimming would let a smuggled SAN value like
|
|
3164
|
+
// "1.2.3.4\n" (a decoded quoted entry) normalize to a clean IP and match a
|
|
3165
|
+
// host the certificate never asserts — net.isIP rejects such a value, so
|
|
3166
|
+
// preserving it verbatim fails closed.
|
|
3105
3167
|
if (typeof ip !== "string") return null;
|
|
3106
|
-
var s = ip
|
|
3168
|
+
var s = ip;
|
|
3107
3169
|
if (s.length >= 2 && s.charAt(0) === "[" && s.charAt(s.length - 1) === "]") {
|
|
3108
3170
|
s = s.slice(1, -1);
|
|
3109
3171
|
}
|
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:b4db0315-d0d2-43e1-b74d-de1c039039b7",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-25T15:20:12.927Z",
|
|
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.17.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.17.22",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.17.
|
|
25
|
+
"version": "0.17.22",
|
|
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.17.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.17.22",
|
|
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.17.
|
|
57
|
+
"ref": "@blamejs/core@0.17.22",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|