@blamejs/core 0.18.48 → 0.18.50
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 +36 -0
- package/lib/ai-model-manifest.js +2 -2
- package/lib/audit-sign.js +3 -3
- package/lib/auth/passkey.js +1 -1
- package/lib/backup/index.js +2 -2
- package/lib/client-hints.js +1 -2
- package/lib/codepoint-class.js +6 -5
- package/lib/gate-contract.js +1 -1
- package/lib/guard-archive.js +6 -5
- package/lib/guard-regex.js +15 -14
- package/lib/json-path.js +54 -4
- package/lib/json-schema.js +90 -5
- package/lib/mail-auth.js +3 -6
- package/lib/mail-scan.js +21 -1
- package/lib/mail-server-submission.js +8 -1
- package/lib/mail-sieve.js +61 -19
- package/lib/metrics.js +4 -4
- package/lib/mtls-ca.js +4 -4
- package/lib/network-dns.js +2 -2
- package/lib/session-device-binding.js +2 -2
- package/lib/vault/index.js +2 -2
- package/lib/ws-client.js +2 -2
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,42 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.18.x
|
|
10
10
|
|
|
11
|
+
- v0.18.50 (2026-08-22) — **Five places let someone else choose how much CPU your server spends on one match.** A Sieve `:matches` wildcard, a JSONPath `match()` filter and a JSON Schema `pattern` each took a pattern from outside the framework and ran it on a backtracking engine. A Sieve rule with five wildcards spent 452ms on a 60-character Subject; a JSONPath filter spent 7 seconds on a 29-character value; a schema pattern spent 1.5 seconds on 28 characters and doubled with every two more. Two more were the same cost from the other direction, a fixed pattern against attacker-shaped input: a DKIM tag scan and a scanner-reply trim, both quadratic. All five now refuse the shape or match without backtracking. **Security:** *`b.mail.sieve` matches `:matches` wildcards without a regular expression* — RFC 5228 `:matches` uses `*` for any sequence and `?` for one character. Translating that into a regular expression turns each `*` into `.*`, and a backtracking engine facing several of them tries every way of dividing the subject between them. The cost is polynomial in the subject length with degree equal to the number of wildcards.
|
|
12
|
+
|
|
13
|
+
Measured on the shipped path, `b.mail.sieve.runScript` with `if header :matches "Subject" ...` and a subject that never supplies the pattern's trailing literal: three wildcards grew by a factor of 7.7 for each doubling of the subject, and ten wildcards — which a script author can type without meaning anything by it — did not finish on a 64-character Subject.
|
|
14
|
+
|
|
15
|
+
The `maxGas` budget did not bound it. Gas counts operations, and one match is one operation however long the engine spends inside it: five wildcards cost 452ms for three gas units.
|
|
16
|
+
|
|
17
|
+
This matters because of who writes the script. Sieve is a user-level filtering language, so in most deployments the pattern comes from the mailbox owner rather than from you. The wildcard is now matched directly against the value, walking both strings with a single remembered wildcard position, so the work is bounded by their lengths rather than by how the pattern is shaped. Every pattern decides exactly as it did before, checked across 42,976 pattern/subject/case combinations against the previous implementation.
|
|
18
|
+
|
|
19
|
+
One deliberate change of behaviour comes with it. `:comparator "i;ascii-casemap"` now folds US-ASCII `A-Z` only, which is what RFC 4790 §9.2 defines it as; the regular expression it replaced carried JavaScript's `i` flag and folded the whole of Unicode. The narrower fold is also the only one that preserves length: `"İ".toLowerCase()` is two UTF-16 units, so folding a subject up front shifts every position after it and leaves `?`, which matches exactly one character, facing two. A script relying on non-ASCII case folding under `i;ascii-casemap` was relying on something the comparator does not promise. · *`b.jsonPath` refuses a catastrophic `match()` or `search()` pattern* — The second argument of an RFC 9535 `match()` or `search()` filter is an I-Regexp (RFC 9485), which is a general regular-expression language: a filter can carry `(a+)+$` as easily as `a+`. `$[?match(@.name, "(a+)+$")]` against a 29-character value that fails the match took 7 seconds.
|
|
20
|
+
|
|
21
|
+
The node ceilings on descendant walks and running nodelists never saw it, because the whole cost is inside one match on one node.
|
|
22
|
+
|
|
23
|
+
The pattern is now screened with `b.guardRegex.assertSafe` before it can run, which is what the framework already does wherever else it compiles a caller-supplied pattern — `b.flag` targeting conditions and MCP tool schemas both go through the same screen. A refusal raises `json-path/unsafe-pattern` rather than being folded into a false test result: a malformed I-Regexp yields false per RFC 9535 §2.4.6, and a refused one must not wear those clothes, or the caller is left with a filter that silently matches nothing. · *A folded DKIM-Signature header made the submission listener spend its CPU on a trim* — `b.mail.server.submission` reads the `d=` tag out of each DKIM-Signature header, and trimmed each tag with `/^\s+|\s+$/`. That is the quadratic trim: the `\s+$` alternative is retried from every start position when the whitespace run does not reach the end of the string.
|
|
24
|
+
|
|
25
|
+
The tag was believed to be bounded by the line cap. It is not, because the header is unfolded first. Every empty continuation line contributes one space to the joined value, so the lever is the number of folded lines rather than the length of any one of them, and the real bound is `maxMessageBytes` — 50 MiB by default. An authenticated client sending 40,000 empty continuation lines, a 120 KB message, produced a 40,000-character run and 446ms of backtracking on one header. Three megabytes would have bought minutes.
|
|
26
|
+
|
|
27
|
+
The trim is now `String.prototype.trim`, which decides identically (the same WhiteSpace and LineTerminator set as `\s`) and does not backtrack. · *A virus-scanner reply with a long newline run cost the same shape* — The ClamAV INSTREAM reply is trimmed of trailing CR / LF / NUL before it is classified, and the trim was `/[\r\n\0]+$/`. Anchoring at `$` does not stop the engine retrying from every start position; it is what makes the scan quadratic. A reply carrying a long interior run of newlines took 1.2 seconds through the real scan path.
|
|
28
|
+
|
|
29
|
+
The reply is capped, but by `maxResponseBytes`, which is 50 MiB on the strict profile and 300 MiB on permissive, so the cap was not what stood between a reply and the cost. The trim now walks backwards from the end.
|
|
30
|
+
|
|
31
|
+
A local daemon is normally trusted, which makes this hardening rather than a hole — but `b.mail.scan` dials a host and a port, a scanning service can be remote, and a reply parser is the wrong place to assume the peer is well behaved. · *`b.jsonSchema` screens `pattern` and `patternProperties` when the schema compiles* — A `pattern` keyword is compiled and then run against the instance, so the schema author decides what each validation costs. `(a+)+$` against a 28-character string that fails took 1.5 seconds, doubling with every two characters added.
|
|
32
|
+
|
|
33
|
+
The reasoning that left this unscreened was that a schema is operator-supplied and therefore trusted. The framework does not hold that assumption elsewhere: `b.mcp` already screens the `pattern` in a tool schema for exactly this shape before matching it against request input. A schema arrives from a registry, a tool manifest, an upload or a config file as readily as from your own source.
|
|
34
|
+
|
|
35
|
+
Every pattern in a schema is now screened, and screened at `b.jsonSchema.compile` rather than on the first instance that reaches it — a bad schema should be refused while you are still looking at the schema, not on a request, through a validator you have already been handed and have no reason to doubt.
|
|
36
|
+
|
|
37
|
+
The screen reads the compiled expression rather than the pattern text, which is what `b.safeJson` does with the same keyword and for the reason recorded there: the flags decide what the source means, so a text-only screen reads `(a|A)+` as two disjoint branches where the engine sees one branch twice.
|
|
38
|
+
|
|
39
|
+
- v0.18.49 (2026-08-22) — **Comments in the shipped tree that answered a question you cannot ask.** A comment in `lib/` reaches anyone who opens the published package, and twenty of them were written for a reader who remembers the previous version or can look up an issue number that resolves nowhere outside this repository. They now say what the code does and why, in the present tense, keeping every reason they carried. No behaviour changes. **Changed:** *Comments that narrated a change now describe the code* — Fourteen comments across `lib/` were written against a version the reader has never seen: "this used to be opt-in", "it used to declare them in every profile anyway", "the legacy fallback was removed". Each is now a property of the design rather than a diff, and each keeps the reason it carried. The zero-width scan's comment still explains that an opt-in argument is an argument a caller can omit, and still names the six call sites that omitted it — it just no longer reads as a changelog entry pasted into the source.
|
|
40
|
+
|
|
41
|
+
One of them was wrong in a way worth naming: a note in the gate-contract context builder said a guard "serves bytes it used to examine", which reads as a past version's behaviour. It means bytes the guard would otherwise examine, and now says so. · *Issue numbers that resolve nowhere are gone; the upstream ones stay* — Six comments cited a bare `issue #100` / `#330` / `#430` / `#532`. Opened from the published tarball those name nothing an operator can look up, and the explanation beside them was already doing the work. The references are gone and the explanations stay.
|
|
42
|
+
|
|
43
|
+
The references that name their upstream are untouched and now read unambiguously: `cure53/DOMPurify issue #233` on the SVG `xlink:href` filtering, and CycloneDX spec issue #702 on the EU CRA property mapping, which is now attributed on both of its mentions rather than one.
|
|
44
|
+
|
|
45
|
+
The boundary is what ships. `test/` carries 144 of the same kind of reference, most of them a `PR #nnn` on a regression test, and those are left alone: they resolve in the repository a contributor is already reading, and on a regression test the pull-request reference is often the most useful line in the comment. · *Three sentences in SECURITY.md* — A bot-guard rationale said an abuser "simply sends the header", which tells a reader the step is trivial and adds nothing to the argument. Two lists ended in `etc.` without saying where the rest was: the post-quantum algorithm-agility example now names HQC and FrodoKEM as the examples they are, and the session-risk scorer now says it scores whatever signal you have. The other six `etc.` in the file name their generating rule in the next clause and are unchanged.
|
|
46
|
+
|
|
11
47
|
- v0.18.48 (2026-08-22) — **A guard option you set and a guard option that exists were not the same set.** Every `*Policy` option across the guard family now names the values it takes, and refuses everything else where options are resolved rather than at the first request that trips the scan. Holding the options to their vocabularies surfaced four places where what a guard advertised and what it did had come apart: an option documented under a name nothing read, a value that disabled a critical check without appearing in any opts block, two policies whose settings the gate's floor overrode anyway, and one declared in all three profiles and read nowhere. **Fixed:** *A guard policy refuses a value outside its vocabulary* — The two hundred and sixty-two `*Policy` options across the guard family each declare the values they accept, checked where options are resolved — boot, not the first hostile input — and on every entry point, including the `gate()` and `validate()` of guards that bind their own resolver.
|
|
12
48
|
|
|
13
49
|
The failure it ends is quiet, because policies are read leniently. `duplicateKeyPolicy: "rejct"` is not `allow`, so the check still runs; it is not `reject` either, so the finding drops from critical to a warning. The operator asked to refuse a duplicate key and silently got an audit line. Nothing was let through that a stricter reading would have caught, so this was never a hole — but a setting that means something other than what it says is worth a restart to learn about.
|
package/lib/ai-model-manifest.js
CHANGED
|
@@ -178,8 +178,8 @@ function build(opts) {
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
// Hyperparameters → CycloneDX properties[] kv pairs per spec
|
|
182
|
-
// issue #702 EU CRA alignment.
|
|
181
|
+
// Hyperparameters → CycloneDX properties[] kv pairs, per CycloneDX spec
|
|
182
|
+
// issue #702 (EU CRA alignment).
|
|
183
183
|
var properties = [];
|
|
184
184
|
if (opts.hyperparameters && typeof opts.hyperparameters === "object") {
|
|
185
185
|
var keys = Object.keys(opts.hyperparameters);
|
package/lib/audit-sign.js
CHANGED
|
@@ -85,8 +85,8 @@ var _err = AuditSignError.factory;
|
|
|
85
85
|
// via opts.algorithm — e.g. `auditSigning: { algorithm: "ml-dsa-87" }`
|
|
86
86
|
// for throughput-sensitive deployments. Every key file MUST carry the
|
|
87
87
|
// `algorithm` field on disk — the framework refuses to load a key file
|
|
88
|
-
// that lacks it.
|
|
89
|
-
//
|
|
88
|
+
// that lacks it. There is no implicit default: a key file whose algorithm has
|
|
89
|
+
// to be guessed is a key file whose signatures cannot be attributed.
|
|
90
90
|
var DEFAULT_SIGNING_ALG = "slh-dsa-shake-256f";
|
|
91
91
|
// ml-dsa-65 (FIPS 204 Category 3, ~192-bit symmetric security) is opt-
|
|
92
92
|
// in alongside ml-dsa-87 — same code path (both auto-detected by
|
|
@@ -102,7 +102,7 @@ var SIGNING_KEY_SCHEMA = {
|
|
|
102
102
|
properties: {
|
|
103
103
|
publicKey: { type: "string" },
|
|
104
104
|
privateKey: { type: "string" },
|
|
105
|
-
algorithm: { type: "string" }, // load-time-required — _initPlaintext + _initWrapped both throw KEY_FILE_MISSING_ALG / UNWRAPPED_MISSING_ALG when the field is absent
|
|
105
|
+
algorithm: { type: "string" }, // load-time-required — _initPlaintext + _initWrapped both throw KEY_FILE_MISSING_ALG / UNWRAPPED_MISSING_ALG when the field is absent, rather than defaulting to an algorithm the file never named. Schema's `required` keeps publicKey + privateKey only so the runtime checks fire with the precise error codes operators have wired alerting on.
|
|
106
106
|
},
|
|
107
107
|
};
|
|
108
108
|
|
package/lib/auth/passkey.js
CHANGED
|
@@ -986,7 +986,7 @@ function _requireCredentialIdMatches(response, authoritativeId, why) {
|
|
|
986
986
|
// _credentialDescriptors deliberately supports -- while the browser returns
|
|
987
987
|
// the unpadded spelling. Two spellings of ONE credential must not read as
|
|
988
988
|
// two credentials, or the compatibility this binding sits next to would
|
|
989
|
-
// lock out exactly the deployments it
|
|
989
|
+
// lock out exactly the deployments it exists for.
|
|
990
990
|
var expected = _canonicalBase64Url(authoritativeId);
|
|
991
991
|
var fields = ["id", "rawId"];
|
|
992
992
|
var stated = 0;
|
package/lib/backup/index.js
CHANGED
|
@@ -1697,8 +1697,8 @@ function bundleAdapterStorage(opts) {
|
|
|
1697
1697
|
// "unknown" rather than risk a full payload load. For
|
|
1698
1698
|
// rewrap, we already have to load the payload (to unwrap),
|
|
1699
1699
|
// so fall back to a sniffEnvelope on the loaded sealed
|
|
1700
|
-
// bytes
|
|
1701
|
-
// the minimum contract
|
|
1700
|
+
// bytes, which keeps rewrapBundle reachable for an adapter
|
|
1701
|
+
// that satisfies only the minimum contract.
|
|
1702
1702
|
if (envelopeKind === "unknown") {
|
|
1703
1703
|
envelopeKind = archiveLazy().sniffEnvelope(sealed);
|
|
1704
1704
|
}
|
package/lib/client-hints.js
CHANGED
|
@@ -136,8 +136,7 @@ function _parseSfBrandList(s) {
|
|
|
136
136
|
// sf-list members don't allow parenthesized inner-list values in
|
|
137
137
|
// the Sec-CH-UA grammar (only sf-string + parameters), so the
|
|
138
138
|
// simple top-level comma split suffices — no `depth` tracking
|
|
139
|
-
// needed
|
|
140
|
-
// tracking left over from a generic sf-list walker prototype).
|
|
139
|
+
// needed.
|
|
141
140
|
var pieces = structuredFields.splitTopLevel(t, ",");
|
|
142
141
|
var out = [];
|
|
143
142
|
for (var i = 0; i < pieces.length; i += 1) {
|
package/lib/codepoint-class.js
CHANGED
|
@@ -860,11 +860,12 @@ function detectCharThreats(text, opts, codePrefix) {
|
|
|
860
860
|
// zero-width-only input under `strip` reaches the sanitizer instead of being
|
|
861
861
|
// served unchanged.
|
|
862
862
|
//
|
|
863
|
-
//
|
|
864
|
-
//
|
|
865
|
-
//
|
|
866
|
-
//
|
|
867
|
-
//
|
|
863
|
+
// Not opt-in, and deliberately: an argument the caller has to pass in order
|
|
864
|
+
// for the scan to run is an argument a caller can omit, and omitting it
|
|
865
|
+
// disables the scan no matter what the operator's policy says. Six call
|
|
866
|
+
// sites omitted it, the shared gate path among them, so seven guards declared
|
|
867
|
+
// `zeroWidthPolicy: "reject"` and never applied it. Six more passed a
|
|
868
|
+
// hardcoded "warn", which dispositions to serve, so they reported the
|
|
868
869
|
// character and shipped it anyway.
|
|
869
870
|
//
|
|
870
871
|
// Severity follows the resolved POLICY, not a per-caller constant — the same
|
package/lib/gate-contract.js
CHANGED
|
@@ -305,7 +305,7 @@ function validateGateShape(gate, label, errorClass) {
|
|
|
305
305
|
// - `Object.assign({}, base, ...)` copies OWN enumerable properties, so a
|
|
306
306
|
// class instance whose fields come from prototype getters arrives with them
|
|
307
307
|
// missing. A guard reading an absent subject as nothing-to-inspect then
|
|
308
|
-
// serves bytes it
|
|
308
|
+
// serves bytes it would otherwise examine.
|
|
309
309
|
// - `Object.create(base)` fixes that but makes inherited getters run with the
|
|
310
310
|
// DERIVED object as `this`, so a getter returning a private field throws
|
|
311
311
|
// and a valid context is refused.
|
package/lib/guard-archive.js
CHANGED
|
@@ -115,11 +115,12 @@ var MAGIC_SIGNATURES = Object.freeze([
|
|
|
115
115
|
|
|
116
116
|
// Character-class policy for an entry NAME comes from `filenameProfile`, which
|
|
117
117
|
// routes the name through b.guardFilename — this guard never reads a
|
|
118
|
-
// bidi/control/null/zero-width policy of its own
|
|
119
|
-
//
|
|
120
|
-
// configuring nothing
|
|
121
|
-
//
|
|
122
|
-
//
|
|
118
|
+
// bidi/control/null/zero-width policy of its own, and does not declare them in
|
|
119
|
+
// its profiles either. Declaring an option nothing reads leaves an operator
|
|
120
|
+
// setting `zeroWidthPolicy` here configuring nothing, with no way to find that
|
|
121
|
+
// out. Wiring them instead would give the same setting two spellings, which is
|
|
122
|
+
// how the two come to disagree, and `filenameProfile` is already the one place
|
|
123
|
+
// that decides it.
|
|
123
124
|
var PROFILES = Object.freeze({
|
|
124
125
|
"strict": {
|
|
125
126
|
traversalPolicy: "reject",
|
package/lib/guard-regex.js
CHANGED
|
@@ -201,14 +201,15 @@ function _classUsesSetSyntax(text, from) {
|
|
|
201
201
|
// ---- pattern parsing ------------------------------------------------------
|
|
202
202
|
//
|
|
203
203
|
// Every analysis below reads a parse tree. None of them reads the pattern
|
|
204
|
-
// source. Reading source with regexes
|
|
205
|
-
//
|
|
206
|
-
// past a nested group, one
|
|
207
|
-
// the previous CHARACTER (so the `?` in `\*?`
|
|
208
|
-
// length variation it contributes
|
|
209
|
-
// `{n,m}` (so a longer bound
|
|
210
|
-
//
|
|
211
|
-
//
|
|
204
|
+
// source. Reading source with regexes puts a separate reader behind each
|
|
205
|
+
// analysis, and separate readers draw the token boundaries differently: one
|
|
206
|
+
// cannot see past a nested group, one decides whether a `?` is a quantifier by
|
|
207
|
+
// looking at the previous CHARACTER (so the `?` in `\*?` reads as a lazy marker
|
|
208
|
+
// and the length variation it contributes is lost), one caps the digits inside
|
|
209
|
+
// `{n,m}` (so a longer bound reads as no quantifier at all). Every such
|
|
210
|
+
// disagreement is a way to write a catastrophic pattern that one reader finds
|
|
211
|
+
// and another waves through, and patching them one at a time moves the edge
|
|
212
|
+
// rather than closing it.
|
|
212
213
|
//
|
|
213
214
|
// So: one tokenizer, one tree, and anything it cannot represent becomes an
|
|
214
215
|
// OPAQUE node — which every analysis treats as "cannot prove", never as
|
|
@@ -754,9 +755,9 @@ function _codePointAt(src, at, flags) {
|
|
|
754
755
|
// `k`, but `k` uppercases to `K` and never back to the Kelvin sign, so a pass
|
|
755
756
|
// starting at `K` never reaches it and two branches that both match it were
|
|
756
757
|
// proven disjoint. Which characters an engine treats as equal under `i` is a
|
|
757
|
-
// rule the language states, so the rule is applied
|
|
758
|
-
//
|
|
759
|
-
//
|
|
758
|
+
// rule the language states, so the rule is applied rather than discovered by
|
|
759
|
+
// building a RegExp per pair of characters and seeing which ones match, which
|
|
760
|
+
// would be the screen reaching for the construct it exists to screen.
|
|
760
761
|
//
|
|
761
762
|
// Only characters PRESENT in the pattern can create an overlap between two of
|
|
762
763
|
// its sets, so the comparison is made over that alphabet alone. Pairs whose
|
|
@@ -796,9 +797,9 @@ function _foldGroups(src, flags) {
|
|
|
796
797
|
var x = alphabet[a], y = alphabet[b];
|
|
797
798
|
if (_linkedByCase(x, y)) continue; // already found by folding
|
|
798
799
|
// Which characters an engine treats as the same under `i` is a rule, not
|
|
799
|
-
// something to be discovered by asking.
|
|
800
|
-
//
|
|
801
|
-
//
|
|
800
|
+
// something to be discovered by asking. Building a RegExp per pair and
|
|
801
|
+
// seeing whether one matches the other is the screen reaching for the very
|
|
802
|
+
// construct it screens, and a pattern's worth of them per call.
|
|
802
803
|
// The rule itself is exact and costs a comparison.
|
|
803
804
|
if (_canonical(x, unicodeMode) !== _canonical(y, unicodeMode)) continue;
|
|
804
805
|
_linkFold(groups, x, y);
|
package/lib/json-path.js
CHANGED
|
@@ -33,6 +33,11 @@
|
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
var { defineClass } = require("./framework-error");
|
|
36
|
+
var lazyRequire = require("./lazy-require");
|
|
37
|
+
// Lazy: guard-regex composes the parser in this file's sibling modules, and the
|
|
38
|
+
// screen is only reached by a filter that calls match() or search().
|
|
39
|
+
var guardRegex = lazyRequire(function () { return require("./guard-regex"); });
|
|
40
|
+
var boundedMap = require("./bounded-map");
|
|
36
41
|
|
|
37
42
|
var JsonPathError = defineClass("JsonPathError", { alwaysPermanent: true });
|
|
38
43
|
|
|
@@ -573,7 +578,25 @@ function _evalFunctionValue(node, current, root) {
|
|
|
573
578
|
return NOTHING;
|
|
574
579
|
}
|
|
575
580
|
|
|
581
|
+
// A filter runs once per candidate node, and the pattern is almost always the
|
|
582
|
+
// same literal throughout a query, so translating and screening it per node
|
|
583
|
+
// would put the cost of the analysis on the nodelist length. Screened once per
|
|
584
|
+
// distinct (pattern, anchored) pair instead.
|
|
585
|
+
//
|
|
586
|
+
// Bounded, because the key comes from the query: an unbounded map keyed by
|
|
587
|
+
// caller-supplied text is a slower way to spend the same memory. The framework's
|
|
588
|
+
// own ceiling primitive owns the eviction, so the oldest entry goes rather than
|
|
589
|
+
// the whole cache.
|
|
590
|
+
var _filterRegexCache = boundedMap.boundedMap({ maxEntries: 256 });
|
|
591
|
+
|
|
592
|
+
// Only an accepted pattern is stored: a refusal throws out of `query()` on the
|
|
593
|
+
// first candidate node, so there is no second node to re-decide it for.
|
|
576
594
|
function _iRegexpToJs(pattern, anchored) {
|
|
595
|
+
return boundedMap.getOrInsert(_filterRegexCache, (anchored ? "1" : "0") + pattern,
|
|
596
|
+
function () { return _buildFilterRegex(pattern, anchored); });
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function _buildFilterRegex(pattern, anchored) {
|
|
577
600
|
// I-Regexp (RFC 9485) is close to a JS regex subset. Translate the one
|
|
578
601
|
// systematic difference — "." must not match line separators — and
|
|
579
602
|
// (for match) anchor the whole input.
|
|
@@ -582,16 +605,43 @@ function _iRegexpToJs(pattern, anchored) {
|
|
|
582
605
|
if (cls) return cls;
|
|
583
606
|
return "[^\\n\\r]";
|
|
584
607
|
});
|
|
585
|
-
|
|
586
|
-
//
|
|
587
|
-
|
|
608
|
+
var source = anchored ? "^(?:" + translated + ")$" : translated;
|
|
609
|
+
// I-Regexp is a general regular-expression language, so the argument of a
|
|
610
|
+
// match() or search() filter can be `(a+)+$` as easily as `a+`, and a
|
|
611
|
+
// backtracking engine will spend the caller's chosen amount of CPU deciding
|
|
612
|
+
// it. Being a boolean test is no protection: `.test()` is exactly where the
|
|
613
|
+
// backtracking happens, and the node caps above never see it because the
|
|
614
|
+
// whole blow-up is inside one call on one node. Measured before this screen:
|
|
615
|
+
// 7 seconds on a 29-character subject, doubling per added character.
|
|
616
|
+
//
|
|
617
|
+
// Screened with the same primitive the framework already uses wherever it
|
|
618
|
+
// compiles a caller-supplied pattern — b.flag targeting conditions and MCP
|
|
619
|
+
// tool schemas both go through it.
|
|
620
|
+
//
|
|
621
|
+
// Compiled first and screened on the COMPILED form, matching b.safeJson: the
|
|
622
|
+
// flags decide what the source means, so a source-only screen reads `(a|A)+`
|
|
623
|
+
// as two disjoint branches where the engine sees one branch twice. Compiling
|
|
624
|
+
// is safe on its own; the cost is in matching, which has not happened yet.
|
|
625
|
+
var re = new RegExp(source, "su"); // allow:dynamic-regex — translated I-Regexp, ReDoS-screened via guardRegex.assertSafe below, before it is returned to the caller that will run it
|
|
626
|
+
guardRegex().assertSafe(re, "jsonPath.filter.pattern",
|
|
627
|
+
JsonPathError, "json-path/unsafe-pattern");
|
|
628
|
+
return re;
|
|
588
629
|
}
|
|
589
630
|
function _evalFunctionLogical(node, current, root) {
|
|
590
631
|
var input = _funcArgValue(node.args[0], current, root);
|
|
591
632
|
var pat = _funcArgValue(node.args[1], current, root);
|
|
592
633
|
if (typeof input !== "string" || typeof pat !== "string") return false;
|
|
593
634
|
var re;
|
|
594
|
-
try { re = _iRegexpToJs(pat, node.name === "match"); }
|
|
635
|
+
try { re = _iRegexpToJs(pat, node.name === "match"); }
|
|
636
|
+
catch (e) {
|
|
637
|
+
// A malformed I-Regexp yields Nothing, which RFC 9535 §2.4.6 renders as a
|
|
638
|
+
// false test result. A pattern REFUSED as catastrophic is a different
|
|
639
|
+
// answer and must not wear the same clothes: swallowing it would leave the
|
|
640
|
+
// caller with a filter that silently matches nothing and no way to tell
|
|
641
|
+
// that from a filter that matched nothing.
|
|
642
|
+
if (e && e.code === "json-path/unsafe-pattern") throw e;
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
595
645
|
return re.test(input);
|
|
596
646
|
}
|
|
597
647
|
|
package/lib/json-schema.js
CHANGED
|
@@ -52,6 +52,10 @@
|
|
|
52
52
|
var numericBounds = require("./numeric-bounds");
|
|
53
53
|
var rfc3339 = require("./rfc3339");
|
|
54
54
|
var { defineClass } = require("./framework-error");
|
|
55
|
+
var lazyRequire = require("./lazy-require");
|
|
56
|
+
// Lazy: the screen is only reached by a schema that carries a `pattern`.
|
|
57
|
+
var guardRegex = lazyRequire(function () { return require("./guard-regex"); });
|
|
58
|
+
var boundedMap = require("./bounded-map");
|
|
55
59
|
|
|
56
60
|
var JsonSchemaError = defineClass("JsonSchemaError", { alwaysPermanent: true });
|
|
57
61
|
|
|
@@ -288,6 +292,51 @@ function _buildModule() {
|
|
|
288
292
|
* properties: { n: { type: "integer" } }, required: ["n"] });
|
|
289
293
|
* v.validate({ n: 1 }).valid; // → true
|
|
290
294
|
*/
|
|
295
|
+
// Where a subschema can appear. Named rather than inferred, because a schema
|
|
296
|
+
// also carries DATA — `const`, `enum`, `default` and `examples` hold instance
|
|
297
|
+
// values — and a walk that recursed into everything would read a `pattern` key
|
|
298
|
+
// inside a `const` value as a schema keyword and refuse a legal schema for the
|
|
299
|
+
// shape of its example data.
|
|
300
|
+
//
|
|
301
|
+
// Missing a position here is safe in the direction that matters: the screen in
|
|
302
|
+
// _compileRegex still runs on whatever is actually compiled, so a subschema
|
|
303
|
+
// this walk does not reach is refused when it is used rather than never. The
|
|
304
|
+
// walk buys an earlier refusal, not the refusal itself.
|
|
305
|
+
// `additionalItems` and `contentSchema` are deliberately absent: this validator
|
|
306
|
+
// does not evaluate either keyword, so a pattern inside one never compiles and
|
|
307
|
+
// never runs. Screening there could only refuse a schema for a branch that has
|
|
308
|
+
// no effect.
|
|
309
|
+
var _SUBSCHEMA_KEYS = ["additionalProperties", "contains",
|
|
310
|
+
"else", "if", "items", "not", "propertyNames", "then",
|
|
311
|
+
"unevaluatedItems", "unevaluatedProperties"];
|
|
312
|
+
var _SUBSCHEMA_LIST_KEYS = ["allOf", "anyOf", "oneOf", "prefixItems", "items"];
|
|
313
|
+
var _SUBSCHEMA_MAP_KEYS = ["$defs", "definitions", "dependentSchemas",
|
|
314
|
+
"patternProperties", "properties"];
|
|
315
|
+
|
|
316
|
+
// Walk the subschema positions for the two keywords that carry a regular
|
|
317
|
+
// expression, and hand each to the same screen the compile site uses.
|
|
318
|
+
// Depth-bounded by the ceiling the validator applies to `$ref` chains, so a
|
|
319
|
+
// deeply nested schema cannot spend the walk instead of the match.
|
|
320
|
+
function _screenPatterns(node, depth) {
|
|
321
|
+
depth = depth || 0;
|
|
322
|
+
if (depth > MAX_REF_DEPTH || !_isObject(node)) return;
|
|
323
|
+
|
|
324
|
+
if (typeof node.pattern === "string") _compileRegex(node.pattern);
|
|
325
|
+
if (_isObject(node.patternProperties)) {
|
|
326
|
+
Object.keys(node.patternProperties).forEach(function (p) { _compileRegex(p); });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
_SUBSCHEMA_KEYS.forEach(function (k) { _screenPatterns(node[k], depth + 1); });
|
|
330
|
+
_SUBSCHEMA_LIST_KEYS.forEach(function (k) {
|
|
331
|
+
if (!Array.isArray(node[k])) return;
|
|
332
|
+
node[k].forEach(function (sub) { _screenPatterns(sub, depth + 1); });
|
|
333
|
+
});
|
|
334
|
+
_SUBSCHEMA_MAP_KEYS.forEach(function (k) {
|
|
335
|
+
if (!_isObject(node[k])) return;
|
|
336
|
+
Object.keys(node[k]).forEach(function (name) { _screenPatterns(node[k][name], depth + 1); });
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
291
340
|
function compile(schema, opts) {
|
|
292
341
|
opts = opts || {};
|
|
293
342
|
if (!_isObject(schema) && typeof schema !== "boolean") {
|
|
@@ -304,6 +353,15 @@ function compile(schema, opts) {
|
|
|
304
353
|
var rootBase = (_isObject(schema) && typeof schema.$id === "string") ? _resolveUri(schema.$id, "") : "";
|
|
305
354
|
registry.add(schema, rootBase);
|
|
306
355
|
|
|
356
|
+
// Screen every pattern the schema carries HERE, while the author is still
|
|
357
|
+
// looking at the schema. Compiling is where a bad schema should be refused;
|
|
358
|
+
// leaving it to the first instance means the refusal lands on a request, in
|
|
359
|
+
// a validator the caller has already been handed and treats as good.
|
|
360
|
+
_screenPatterns(schema);
|
|
361
|
+
if (_isObject(opts.schemas)) {
|
|
362
|
+
Object.keys(opts.schemas).forEach(function (uri) { _screenPatterns(opts.schemas[uri]); });
|
|
363
|
+
}
|
|
364
|
+
|
|
307
365
|
var assertFormat = opts.assertFormat === true;
|
|
308
366
|
var maxErrors = numericBounds.isPositiveFiniteInt(opts.maxErrors) ? opts.maxErrors : DEFAULT_MAX_ERRORS;
|
|
309
367
|
|
|
@@ -525,15 +583,42 @@ function _checkString(schema, s, ctx, emit) {
|
|
|
525
583
|
}
|
|
526
584
|
}
|
|
527
585
|
|
|
528
|
-
|
|
586
|
+
// Keyed by the schema's own pattern text, so a long-lived process compiling
|
|
587
|
+
// many distinct schemas would otherwise grow this without bound. The
|
|
588
|
+
// framework's own ceiling primitive owns the eviction.
|
|
589
|
+
var _regexCache = boundedMap.boundedMap({ maxEntries: 512 });
|
|
529
590
|
function _compileRegex(pattern, ctx) {
|
|
530
|
-
if (
|
|
591
|
+
if (_regexCache.has(pattern)) return _regexCache.get(pattern);
|
|
592
|
+
// `pattern` / `patternProperties` decide how much CPU each validation costs,
|
|
593
|
+
// because the compiled expression runs against the instance. `(a+)+$` against
|
|
594
|
+
// a run of `a` ending in anything else measured 1.5 seconds at 28 characters
|
|
595
|
+
// and doubles with every two more, so a schema is a denial-of-service lever
|
|
596
|
+
// as much as a contract.
|
|
597
|
+
//
|
|
598
|
+
// A schema is not automatically the operator's own source: it arrives from a
|
|
599
|
+
// registry, a tool manifest, an upload or a config file. b.mcp already
|
|
600
|
+
// screens the pattern in a tool schema for this shape before matching it
|
|
601
|
+
// against request input, and this is the same construct.
|
|
602
|
+
//
|
|
603
|
+
// Screened once per distinct pattern — the cache below means a schema pays
|
|
604
|
+
// for the analysis at its first use rather than per instance.
|
|
605
|
+
//
|
|
606
|
+
// Compiled first and screened AFTER, on the compiled form, which is what
|
|
607
|
+
// b.safeJson does with the same keyword and for the reason recorded there:
|
|
608
|
+
// the flags decide what the source means, so a screen reading the source
|
|
609
|
+
// alone reads `(a|A)+` as two disjoint branches where the engine sees one
|
|
610
|
+
// branch twice. Compiling a pathological pattern is safe on its own — the
|
|
611
|
+
// cost is in matching, which has not happened yet.
|
|
531
612
|
var re = null;
|
|
532
|
-
try { re = new RegExp(pattern, "u"); } // allow:dynamic-regex —
|
|
613
|
+
try { re = new RegExp(pattern, "u"); } // allow:dynamic-regex — ReDoS-screened via guardRegex.assertSafe below, before the compiled form is returned to any caller
|
|
533
614
|
catch (_e) {
|
|
534
|
-
try { re = new RegExp(pattern); } catch (_e2) { re = null; }
|
|
615
|
+
try { re = new RegExp(pattern); } catch (_e2) { re = null; } // allow:dynamic-regex — same screen, non-unicode fallback for a pattern `u` rejects
|
|
616
|
+
}
|
|
617
|
+
if (re) {
|
|
618
|
+
guardRegex().assertSafe(re, "jsonSchema.pattern",
|
|
619
|
+
JsonSchemaError, "json-schema/unsafe-pattern");
|
|
535
620
|
}
|
|
536
|
-
_regexCache
|
|
621
|
+
_regexCache.set(pattern, re);
|
|
537
622
|
return re;
|
|
538
623
|
}
|
|
539
624
|
|
package/lib/mail-auth.js
CHANGED
|
@@ -1227,12 +1227,9 @@ function _dmarcAuthorDomainLabels(domain) {
|
|
|
1227
1227
|
if (!d) return null;
|
|
1228
1228
|
// Both RFC 1035 §2.3.4 bounds — 63 octets per label and 253 for the whole
|
|
1229
1229
|
// name — are enforced by `canonicalDomain` itself, so an over-long or empty
|
|
1230
|
-
// label has already returned "" above.
|
|
1231
|
-
//
|
|
1232
|
-
//
|
|
1233
|
-
// did not survive the observation that the 253 cap is equally a wire rule and
|
|
1234
|
-
// was enforced there anyway. Fixing it at the definition means every caller
|
|
1235
|
-
// gets it, not just this one.
|
|
1230
|
+
// label has already returned "" above. No re-check here: both are wire rules
|
|
1231
|
+
// of the same kind, and enforcing them where the name is defined gives them
|
|
1232
|
+
// to every caller rather than to whichever ones remembered.
|
|
1236
1233
|
return d.split(".");
|
|
1237
1234
|
}
|
|
1238
1235
|
|
package/lib/mail-scan.js
CHANGED
|
@@ -104,6 +104,26 @@ var CLAMAV_LENGTH_PREFIX_BYTES = 4;
|
|
|
104
104
|
// ClamAV INSTREAM chunk size for streaming.
|
|
105
105
|
var CLAMAV_CHUNK_BYTES = 65536;
|
|
106
106
|
|
|
107
|
+
// Trailing CR / LF / NUL off a daemon reply, walked backwards from the end.
|
|
108
|
+
//
|
|
109
|
+
// The regex form of this, `/[\r\n\0]+$/`, is quadratic: `$` does not stop the
|
|
110
|
+
// engine retrying from every start position, so a reply carrying a long
|
|
111
|
+
// interior run of newlines costs O(n^2) — measured 135ms at 20k characters,
|
|
112
|
+
// 538ms at 40k and 2207ms at 80k, and 1223ms through the real scan path.
|
|
113
|
+
// The reply IS capped, but by `maxResponseBytes`, which is 50 MiB on the strict
|
|
114
|
+
// profile and 300 MiB on permissive, so the cap is not what would save it.
|
|
115
|
+
//
|
|
116
|
+
// `String.prototype.trim` is not the substitute here: NUL is not whitespace.
|
|
117
|
+
function _stripTrailingEol(s) {
|
|
118
|
+
var end = s.length;
|
|
119
|
+
while (end > 0) {
|
|
120
|
+
var c = s.charCodeAt(end - 1);
|
|
121
|
+
if (c !== 0x0D && c !== 0x0A && c !== 0x00) break;
|
|
122
|
+
end -= 1;
|
|
123
|
+
}
|
|
124
|
+
return end === s.length ? s : s.slice(0, end);
|
|
125
|
+
}
|
|
126
|
+
|
|
107
127
|
var PROFILES = Object.freeze({
|
|
108
128
|
strict: { timeoutMs: C.TIME.seconds(30), maxMessageBytes: C.BYTES.mib(25), maxResponseBytes: C.BYTES.mib(50) }, // operator-facing default mailbox cap
|
|
109
129
|
balanced: { timeoutMs: C.TIME.seconds(60), maxMessageBytes: C.BYTES.mib(50), maxResponseBytes: C.BYTES.mib(100) }, // operator-facing default mailbox cap
|
|
@@ -385,7 +405,7 @@ function create(opts) {
|
|
|
385
405
|
if (done) return;
|
|
386
406
|
done = true;
|
|
387
407
|
clearTimeout(to);
|
|
388
|
-
var reply = collector.result().toString("utf8")
|
|
408
|
+
var reply = _stripTrailingEol(collector.result().toString("utf8"));
|
|
389
409
|
// ClamAV INSTREAM reply: "<id>: <verdict>" where verdict is
|
|
390
410
|
// "stream: OK", "stream: <Sig.Name> FOUND", or "INSTREAM size
|
|
391
411
|
// limit exceeded. ERROR". This is a security verdict classifier,
|
|
@@ -198,7 +198,14 @@ function _extractDkimSignatures(headerBlock) {
|
|
|
198
198
|
function _extractDkimDTag(sigValue) {
|
|
199
199
|
var tags = sigValue.split(";");
|
|
200
200
|
for (var i = 0; i < tags.length; i += 1) {
|
|
201
|
-
|
|
201
|
+
// Native trim, not `/^\s+|\s+$/`. The regex form is the classic quadratic
|
|
202
|
+
// trim: `\s+$` is retried from every start position when the run does not
|
|
203
|
+
// reach the end. The tag is NOT bounded by the line cap, because the header
|
|
204
|
+
// is unfolded first — every empty continuation line adds one space to the
|
|
205
|
+
// run, so 40,000 of them in a 120 KB message produced a 40,000-character
|
|
206
|
+
// run and 446ms of backtracking. `String.prototype.trim` uses the same
|
|
207
|
+
// WhiteSpace and LineTerminator set as `\s`, so this decides identically.
|
|
208
|
+
var t = tags[i].trim();
|
|
202
209
|
if (t.length > 2 && t.charAt(0) === "d" && t.charAt(1) === "=") {
|
|
203
210
|
return t.slice(2).replace(/\s+/g, ""); // allow:regex-no-length-cap — value length bounded by tag length // allow:duplicate-regex — internal-WS strip
|
|
204
211
|
}
|
package/lib/mail-sieve.js
CHANGED
|
@@ -43,9 +43,10 @@
|
|
|
43
43
|
* parse time (require 'comparator-NAME' not in KNOWN_CAPABILITIES).
|
|
44
44
|
*
|
|
45
45
|
* Match-type wildcards: `:matches` uses `*` (any sequence) and `?`
|
|
46
|
-
* (one byte), per RFC 5228 §2.7.1.
|
|
47
|
-
*
|
|
48
|
-
*
|
|
46
|
+
* (one byte), per RFC 5228 §2.7.1. The pattern is matched directly
|
|
47
|
+
* against the value rather than translated into a regular
|
|
48
|
+
* expression, so a script author cannot spend the delivery thread's
|
|
49
|
+
* CPU by writing a pattern that a backtracking engine explores.
|
|
49
50
|
*
|
|
50
51
|
* The interpreter does NOT execute multi-script chains, sieve
|
|
51
52
|
* `include`s (RFC 6609), `notify` actions (RFC 5435), or `vacation`
|
|
@@ -63,7 +64,6 @@ var safeSieve = require("./safe-sieve");
|
|
|
63
64
|
var { defineClass } = require("./framework-error");
|
|
64
65
|
var numericBounds = require("./numeric-bounds");
|
|
65
66
|
var validateOpts = require("./validate-opts");
|
|
66
|
-
var codepointClass = require("./codepoint-class");
|
|
67
67
|
|
|
68
68
|
var MailSieveError = defineClass("MailSieveError", { alwaysPermanent: true });
|
|
69
69
|
|
|
@@ -124,22 +124,64 @@ function _envelopeAddresses(env, key) {
|
|
|
124
124
|
|
|
125
125
|
// ---- match-type ---------------------------------------------------------
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
// RFC 5228 §2.7.1 — `*` matches any sequence, `?` matches exactly one. Matched
|
|
128
|
+
// directly rather than translated into a regular expression.
|
|
129
|
+
//
|
|
130
|
+
// The translation is the obvious implementation and it is a denial of service.
|
|
131
|
+
// `*` becomes `.*`, so a pattern with N stars hands a backtracking engine N
|
|
132
|
+
// independent `.*` groups, and on a subject that never supplies the trailing
|
|
133
|
+
// literal the engine tries every way of dividing the subject between them: the
|
|
134
|
+
// cost is polynomial in the subject length with degree N. Three stars measured
|
|
135
|
+
// x7.7 per doubling of the subject, and ten stars — which a mailbox owner can
|
|
136
|
+
// type without meaning anything by it — did not finish on a 64-character
|
|
137
|
+
// Subject. A Sieve script is USER-authored in most deployments, and the gas
|
|
138
|
+
// budget does not help: gas counts operations, and one match is one operation
|
|
139
|
+
// however long the engine spends inside it.
|
|
140
|
+
//
|
|
141
|
+
// This walks the two strings with a single remembered wildcard position to fall
|
|
142
|
+
// back to. It is not linear: `*` followed by a long literal, matched against a
|
|
143
|
+
// subject that keeps almost matching it, re-walks the literal from each new
|
|
144
|
+
// alignment, so the worst case is the product of the two lengths. Measured at
|
|
145
|
+
// 3.9ms for a 500-character pattern against 2,000 characters, 11.4ms at
|
|
146
|
+
// 1,000 x 4,000, and 52.2ms at 2,000 x 8,000 — the quadratic shape, and a
|
|
147
|
+
// bound that both the script size and the header size already cap.
|
|
148
|
+
//
|
|
149
|
+
// What it is NOT is a function of how many wildcards the pattern contains,
|
|
150
|
+
// which is the property that mattered: the regex translation was polynomial
|
|
151
|
+
// with degree equal to the wildcard count, so a tenth `*` cost another factor
|
|
152
|
+
// of the subject length and a short Subject stopped returning at all.
|
|
153
|
+
// `i;ascii-casemap` folds US-ASCII A-Z to a-z and leaves every other character
|
|
154
|
+
// alone (RFC 4790 §9.2), which is also the only fold that preserves length.
|
|
155
|
+
// `String.prototype.toLowerCase` does not: `"İ".toLowerCase()` is two
|
|
156
|
+
// UTF-16 units, so folding the whole subject up front would shift every
|
|
157
|
+
// position after it and leave `?` — which matches exactly one character —
|
|
158
|
+
// facing two.
|
|
159
|
+
function _asciiLower(ch) {
|
|
160
|
+
var c = ch.charCodeAt(0);
|
|
161
|
+
return (c >= 0x41 && c <= 0x5A) ? String.fromCharCode(c + 0x20) : ch;
|
|
129
162
|
}
|
|
130
163
|
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
var
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
164
|
+
function _wildcardMatches(pattern, subject, caseInsensitive) {
|
|
165
|
+
var pat = pattern, sub = subject;
|
|
166
|
+
var p = 0, s = 0;
|
|
167
|
+
var starP = -1, starS = 0;
|
|
168
|
+
function same(a, bChar) {
|
|
169
|
+
return caseInsensitive ? _asciiLower(a) === _asciiLower(bChar) : a === bChar;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
while (s < sub.length) {
|
|
173
|
+
if (p < pat.length && (pat[p] === "?" || same(pat[p], sub[s]))) { p += 1; s += 1; continue; }
|
|
174
|
+
if (p < pat.length && pat[p] === "*") { starP = p; starS = s; p += 1; continue; }
|
|
175
|
+
// No match here. If a `*` came earlier, give it one more character and
|
|
176
|
+
// resume from just after it; otherwise the subject cannot match.
|
|
177
|
+
if (starP === -1) return false;
|
|
178
|
+
starS += 1;
|
|
179
|
+
s = starS;
|
|
180
|
+
p = starP + 1;
|
|
181
|
+
}
|
|
182
|
+
// Trailing stars can absorb the empty remainder; anything else cannot.
|
|
183
|
+
while (p < pat.length && pat[p] === "*") p += 1;
|
|
184
|
+
return p === pat.length;
|
|
143
185
|
}
|
|
144
186
|
|
|
145
187
|
function _matches(haystack, needle, matchType, comparator) {
|
|
@@ -155,7 +197,7 @@ function _matches(haystack, needle, matchType, comparator) {
|
|
|
155
197
|
: haystack.indexOf(needle) !== -1;
|
|
156
198
|
}
|
|
157
199
|
if (matchType === "matches") {
|
|
158
|
-
return
|
|
200
|
+
return _wildcardMatches(needle, haystack, ci);
|
|
159
201
|
}
|
|
160
202
|
throw new MailSieveError("mail-sieve/bad-match-type",
|
|
161
203
|
"unknown match-type: " + matchType);
|
package/lib/metrics.js
CHANGED
|
@@ -1024,7 +1024,7 @@ function _serializeRegistry(registry) {
|
|
|
1024
1024
|
// a JSON-friendly structured shape. Histograms get full buckets +
|
|
1025
1025
|
// bucket counts so downstream consumers compose
|
|
1026
1026
|
// `histogram_quantile()` against the snapshot without a separate
|
|
1027
|
-
// exposition endpoint
|
|
1027
|
+
// exposition endpoint.
|
|
1028
1028
|
var out = {};
|
|
1029
1029
|
var names = registry.metrics instanceof Map
|
|
1030
1030
|
? Array.from(registry.metrics.keys()).sort()
|
|
@@ -1073,8 +1073,8 @@ function snapshotStartWriter(opts) {
|
|
|
1073
1073
|
throw new MetricsError("metrics-snapshot/bad-fields",
|
|
1074
1074
|
"metrics.snapshot.startWriter: opts.fields must be a function returning the snapshot object");
|
|
1075
1075
|
}
|
|
1076
|
-
//
|
|
1077
|
-
//
|
|
1076
|
+
// The optional `registry` handle pulls every registered metric into a
|
|
1077
|
+
// structured `metrics` field in the JSON snapshot:
|
|
1078
1078
|
// counters / gauges as `{ value }` per label set, histograms as
|
|
1079
1079
|
// `{ buckets, observations }` with bucket counts + sum + count.
|
|
1080
1080
|
// Sidecar readers compose `histogram_quantile()` against the
|
|
@@ -1420,7 +1420,7 @@ function snapshotRender(snap, opts) {
|
|
|
1420
1420
|
"metrics.snapshot.render: snap must be a startWriter-produced object (got " + typeof snap + ")");
|
|
1421
1421
|
}
|
|
1422
1422
|
var fields = snap.fields;
|
|
1423
|
-
// Labeled registry families
|
|
1423
|
+
// Labeled registry families — a snapshot written with
|
|
1424
1424
|
// startWriter's `registry` option carries every registered counter /
|
|
1425
1425
|
// gauge / histogram under `metrics`. Both formats render them so a
|
|
1426
1426
|
// sidecar consuming a snapshot written by another process gets the
|
package/lib/mtls-ca.js
CHANGED
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
* `ca.crl` (signed CRL derived from the registry).
|
|
26
26
|
*
|
|
27
27
|
* `caKeySealedMode` defaults to "required" — sealed file required,
|
|
28
|
-
* plaintext refused.
|
|
29
|
-
*
|
|
30
|
-
* inverse of the framework's security-defaults-on posture for
|
|
28
|
+
* plaintext refused. There is no "auto" mode, because deciding
|
|
29
|
+
* for the operator means writing plaintext on a fresh install,
|
|
30
|
+
* which is the inverse of the framework's security-defaults-on posture for
|
|
31
31
|
* at-rest key material. The "disabled" mode is a dev-only opt-out
|
|
32
32
|
* (operator must justify with audited reason).
|
|
33
33
|
*
|
|
@@ -2554,7 +2554,7 @@ function create(opts) {
|
|
|
2554
2554
|
path: paths.crl };
|
|
2555
2555
|
}
|
|
2556
2556
|
|
|
2557
|
-
// ---- Algorithm migration
|
|
2557
|
+
// ---- Algorithm migration ----
|
|
2558
2558
|
|
|
2559
2559
|
// Serialize rotations on this handle. Two concurrent rotate() calls must not
|
|
2560
2560
|
// both read the same current generation, both mint the next one, and clobber
|
package/lib/network-dns.js
CHANGED
|
@@ -535,8 +535,8 @@ function _dnsQueryLabels(host, primitive) {
|
|
|
535
535
|
// A delimiter is the one that bites, because `domainToASCII` TRUNCATES at
|
|
536
536
|
// one, so `example.com/evil` can masquerade as a trusted prefix of itself.
|
|
537
537
|
//
|
|
538
|
-
// Mirroring the rule
|
|
539
|
-
//
|
|
538
|
+
// Mirroring the rule locally produces the list of near-misses above. Asking
|
|
539
|
+
// the owner is the version that cannot drift.
|
|
540
540
|
var canonical = publicSuffix.canonicalDomain(h);
|
|
541
541
|
if (!canonical) {
|
|
542
542
|
throw new DnsError("dns/bad-host",
|
|
@@ -301,8 +301,8 @@ function create(opts) {
|
|
|
301
301
|
// A no-store instance is still useful: the stateless fingerprint() reads no
|
|
302
302
|
// store and is the soft device-binding building block for self-validating
|
|
303
303
|
// tokens (a sealed cookie / JWT carrying the fingerprint inside). Rather than
|
|
304
|
-
// refuse to construct
|
|
305
|
-
// store
|
|
304
|
+
// refuse to construct, which would put fingerprint() out of reach for want of
|
|
305
|
+
// a store, build the instance and let the persisted bind()/verify() lifecycle
|
|
306
306
|
// throw a clear "no store configured" when actually called. Operators wanting
|
|
307
307
|
// ONLY the stateless digest can also use the static
|
|
308
308
|
// b.sessionDeviceBinding.fingerprint(req, opts) with no create() at all.
|
package/lib/vault/index.js
CHANGED
|
@@ -472,8 +472,8 @@ async function initFirstRunWrapped() {
|
|
|
472
472
|
"failed to wrap new vault key: " + e.message);
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
-
// Atomic write via the framework's atomic-file primitive
|
|
476
|
-
// rename + dir fsync
|
|
475
|
+
// Atomic write via the framework's atomic-file primitive: temp + fsync +
|
|
476
|
+
// rename + dir fsync.
|
|
477
477
|
atomicFile.writeSync(paths.sealed, sealed, { fileMode: 0o600 });
|
|
478
478
|
|
|
479
479
|
log("generated and sealed new vault keypair (ML-KEM-1024 + P-384 hybrid)");
|
package/lib/ws-client.js
CHANGED
|
@@ -453,8 +453,8 @@ class WsClient extends EventEmitter {
|
|
|
453
453
|
if (lookup) tlsOpts.lookup = lookup;
|
|
454
454
|
// The group preference arrives with the shared posture above, already
|
|
455
455
|
// reflecting a runtime setKeyShares(). It lands as `ecdhCurve`: node:tls
|
|
456
|
-
// has no `curves` option — it accepts that key and ignores it,
|
|
457
|
-
//
|
|
456
|
+
// has no `curves` option — it accepts that key and ignores it, so a
|
|
457
|
+
// preference sent under that name leaves the handshake in silence,
|
|
458
458
|
// whereas a bad `ecdhCurve` throws. An operator value in dialTlsOpts
|
|
459
459
|
// still wins, since it is merged last.
|
|
460
460
|
socket = tls.connect(tlsOpts);
|
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:0535e946-47d0-4e95-879e-65d801bc533b",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
8
|
+
"timestamp": "2026-08-22T21:30:23.773Z",
|
|
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.18.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.18.50",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.18.
|
|
25
|
+
"version": "0.18.50",
|
|
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.18.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.18.50",
|
|
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.18.
|
|
57
|
+
"ref": "@blamejs/core@0.18.50",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|