@blamejs/core 0.7.75 → 0.7.76
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/log.js +27 -0
- package/lib/static.js +50 -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.7.x
|
|
10
10
|
|
|
11
|
+
- **0.7.76** (2026-05-06) — CVE-class web hardening sweep — Trojan Source (CVE-2021-42574) log defense + drive-by-MIME attachment opt for `staticServe`. **Trojan Source defense** in `b.log` output: every log line now post-processes Unicode bidi / format-control characters (U+061C / U+200E-200F / U+202A-202E / U+2066-2069) into their `\uXXXX` literal escape on the wire so a hostile log message can't re-order the visible line in a TTY / syslog / file reader. JSON.stringify alone does NOT escape these codepoints, so a captured-error message containing `U+202E` (RIGHT-TO-LEFT OVERRIDE) survives into the log surface and silently flips visible field/key associations. The escape applies to the entire serialized JSON line including any extras / bound-context fields. **`staticServe.create({ safeAttachmentForRiskyMimes: true })`** — opt-in flag that adds `Content-Disposition: attachment` to responses whose Content-Type is in the risky-inline-MIME set (`text/html`, `text/xml`, `application/xml`, `application/xhtml+xml`, `image/svg+xml`, `application/javascript`, `text/javascript`, `application/x-javascript`). Defends against drive-by execution of user-uploaded HTML / JS / SVG (CVE-2017-15012 SVG XSS / CVE-2009-1312 HTML drive-by class). Default `false` — operators serving framework asset bundles continue to render inline; operators serving user-content directories opt in. Filename is RFC 5987-encoded (ASCII filename + `filename*=UTF-8''...`) so non-ASCII filenames survive without allowing CR/LF header injection.
|
|
12
|
+
|
|
11
13
|
- **0.7.75** (2026-05-06) — Audit-emit redaction pipeline. `b.audit.safeEmit` now scrubs the `actor` / `reason` / `metadata` fields through `b.redact.redact()` before they hit the audit handler. Operators who pass `metadata: { reason: e.message }` from a caught error could land DB connection strings, bearer tokens, JWT compact-serialization fixtures, AWS access keys, PEM private keys, SSH keys, credit cards, and SSNs in audit rows; the redact pipeline catches the common shapes (sensitive field names + value-shape detectors) and replaces them with markers (`[REDACTED]`, `[REDACTED-CONN-STRING]`, `[REDACTED-JWT]`, `[REDACTED-PEM]`, `[REDACTED-AWS-KEY]`, `[REDACTED-CC]`, `[REDACTED-SSN]`, `[REDACTED-SEALED]`, `[REDACTED-SSH-KEY]`). The `b.redact` primitive existed in lib/ since v0.4.x but was dead code — `audit.safeEmit` previously passed metadata through unchanged. New connection-string detector matches `protocol://user:pass@host` shapes that surface in error messages from external-DB / SMTP / HTTP drivers (RFC 3986 generic syntax). Drop-silent on redact failure — the pipeline never breaks the caller's audit attempt.
|
|
12
14
|
|
|
13
15
|
- **0.7.74** (2026-05-06) — Email receive-side parity: DMARC aggregate (RUA) report parser + ARC trust evaluation + Authentication-Results header builder + TLS-RPT receive-side report parser. Closes the "framework can send mail compliantly but can't receive compliantly" gap. **`b.mail.dmarc.parseAggregateReport(xmlBytes, { contentType? })`** parses RFC 7489 §7.2 aggregate XML reports through the framework's existing `lib/parsers/safe-xml.js` (the existing security-focused XML parser handles XXE / DOCTYPE / entity-expansion defenses by default). Auto-detects gzip via magic bytes (`0x1f 0x8b`) or `Content-Type: application/gzip`. Returns `{ reportMetadata, policyPublished, records, totals }` with per-record source-IP / count / policy-evaluated dispositions / identifiers / DKIM + SPF auth results, plus aggregated `messages` / `aligned` / `notAligned` totals operators want for dashboards. Caps report size at 8 MiB and records-per-report at 10 000. **`b.mail.arc.evaluate(rfc822, { trustedSealers })`** wraps the existing `arc.verify` cryptographic chain check with the operator-side trust decision: given a passing chain, did any hop in the chain belong to a sealer the operator trusts? Returns `{ chainStatus, trusted, trustedHop, trustedDomain }` walking hops most-recent-first so the deepest trusted sealer wins. **`b.mail.authResults.emit({ authservId, results, fold? })`** builds the RFC 8601 Authentication-Results header value — operators consume per-method results from `b.mail.spf.verify` / `b.mail.dmarc.evaluate` / `b.mail.arc.verify`, hand them to `.emit`, and the framework formats the conformant header string with method-specific properties (`smtp.mailfrom`, `header.d`, `header.from`, `policy.iprev`, `policy.ip`, `policy.tls`). Refuses unknown methods / results at config-mistake time. **`b.network.smtp.tlsRpt.parseReport(body, { contentType? })`** is the receive-side counterpart to `tlsRpt.recordShape` / `tlsRpt.submit` — accepts a Buffer or string, auto-detects gzip, parses JSON, validates the RFC 8460 §4.4 required-fields shape (`organization-name`, `date-range`, `report-id`, `policies`), aggregates `total-successful-session-count` / `total-failure-session-count` across policies. Caps report size at 8 MiB and policies-per-report at 1024.
|
package/lib/log.js
CHANGED
|
@@ -279,6 +279,14 @@ function create(opts) {
|
|
|
279
279
|
_logError: "extras not serializable",
|
|
280
280
|
}) + "\n";
|
|
281
281
|
}
|
|
282
|
+
// Trojan-Source defense (CVE-2021-42574). JSON.stringify does NOT
|
|
283
|
+
// escape Unicode bidi / format controls, so a hostile log message
|
|
284
|
+
// containing U+202E (RIGHT-TO-LEFT OVERRIDE) survives into TTY /
|
|
285
|
+
// syslog / file sinks where it can re-order the visible line —
|
|
286
|
+
// forging which fields appear under which keys when the operator
|
|
287
|
+
// reads the log. Escape the entire bidi/format-control set to
|
|
288
|
+
// `\uXXXX` literals on the wire.
|
|
289
|
+
line = _escapeBidiControls(line);
|
|
282
290
|
|
|
283
291
|
var lvlNum = LEVELS[levelName];
|
|
284
292
|
for (var s = 0; s < sinks.length; s++) {
|
|
@@ -479,6 +487,25 @@ function makeViaOrFallback(operatorLog, fallbackLog) {
|
|
|
479
487
|
|
|
480
488
|
// Boot-time minimum level (debug suppressed unless explicitly enabled).
|
|
481
489
|
// Uses raw process.env per the documented load-cycle exception: log.js
|
|
490
|
+
// Trojan-Source defense (CVE-2021-42574). Replace Unicode bidi /
|
|
491
|
+
// format-control characters with their `\uXXXX` literal escape so a
|
|
492
|
+
// hostile log message can't re-order the visible line in a TTY /
|
|
493
|
+
// syslog reader. Set covers:
|
|
494
|
+
// U+061C — Arabic Letter Mark
|
|
495
|
+
// U+200E/U+200F — LRM/RLM
|
|
496
|
+
// U+202A-U+202E — LRE/RLE/PDF/LRO/RLO
|
|
497
|
+
// U+2066-U+2069 — LRI/RLI/FSI/PDI
|
|
498
|
+
var _BIDI_CONTROL_RE = /[]/g;
|
|
499
|
+
|
|
500
|
+
function _escapeBidiControls(s) {
|
|
501
|
+
if (typeof s !== "string" || s.length === 0) return s;
|
|
502
|
+
return s.replace(_BIDI_CONTROL_RE, function (ch) {
|
|
503
|
+
var code = ch.charCodeAt(0).toString(16); // allow:raw-byte-literal — Unicode hex radix
|
|
504
|
+
while (code.length < 4) code = "0" + code;
|
|
505
|
+
return "\\u" + code;
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
482
509
|
// runs before safeEnv on the boot path; safeEnv requires log, so log
|
|
483
510
|
// can't go through safeEnv to read its own level.
|
|
484
511
|
function _bootMinLevel() {
|
package/lib/static.js
CHANGED
|
@@ -208,6 +208,46 @@ function _contentTypeFor(filePath, table) {
|
|
|
208
208
|
return (table && table[ext]) || DEFAULT_CONTENT_TYPES[ext] || "application/octet-stream";
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
// Risky MIMEs that the browser executes inline. When safeAttachment is
|
|
212
|
+
// on, the framework forces Content-Disposition: attachment so
|
|
213
|
+
// drive-by uploads can't be executed in the user's browser. Operators
|
|
214
|
+
// serving trusted assets (CSS / JS bundle / SVG icon) opt out by NOT
|
|
215
|
+
// setting safeAttachmentForRiskyMimes — leaving the existing inline
|
|
216
|
+
// default. The defense is per-CVE-2017-15012 (SVG XSS) /
|
|
217
|
+
// CVE-2009-1312 (HTML drive-by) class.
|
|
218
|
+
var RISKY_INLINE_MIMES = {
|
|
219
|
+
"text/html": true,
|
|
220
|
+
"text/xml": true,
|
|
221
|
+
"application/xml": true,
|
|
222
|
+
"application/xhtml+xml": true,
|
|
223
|
+
"image/svg+xml": true,
|
|
224
|
+
"application/javascript": true,
|
|
225
|
+
"text/javascript": true,
|
|
226
|
+
"application/x-javascript": true,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
function _isRiskyInlineMime(contentType) {
|
|
230
|
+
if (typeof contentType !== "string" || contentType.length === 0) return false;
|
|
231
|
+
// Strip parameters like "; charset=utf-8".
|
|
232
|
+
var semi = contentType.indexOf(";");
|
|
233
|
+
var bare = (semi === -1 ? contentType : contentType.slice(0, semi)).trim().toLowerCase();
|
|
234
|
+
return RISKY_INLINE_MIMES[bare] === true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Build a safe Content-Disposition value for an attachment. The
|
|
238
|
+
// filename is RFC 5987-encoded so non-ASCII characters survive without
|
|
239
|
+
// allowing CR/LF header injection.
|
|
240
|
+
function _attachmentDisposition(filePath) {
|
|
241
|
+
var name = path.basename(filePath);
|
|
242
|
+
// Refuse CR/LF/NUL outright — they're already filtered upstream by
|
|
243
|
+
// the path-traversal guard, but defense-in-depth here.
|
|
244
|
+
if (/[\r\n\0]/.test(name)) name = "download";
|
|
245
|
+
// ASCII-safe filename (replace non-ASCII with _) plus filename* = UTF-8 form.
|
|
246
|
+
var asciiName = name.replace(/[^\x20-\x7e]/g, "_").replace(/["\\]/g, "_");
|
|
247
|
+
var encName = encodeURIComponent(name);
|
|
248
|
+
return 'attachment; filename="' + asciiName + '"; filename*=UTF-8\'\'' + encName;
|
|
249
|
+
}
|
|
250
|
+
|
|
211
251
|
// _parseRangeHeader — RFC 7233 single-range parser. Returns null when:
|
|
212
252
|
// - header absent
|
|
213
253
|
// - syntactically malformed (not `bytes=`, multi-range, suffix syntax
|
|
@@ -331,6 +371,8 @@ function _validateCreateOpts(opts) {
|
|
|
331
371
|
validateOpts.optionalBoolean(opts.acceptRanges, "staticServe.create: acceptRanges", StaticServeError);
|
|
332
372
|
validateOpts.optionalBoolean(opts.auditSuccess, "staticServe.create: auditSuccess", StaticServeError);
|
|
333
373
|
validateOpts.optionalBoolean(opts.auditFailures, "staticServe.create: auditFailures", StaticServeError);
|
|
374
|
+
validateOpts.optionalBoolean(opts.safeAttachmentForRiskyMimes,
|
|
375
|
+
"staticServe.create: safeAttachmentForRiskyMimes", StaticServeError);
|
|
334
376
|
numericBounds.requireNonNegativeFiniteIntIfPresent(opts.maxBytesPerActorPerWindowMs,
|
|
335
377
|
"staticServe.create: maxBytesPerActorPerWindowMs", StaticServeError, "BAD_OPT");
|
|
336
378
|
numericBounds.requireNonNegativeFiniteIntIfPresent(opts.maxBytesAllActorsPerWindowMs,
|
|
@@ -507,6 +549,7 @@ function create(opts) {
|
|
|
507
549
|
var auditSuccess = cfg.auditSuccess;
|
|
508
550
|
var auditFailures = cfg.auditFailures;
|
|
509
551
|
var acceptRanges = cfg.acceptRanges;
|
|
552
|
+
var safeAttachment = !!cfg.safeAttachmentForRiskyMimes;
|
|
510
553
|
var perActorCap = cfg.maxBytesPerActorPerWindowMs;
|
|
511
554
|
var globalCap = cfg.maxBytesAllActorsPerWindowMs;
|
|
512
555
|
var bandwidthWindowMs = cfg.bandwidthWindowMs;
|
|
@@ -851,6 +894,13 @@ function create(opts) {
|
|
|
851
894
|
"Last-Modified": meta.lastModified,
|
|
852
895
|
"X-Integrity": meta.integrity,
|
|
853
896
|
};
|
|
897
|
+
// Drive-by-execution defense — when safeAttachmentForRiskyMimes is
|
|
898
|
+
// on, force Content-Disposition: attachment for HTML / JS / SVG /
|
|
899
|
+
// XML so the browser downloads instead of rendering. Operator's
|
|
900
|
+
// onServe hook can override.
|
|
901
|
+
if (safeAttachment && _isRiskyInlineMime(headers["Content-Type"])) {
|
|
902
|
+
headers["Content-Disposition"] = _attachmentDisposition(absPath);
|
|
903
|
+
}
|
|
854
904
|
if (acceptRanges) headers["Accept-Ranges"] = "bytes";
|
|
855
905
|
if (range) headers["Content-Range"] = "bytes " + range.start + "-" + range.end + "/" + meta.size;
|
|
856
906
|
|
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:16490b30-e9d2-4135-a820-485954fc745f",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-06T04:
|
|
8
|
+
"timestamp": "2026-05-06T04:28:11.034Z",
|
|
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.7.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.7.76",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.7.
|
|
25
|
+
"version": "0.7.76",
|
|
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.7.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.7.76",
|
|
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.7.
|
|
57
|
+
"ref": "@blamejs/core@0.7.76",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|