@blamejs/core 0.18.53 → 0.18.55
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 +228 -0
- package/NOTICE +1 -1
- package/README.md +5 -5
- package/lib/agent-audit.js +27 -2
- package/lib/ai-adverse-decision.js +18 -2
- package/lib/audit-sign.js +24 -5
- package/lib/auth/passkey.js +4 -1
- package/lib/codepoint-class.js +72 -0
- package/lib/cookies.js +7 -10
- package/lib/credential-hash.js +8 -1
- package/lib/crypto.js +7 -5
- package/lib/db-file-lifecycle.js +14 -3
- package/lib/db.js +505 -49
- package/lib/guard-auth.js +34 -11
- package/lib/guard-filename.js +41 -33
- package/lib/guard-html.js +10 -2
- package/lib/guard-list-unsubscribe.js +6 -1
- package/lib/guard-managesieve-command.js +73 -12
- package/lib/guard-regex.js +3 -5
- package/lib/guard-smtp-command.js +20 -4
- package/lib/guard-svg.js +6 -1
- package/lib/guard-yaml.js +60 -15
- package/lib/http-client.js +17 -3
- package/lib/mail-agent.js +29 -13
- package/lib/mail-arc-sign.js +40 -7
- package/lib/mail-auth.js +134 -22
- package/lib/mail-crypto-pgp.js +1 -1
- package/lib/mail-dkim.js +80 -11
- package/lib/mail-helo.js +10 -0
- package/lib/mail-rbl.js +10 -3
- package/lib/mail-send-deliver.js +151 -32
- package/lib/mail-server-imap.js +186 -89
- package/lib/mail-server-jmap.js +31 -4
- package/lib/mail-server-managesieve.js +198 -42
- package/lib/mail-server-mx.js +191 -38
- package/lib/mail-server-net.js +281 -1
- package/lib/mail-server-pop3.js +89 -41
- package/lib/mail-server-rate-limit.js +104 -6
- package/lib/mail-server-submission.js +183 -35
- package/lib/mail-server-tls.js +48 -3
- package/lib/mail-store.js +33 -11
- package/lib/mail.js +355 -17
- package/lib/mcp.js +11 -3
- package/lib/middleware/bearer-auth.js +6 -1
- package/lib/middleware/fetch-metadata.js +5 -1
- package/lib/middleware/headers.js +7 -10
- package/lib/middleware/require-mtls.js +8 -1
- package/lib/network-dns-resolver.js +71 -8
- package/lib/network-dns.js +26 -0
- package/lib/network-smtp-policy.js +42 -10
- package/lib/network-tls.js +18 -0
- package/lib/redact.js +13 -3
- package/lib/retention.js +22 -2
- package/lib/safe-mount-info.js +39 -6
- package/lib/safe-smtp.js +96 -1
- package/lib/safe-url.js +8 -2
- package/lib/self-update.js +4 -1
- package/lib/vendor/MANIFEST.json +12 -12
- package/lib/vendor/blamejs-pki.cjs +672 -75
- package/lib/watcher.js +31 -6
- package/lib/ws-client.js +17 -2
- package/lib/yaml-lex.js +55 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/codepoint-class.js
CHANGED
|
@@ -1250,6 +1250,44 @@ function isIdentifierChar(cc) {
|
|
|
1250
1250
|
return isAsciiAlnum(cc) || cc === 0x5F; // "_"
|
|
1251
1251
|
}
|
|
1252
1252
|
|
|
1253
|
+
/**
|
|
1254
|
+
* @primitive b.codepointClass.hasPairWhere
|
|
1255
|
+
* @signature b.codepointClass.hasPairWhere(text, first, second, accept)
|
|
1256
|
+
* @since 0.18.54
|
|
1257
|
+
* @status stable
|
|
1258
|
+
* @related b.codepointClass.splitLines
|
|
1259
|
+
*
|
|
1260
|
+
* Is there a place where `first` is immediately followed by `second`, and
|
|
1261
|
+
* `accept` says that place counts? `accept(i)` is called with the index of
|
|
1262
|
+
* `first`, and returns whether the construct opening there is a real one.
|
|
1263
|
+
*
|
|
1264
|
+
* This is the frame every two-character construct screen was writing out by
|
|
1265
|
+
* hand: a path traversal opening on `..`, a regex inline-flag group on `(?`, a
|
|
1266
|
+
* YAML merge key on `<<`. The literals and the follow-on tests differ, the scan
|
|
1267
|
+
* does not, and three copies of a scan are three places for an off-by-one to
|
|
1268
|
+
* live. It reads the string one character at a time, which is the property a
|
|
1269
|
+
* guard needs: asking the question with a pattern would be a screen running the
|
|
1270
|
+
* construct it screens over hostile text.
|
|
1271
|
+
*
|
|
1272
|
+
* `accept` decides alone — the scan does not skip past a hit it rejected, so
|
|
1273
|
+
* overlapping openers are all offered. A screen that needs to COUNT
|
|
1274
|
+
* non-overlapping occurrences is asking a different question and keeps its own
|
|
1275
|
+
* cursor.
|
|
1276
|
+
*
|
|
1277
|
+
* @example
|
|
1278
|
+
* var CP = b.codepointClass;
|
|
1279
|
+
* CP.hasPairWhere("a/../b", ".", ".", function () { return true; }); // → true
|
|
1280
|
+
* CP.hasPairWhere("a.b", ".", ".", function () { return true; }); // → false
|
|
1281
|
+
*/
|
|
1282
|
+
function hasPairWhere(text, first, second, accept) {
|
|
1283
|
+
if (typeof text !== "string") return false;
|
|
1284
|
+
for (var i = 0; i + 1 < text.length; i += 1) {
|
|
1285
|
+
if (text.charAt(i) !== first || text.charAt(i + 1) !== second) continue;
|
|
1286
|
+
if (accept(i)) return true;
|
|
1287
|
+
}
|
|
1288
|
+
return false;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1253
1291
|
/**
|
|
1254
1292
|
* @primitive b.codepointClass.splitLines
|
|
1255
1293
|
* @signature b.codepointClass.splitLines(text)
|
|
@@ -1424,6 +1462,38 @@ function firstControlCharOffset(s, opts) {
|
|
|
1424
1462
|
return -1;
|
|
1425
1463
|
}
|
|
1426
1464
|
|
|
1465
|
+
/**
|
|
1466
|
+
* @primitive b.codepointClass.firstLineInjectionCharOffset
|
|
1467
|
+
* @signature b.codepointClass.firstLineInjectionCharOffset(s)
|
|
1468
|
+
* @since 0.18.54
|
|
1469
|
+
* @status stable
|
|
1470
|
+
* @related b.codepointClass.firstControlCharOffset
|
|
1471
|
+
*
|
|
1472
|
+
* Return the index of the first CR, LF or NUL in `s`, or `-1` when there is
|
|
1473
|
+
* none. These three are the bytes that end a line-oriented protocol record,
|
|
1474
|
+
* so a value carrying one splits the record it is written into and the
|
|
1475
|
+
* remainder is read as a second, attacker-chosen line: the header-injection
|
|
1476
|
+
* class in HTTP and Set-Cookie, and the command-injection class in SMTP,
|
|
1477
|
+
* POP3, IMAP and ManageSieve.
|
|
1478
|
+
*
|
|
1479
|
+
* Narrower than `firstControlCharOffset`, deliberately. That one refuses every
|
|
1480
|
+
* C0 control and DEL, which is right for text a human wrote; this one answers
|
|
1481
|
+
* the specific question a wire-protocol writer asks, so a caller adopting it
|
|
1482
|
+
* does not silently start refusing values it used to accept.
|
|
1483
|
+
*
|
|
1484
|
+
* @example
|
|
1485
|
+
* b.codepointClass.firstLineInjectionCharOffset("nonce-42"); // -1
|
|
1486
|
+
* b.codepointClass.firstLineInjectionCharOffset("abc\r\nOK"); // 3
|
|
1487
|
+
*/
|
|
1488
|
+
function firstLineInjectionCharOffset(s) {
|
|
1489
|
+
if (typeof s !== "string") return -1;
|
|
1490
|
+
for (var i = 0; i < s.length; i += 1) {
|
|
1491
|
+
var c = s.charCodeAt(i);
|
|
1492
|
+
if (c === 0x0d || c === 0x0a || c === 0x00) return i; // CR / LF / NUL
|
|
1493
|
+
}
|
|
1494
|
+
return -1;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1427
1497
|
// Decode HTML numeric character references (hex &#x..; and decimal &#..;) just
|
|
1428
1498
|
// enough to expose a scheme hidden behind entity-encoding. The trailing
|
|
1429
1499
|
// semicolon is OPTIONAL — a browser decodes `javascript:` (no semicolon)
|
|
@@ -1743,6 +1813,7 @@ module.exports = {
|
|
|
1743
1813
|
caseFoldPartners: caseFoldPartners,
|
|
1744
1814
|
isForbiddenControlChar: isForbiddenControlChar,
|
|
1745
1815
|
firstControlCharOffset: firstControlCharOffset,
|
|
1816
|
+
firstLineInjectionCharOffset: firstLineInjectionCharOffset,
|
|
1746
1817
|
decodeNumericEntities: decodeNumericEntities,
|
|
1747
1818
|
decodeMarkupEntities: decodeMarkupEntities,
|
|
1748
1819
|
NAMED_ENTITY_ASCII: NAMED_ENTITY_ASCII,
|
|
@@ -1769,6 +1840,7 @@ module.exports = {
|
|
|
1769
1840
|
isAsciiDigit: isAsciiDigit,
|
|
1770
1841
|
isAsciiHexDigit: isAsciiHexDigit,
|
|
1771
1842
|
isIdentifierChar: isIdentifierChar,
|
|
1843
|
+
hasPairWhere: hasPairWhere,
|
|
1772
1844
|
splitLines: splitLines,
|
|
1773
1845
|
splitLinesAny: splitLinesAny,
|
|
1774
1846
|
splitOnWhitespace: splitOnWhitespace,
|
package/lib/cookies.js
CHANGED
|
@@ -591,16 +591,13 @@ function parseSafe(cookieHeader, opts) {
|
|
|
591
591
|
});
|
|
592
592
|
return { jar: jar, issues: issues };
|
|
593
593
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
});
|
|
602
|
-
return { jar: jar, issues: issues };
|
|
603
|
-
}
|
|
594
|
+
if (codepointClass.firstLineInjectionCharOffset(cookieHeader) !== -1) {
|
|
595
|
+
issues.push({
|
|
596
|
+
kind: "header-control-byte", severity: "high",
|
|
597
|
+
snippet: "Cookie header contains CR / LF / NUL — proxy-side " +
|
|
598
|
+
"header injection vector",
|
|
599
|
+
});
|
|
600
|
+
return { jar: jar, issues: issues };
|
|
604
601
|
}
|
|
605
602
|
|
|
606
603
|
var pairs = cookieHeader.split(/;\s*/);
|
package/lib/credential-hash.js
CHANGED
|
@@ -372,8 +372,15 @@ function inspect(envelope) {
|
|
|
372
372
|
function needsRehash(envelope, opts) {
|
|
373
373
|
var decoded = _decodeEnvelope(envelope);
|
|
374
374
|
if (!decoded) return true; // unrecognized → migrate aggressively
|
|
375
|
+
// Same validation hash() applies to the same option. Reading the table with
|
|
376
|
+
// a fallback to the default meant a misspelled algo silently answered a
|
|
377
|
+
// different question than the caller asked — "is this row still the default?"
|
|
378
|
+
// instead of "is this row argon2id?" — on a credential-rotation decision. It
|
|
379
|
+
// also read through the prototype: algo "constructor" yielded a function,
|
|
380
|
+
// which compares unequal to every real algorithm id.
|
|
381
|
+
_validateOpts(opts);
|
|
375
382
|
var targetAlgoName = (opts && opts.algo) || DEFAULTS.algo;
|
|
376
|
-
var targetId = NAME_TO_ID[targetAlgoName]
|
|
383
|
+
var targetId = NAME_TO_ID[targetAlgoName];
|
|
377
384
|
if (decoded.algoId !== targetId) return true;
|
|
378
385
|
if (decoded.algoId === C.CRED_HASH_IDS.ARGON2ID) {
|
|
379
386
|
// Defer the parameter-lag check to the password primitive's
|
package/lib/crypto.js
CHANGED
|
@@ -1815,7 +1815,8 @@ function encryptMlkem768X25519(plaintext, recipient) {
|
|
|
1815
1815
|
// algorithm gets a clear error rather than the generic "unsupported
|
|
1816
1816
|
// KEM ID" path.
|
|
1817
1817
|
//
|
|
1818
|
-
// recipient:
|
|
1818
|
+
// recipient: the operator's two private keys, named privateKey (ML-KEM-768)
|
|
1819
|
+
// and x25519PrivateKey (X25519)
|
|
1819
1820
|
// ciphertext: base64 envelope from encryptMlkem768X25519
|
|
1820
1821
|
/**
|
|
1821
1822
|
* @primitive b.crypto.decryptMlkem768X25519
|
|
@@ -1827,8 +1828,9 @@ function encryptMlkem768X25519(plaintext, recipient) {
|
|
|
1827
1828
|
* envelope whose KEM ID byte is not `ML_KEM_768_X25519` so an
|
|
1828
1829
|
* operator who calls this with a ciphertext sealed under a different
|
|
1829
1830
|
* algorithm gets a clear error rather than the generic dispatch path.
|
|
1830
|
-
*
|
|
1831
|
-
*
|
|
1831
|
+
* The recipient carries two private keys: `privateKey`, which is the
|
|
1832
|
+
* ML-KEM-768 PEM and NOT the framework default ML-KEM-1024, and
|
|
1833
|
+
* `x25519PrivateKey`, which is the X25519 PEM.
|
|
1832
1834
|
*
|
|
1833
1835
|
* @example
|
|
1834
1836
|
* var pair = b.crypto.generateMlkem768X25519KeyPair();
|
|
@@ -1845,8 +1847,8 @@ function encryptMlkem768X25519(plaintext, recipient) {
|
|
|
1845
1847
|
function decryptMlkem768X25519(ciphertext, recipient) {
|
|
1846
1848
|
if (!recipient || typeof recipient !== "object" ||
|
|
1847
1849
|
!recipient.privateKey || !recipient.x25519PrivateKey) {
|
|
1848
|
-
throw new Error("decryptMlkem768X25519 requires
|
|
1849
|
-
"
|
|
1850
|
+
throw new Error("decryptMlkem768X25519 requires both a privateKey, which is the " +
|
|
1851
|
+
"ML-KEM-768 PEM, and an x25519PrivateKey, which is the X25519 PEM");
|
|
1850
1852
|
}
|
|
1851
1853
|
var packed = Buffer.from(ciphertext, "base64");
|
|
1852
1854
|
if (packed[0] !== C.ENVELOPE_MAGIC) {
|
package/lib/db-file-lifecycle.js
CHANGED
|
@@ -90,12 +90,17 @@ function _aad(dataDir, label) {
|
|
|
90
90
|
return Buffer.from("blamejs.db-file-lifecycle.v1\0" + label + "\0" + (dataDir || ""), "utf8");
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
// Platform and stat are parameters so the non-Linux branch is reachable from a
|
|
94
|
+
// Linux CI host. They also keep the "/dev/shm" literal off a direct fs call:
|
|
95
|
+
// the path is only meaningful on Linux, and a leading slash on Windows is
|
|
96
|
+
// drive-relative, so an unguarded probe there asks about C:\dev\shm and can be
|
|
97
|
+
// answered yes by any directory an unprivileged user creates.
|
|
98
|
+
function _resolveTmpDirFrom(operatorTmpDir, allowDiskFallback, platform, stat) {
|
|
94
99
|
if (operatorTmpDir) return operatorTmpDir;
|
|
95
100
|
// Linux: /dev/shm is the standard tmpfs mount.
|
|
96
|
-
if (
|
|
101
|
+
if (platform === "linux") {
|
|
97
102
|
try {
|
|
98
|
-
var st =
|
|
103
|
+
var st = stat("/dev/shm");
|
|
99
104
|
if (st && st.isDirectory()) return "/dev/shm";
|
|
100
105
|
} catch (_e) { /* fall through */ }
|
|
101
106
|
}
|
|
@@ -108,6 +113,11 @@ function _resolveTmpDir(operatorTmpDir, allowDiskFallback) {
|
|
|
108
113
|
"OR set opts.allowDiskFallback: true to accept disk-backed temporary storage.");
|
|
109
114
|
}
|
|
110
115
|
|
|
116
|
+
function _resolveTmpDir(operatorTmpDir, allowDiskFallback) {
|
|
117
|
+
return _resolveTmpDirFrom(operatorTmpDir, allowDiskFallback,
|
|
118
|
+
process.platform, nodeFs.statSync);
|
|
119
|
+
}
|
|
120
|
+
|
|
111
121
|
/**
|
|
112
122
|
* @primitive b.db.fileLifecycle
|
|
113
123
|
* @signature b.db.fileLifecycle(opts)
|
|
@@ -342,4 +352,5 @@ function fileLifecycle(opts) {
|
|
|
342
352
|
module.exports = {
|
|
343
353
|
fileLifecycle: fileLifecycle,
|
|
344
354
|
DbFileLifecycleError: DbFileLifecycleError,
|
|
355
|
+
_resolveTmpDirFromForTest: _resolveTmpDirFrom,
|
|
345
356
|
};
|