@blamejs/core 0.8.17 → 0.8.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/db-query.js +75 -1
- package/lib/db.js +11 -4
- package/lib/safe-sql.js +6 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.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.8.x
|
|
10
10
|
|
|
11
|
+
- **0.8.18** (2026-05-08) — Database query-builder hardening on `b.db.from(...).where|whereRaw` and `b.safeSql`. **`Query.where(field, "LIKE", value)`** — now escapes SQL `%` and `_` wildcard metacharacters in operator-supplied values and emits `LIKE ? ESCAPE '\\'`. Closes the column-disclosure class where `q=%@%` enumerated the entire table. Operators who deliberately want LIKE wildcards bypass via `whereRaw()`. **`Query.where(field, "IN", [...])`** — now expands an array to `IN (?, ?, ?)` and binds each value separately. Pre-v0.8.18 the array bound to a single placeholder and silently matched zero rows (node:sqlite `?` doesn't support array-binding). Refuses non-array / empty-array inputs. **`Query.whereRaw(sql, params)`** — placeholder-counting now skips `?` characters inside SQL string literals (single + double quoted, including `''`-doubled escapes), line comments (`-- ...`), and block comments (`/* ... */`). Pre-v0.8.18 the naive regex counted literal-`?` and either threw on count mismatch OR let through fragments with masked missing real placeholders. **`b.safeSql.BANNED_IDENTIFIERS`** — extended to refuse `pragma` / `attach` / `detach` / `analyze` / `vacuum` / `reindex` as bare identifiers. Closes the SQLite-specific escape-the-parameterized-model surface (PRAGMAs disable security-relevant protections; ATTACH mounts external databases). **`b.db.declareTable`** — refuses any app schema name that begins with the framework's `_blamejs_` prefix (was an exact-match Set; an app schema entry like `_blamejs_audit_log_archive` slipped past the gate and could shadow / look-alike framework tables).
|
|
12
|
+
|
|
11
13
|
- **0.8.17** (2026-05-08) — Email auth + DANE / TLS-RPT spec-conformance fixes (ARC / DMARC / SPF / A-R / DANE / TLS-RPT). Ten more RFC-cited gaps closed. **ARC (`b.mail.arc`)** — signer's AMS canonicalization now includes the current hop's `ARC-Authentication-Results` per RFC 8617 §5.1.1 (auto-prepends to `h=` unless operator passes `excludeAarFromAms: true`). Microsoft + Google receivers DO include AAR in `h=`; the prior framework default produced chains those receivers couldn't verify. Verifier counterpart: when reconstructing the AMS canonical input, only the CURRENT hop's AAR is kept (prior-hop AARs stripped). Verifier now enforces `t=` (signing time) + `x=` (expiration) time windows per RFC 8617 §5.2 with operator-tunable `clockSkewMs` (default 5 min) — pre-v0.8.17 the verifier parsed but never enforced. **DMARC (`b.mail.dmarc`)** — multiple `v=DMARC1` records on a domain now treated as having no DMARC record per RFC 7489 §6.6.3 (`_fetchDmarcRecord` returns null on multi-match). Subdomain `sp=` fallback wired: when `_dmarc.<from-domain>` returns no record, the verifier walks one label up to the (heuristic) organizational domain and applies its `sp=` (subdomain policy) — falls back to `p=` when `sp=` absent. Result includes `policyOriginDomain` + `orgDomainPolicyApplied: true` so operators can audit which record drove the verdict. (Heuristic only — operators with PSL needs supply their own `dnsLookup` for full Public Suffix List walk.) **SPF (`b.mail.spf`)** — initial query for the sender's SPF record no longer counts toward the 10-lookup limit per RFC 7208 §4.6.4. Pre-v0.8.17 was off-by-one — senders at the spec ceiling got false `permerror`. **A-R (`b.mail.authResults`)** — result vocabulary is now METHOD-SPECIFIC per RFC 8601 §2.7 (per-method `AR_RESULTS_BY_METHOD` map). The flat `AR_VALID_RESULTS` table previously accepted `hardfail` for DKIM (only valid for DMARC §2.7.4) and accepted `temperror` / `permerror` for methods that don't recognize them. **DANE (`b.network.smtp.dane`)** — `daneTlsa()` now REFUSES to return records unless the caller passes `opts.dnssecValidated: true` per RFC 7672 §1.3 (TLSA records that are not DNSSEC-validated MUST NOT be used). Pre-v0.8.17 silently used un-validated records — silent escalation class. `daneVerifyChain` enforces RFC 6698 §2.1.4 + RFC 7672 §3.1.1 chain-order: a DANE-TA match at chain position `i` is accepted only when its DER Subject equals the DER Issuer of cert at position `i-1` (i.e. it's actually the parent in the chain, not a random non-leaf cert that happens to hash-match). Chain-order is best-effort — synthetic test fixtures without ASN.1-parseable DER fall through with `chainOrderUnverified: true` flagged on the match. **TLS-RPT (`b.network.smtp.tlsRpt.fetchPolicy`)** — record without `rua=` now returns `null` (record ignored) per RFC 8460 §3. Pre-v0.8.17 returned `{ version: "TLSRPTv1", rua: [] }` which operators incorrectly treated as a valid record with no destinations. Bug fix only — no new operator-facing primitives.
|
|
12
14
|
|
|
13
15
|
- **0.8.16** (2026-05-08) — Email auth + transport spec-conformance fixes (DKIM / SPF / MTA-STS / OCSP). Ten RFC-cited gaps closed against shipped primitives. **DKIM (`b.mail.dkim`)** — verifier now refuses any signature whose `h=` tag does not include `from` (RFC 6376 §3.5 cornerstone bypass — without From-coverage the signature does not bind to the visible sender; receivers were treating these as valid); refuses unrecognized `v=` tag values per RFC 6376 §3.5 (only `v=1` accepted); enforces empty `p=` as explicit key revocation per RFC 6376 §3.6.1 (verdict `fail`, not `permerror` — well-formed signature against a withdrawn key); enforces `k=` algorithm-family tag agreement with the signature's `a=` family (e.g. `k=rsa` paired with `a=ed25519-sha256` permerrors per RFC 6376 §3.6.1); selector validator now accepts multi-label selectors per RFC 6376 §3.1 ABNF (`sub-domain *("." sub-domain)` — common for time-rotated keys like `2024.s1`). **SPF (`b.mail.spf`)** — refuses domains that publish multiple `v=spf1` TXT records with `permerror` per RFC 7208 §4.5 (most operators don't realize multi-record SPF was always invalid; this surfaces the misconfig instead of silently picking the first); `include:` mechanism now permerrors when the included domain has no SPF record per RFC 7208 §5.2 (closes the silent-authorization class where `include:gone-domain.example` followed by `+all` would silently allow). **MTA-STS (`b.smtpPolicy.mtaSts.fetch`)** — now requires the `_mta-sts.<domain>` TXT precondition record per RFC 8461 §3.1 before fetching the HTTPS policy (closes the silent-escalation class where the framework would fetch policies for domains that never opted in, AND defeats operator-side rotation when the `id=` in the TXT changes); cache TTL is now bounded by the policy's `max_age` value per RFC 8461 §3.2 (clamped between 1 hour floor and 1 year ceiling) instead of the framework's hardcoded 60-min default. **OCSP (`b.network.tls.ocsp.evaluate`)** — `evaluateOcspResponse` now enforces the `thisUpdate` / `nextUpdate` time window per RFC 6960 §4.2.2.1, rejecting responses whose validity window has expired or hasn't started yet (with operator-tunable `clockSkewMs`, default 5 min). Pre-v0.8.16 a captured "good" response could replay forever even after the cert was revoked; this defeats `requireGood` posture. Bug fix only — no new operator-facing primitives, no surface change beyond the additional refusals.
|
package/lib/db-query.js
CHANGED
|
@@ -117,6 +117,33 @@ class Query {
|
|
|
117
117
|
value = lookup.value;
|
|
118
118
|
}
|
|
119
119
|
cryptoField && _validateField(field);
|
|
120
|
+
if (op === "IN") {
|
|
121
|
+
// node:sqlite ? does not support array-binding. Pre-v0.8.18
|
|
122
|
+
// `where(field, "IN", [1,2,3])` silently bound the entire
|
|
123
|
+
// array to a single placeholder and matched zero rows.
|
|
124
|
+
// Expand to (?, ?, ?) and push each value separately.
|
|
125
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
126
|
+
throw new Error("where IN requires a non-empty array of values");
|
|
127
|
+
}
|
|
128
|
+
var placeholders = value.map(function () { return "?"; }).join(", ");
|
|
129
|
+
this._where.push('"' + field + '" IN (' + placeholders + ")");
|
|
130
|
+
for (var i = 0; i < value.length; i += 1) this._whereParams.push(value[i]);
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
if (op === "LIKE" && typeof value === "string") {
|
|
134
|
+
// Escape SQL LIKE metacharacters % and _ in operator-supplied
|
|
135
|
+
// input. Without this, a single `%` in untrusted input becomes
|
|
136
|
+
// a wildcard that matches everything — a column-disclosure
|
|
137
|
+
// class (`q=%@%` enumerates entire table). Use a backslash as
|
|
138
|
+
// the escape character (uniform across SQLite + Postgres) and
|
|
139
|
+
// emit the corresponding ESCAPE clause so the engine treats it
|
|
140
|
+
// as the escape token. Operators who deliberately want LIKE
|
|
141
|
+
// wildcards in their value bypass via whereRaw().
|
|
142
|
+
var escaped = value.replace(/[\\%_]/g, "\\$&");
|
|
143
|
+
this._where.push('"' + field + '" LIKE ? ESCAPE ' + "'\\\\'");
|
|
144
|
+
this._whereParams.push(escaped);
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
120
147
|
this._where.push('"' + field + '" ' + op + " ?");
|
|
121
148
|
this._whereParams.push(value);
|
|
122
149
|
return this;
|
|
@@ -152,7 +179,14 @@ class Query {
|
|
|
152
179
|
throw new Error("whereRaw: sql must be a non-empty string");
|
|
153
180
|
}
|
|
154
181
|
var p = Array.isArray(params) ? params : (params == null ? [] : [params]);
|
|
155
|
-
|
|
182
|
+
// Count `?` placeholders, but skip occurrences inside string
|
|
183
|
+
// literals ('...' or "..."), line comments (-- to EOL), and
|
|
184
|
+
// block comments (/* ... */). Pre-v0.8.18 the naive regex
|
|
185
|
+
// counted `?` inside literals (e.g. `WHERE name = 'a?b' AND id
|
|
186
|
+
// = ?`) which caused mismatched-count errors OR — worse — let
|
|
187
|
+
// through fragments where the literal-`?` placebo masked a
|
|
188
|
+
// missed real placeholder.
|
|
189
|
+
var holders = _countPlaceholders(sql);
|
|
156
190
|
if (holders !== p.length) {
|
|
157
191
|
throw new Error("whereRaw: " + holders + " placeholder(s) in sql but " +
|
|
158
192
|
p.length + " param(s) supplied");
|
|
@@ -392,6 +426,46 @@ class Query {
|
|
|
392
426
|
}
|
|
393
427
|
}
|
|
394
428
|
|
|
429
|
+
// Count `?` placeholders outside string literals + comments.
|
|
430
|
+
// Tracks SQL single-quoted, double-quoted, line-comment, and block-
|
|
431
|
+
// comment state to avoid counting `?` characters that are part of
|
|
432
|
+
// literal text the SQL engine never interprets as a binding marker.
|
|
433
|
+
function _countPlaceholders(sql) {
|
|
434
|
+
var count = 0;
|
|
435
|
+
var i = 0;
|
|
436
|
+
var len = sql.length;
|
|
437
|
+
while (i < len) {
|
|
438
|
+
var ch = sql.charAt(i);
|
|
439
|
+
var next = i + 1 < len ? sql.charAt(i + 1) : "";
|
|
440
|
+
if (ch === "'" || ch === '"') {
|
|
441
|
+
var quote = ch;
|
|
442
|
+
i += 1;
|
|
443
|
+
while (i < len) {
|
|
444
|
+
if (sql.charAt(i) === quote) {
|
|
445
|
+
// SQL doubles the quote char to escape it within a literal.
|
|
446
|
+
if (sql.charAt(i + 1) === quote) { i += 2; continue; }
|
|
447
|
+
i += 1; break;
|
|
448
|
+
}
|
|
449
|
+
i += 1;
|
|
450
|
+
}
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (ch === "-" && next === "-") {
|
|
454
|
+
while (i < len && sql.charAt(i) !== "\n") i += 1;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (ch === "/" && next === "*") {
|
|
458
|
+
i += 2;
|
|
459
|
+
while (i < len && !(sql.charAt(i) === "*" && sql.charAt(i + 1) === "/")) i += 1;
|
|
460
|
+
i += 2;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (ch === "?") count += 1;
|
|
464
|
+
i += 1;
|
|
465
|
+
}
|
|
466
|
+
return count;
|
|
467
|
+
}
|
|
468
|
+
|
|
395
469
|
function _validateField(field) {
|
|
396
470
|
if (typeof field !== "string" ||
|
|
397
471
|
field.length === 0 ||
|
package/lib/db.js
CHANGED
|
@@ -744,13 +744,20 @@ async function init(opts) {
|
|
|
744
744
|
}
|
|
745
745
|
}
|
|
746
746
|
|
|
747
|
-
// Refuse app schema entries that collide with framework-reserved names
|
|
747
|
+
// Refuse app schema entries that collide with framework-reserved names.
|
|
748
|
+
// Pre-v0.8.18 this was an exact-match Set; an app could ship
|
|
749
|
+
// `_blamejs_audit_log_archive` (or similar prefix-collision) and the
|
|
750
|
+
// framework would silently provision it next to the reserved
|
|
751
|
+
// namespace, allowing a row-by-row look-alike attack against audit
|
|
752
|
+
// archive tooling.
|
|
748
753
|
for (var ri = 0; ri < opts.schema.length; ri++) {
|
|
749
|
-
|
|
754
|
+
var appName = opts.schema[ri].name;
|
|
755
|
+
if (RESERVED_TABLE_NAMES.has(appName) ||
|
|
756
|
+
(typeof appName === "string" && appName.indexOf("_blamejs_") === 0)) {
|
|
750
757
|
throw new DbError("db/reserved-table-name",
|
|
751
|
-
"table name '" +
|
|
758
|
+
"table name '" + appName + "' is reserved by the framework. " +
|
|
752
759
|
"Pick a different name (the framework provisions audit_log, consent_log, " +
|
|
753
|
-
"and _blamejs_* tables automatically).");
|
|
760
|
+
"and any '_blamejs_*'-prefixed tables automatically).");
|
|
754
761
|
}
|
|
755
762
|
}
|
|
756
763
|
|
package/lib/safe-sql.js
CHANGED
|
@@ -58,6 +58,12 @@ var BANNED_IDENTIFIERS = new Set([
|
|
|
58
58
|
"where", "from", "join", "into", "values", "table", "database",
|
|
59
59
|
"schema", "index", "view", "trigger", "procedure", "function",
|
|
60
60
|
"begin", "commit", "rollback", "savepoint",
|
|
61
|
+
// SQLite-specific commands that escape the parameterized-query
|
|
62
|
+
// model. attach/detach mount external databases; pragma changes
|
|
63
|
+
// PRAGMAs (foreign_keys / cell_size_check / trusted_schema /
|
|
64
|
+
// journal_mode etc.) which can disable security-relevant
|
|
65
|
+
// protections; analyze / vacuum drop or rewrite indexes.
|
|
66
|
+
"pragma", "attach", "detach", "analyze", "vacuum", "reindex",
|
|
61
67
|
]);
|
|
62
68
|
|
|
63
69
|
// Default identifier shape — Postgres NAMEDATALEN (63 chars) is the
|
package/package.json
CHANGED
package/sbom.cyclonedx.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:1751cf74-fcc9-495e-a159-bac5eb2565b3",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-07T07:
|
|
8
|
+
"timestamp": "2026-05-07T07:40:58.458Z",
|
|
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.8.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.8.18",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.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.8.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.8.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.8.
|
|
57
|
+
"ref": "@blamejs/core@0.8.18",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|