@blamejs/blamejs-shop 0.5.3 → 0.5.4
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/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +12 -10
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/lib/flag-targeting.js +14 -1
- package/lib/vendor/blamejs/lib/guard-regex.js +52 -3
- package/lib/vendor/blamejs/lib/mcp.js +9 -1
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.38.json +26 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/flag.test.js +24 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mcp.test.js +15 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.5.x
|
|
10
10
|
|
|
11
|
+
- v0.5.4 (2026-06-27) — **Vendored framework refreshed to 0.15.38 — caller-supplied regexes are screened for ReDoS before they run on request data.** Updates the vendored blamejs framework to 0.15.38. The upstream release closes two ReDoS exposures and strengthens the framework's regex guard: a feature-flag targeting condition (`op: regex`) and an MCP tool's input-schema `pattern` each compiled a caller-supplied regex and matched it against request-controlled input behind only a length cap — which does not bound catastrophic backtracking, so a six-character pattern like `(a+)+$` could pin a CPU. Both now pass through `b.guardRegex` (strict) before compilation, and the nested-quantifier detector was hardened to also catch wrapped forms (`((a)+)+`, `(([a-z]+)*)*`) that previously slipped past — which strengthens every guardRegex caller. This storefront exposes neither MCP tools nor operator-authored regex flag rules on its request path, and its own patterns are static and ReDoS-clean, so there is no runtime or behavior change for it; the hardened framework rides along and no operator action is required. **Changed:** *Vendored framework refreshed to 0.15.38 (caller-supplied regexes screened for ReDoS)* — Refreshes the vendored blamejs framework to 0.15.38. The upstream fix screens an operator-supplied feature-flag targeting regex and an MCP tool input-schema `pattern` through `b.guardRegex` (strict) before they are compiled and matched against request input, so a catastrophic-backtracking shape is refused at validation time instead of pinning a CPU; the nested-quantifier detector also now catches wrapped forms at any group depth, strengthening every guardRegex caller. The shipped storefront does not expose those caller-supplied-regex surfaces and its own patterns are static, so its runtime, public API, and wire format are unchanged; the hardened framework simply rides along.
|
|
12
|
+
|
|
11
13
|
- v0.5.3 (2026-06-27) — **Vendored framework refreshed to 0.15.37 — non-finite numeric options can no longer silently disable a security check.** Updates the vendored blamejs framework to 0.15.37. The upstream release closes a class of fail-open configuration bugs: several numeric options validated a value only as `typeof === number && >= 0`, which accepts `Infinity`. Where the value is a clock-skew tolerance or a resource cap, an `Infinity` (or `NaN`) silently turned off the very protection it tunes — an `Infinity` CWT / OCSP-staple / ARC clock-skew made the expiry and freshness comparisons unsatisfiable (so an expired token, a replayed pre-revocation OCSP `good` response, or an expired ARC seal would be accepted), and an `Infinity` WebSocket message/frame cap, inbox/flag-cache size cap, or audit-chain fan-out cap disabled the bound. These options now route through the framework's finite-bounds validator, refusing a non-finite value at the entry point (or falling back to the safe default), while options where unbounded is deliberate (reconnect-indefinitely, retain-indefinitely) still accept it. This storefront configures these options with finite values, so there is no runtime or behavior change for it; the hardened framework rides along and no operator action is required. **Changed:** *Vendored framework refreshed to 0.15.37 (non-finite option values rejected)* — Refreshes the vendored blamejs framework to 0.15.37. The upstream fix makes a clock-skew tolerance or resource cap of `Infinity` / `NaN` a rejected value rather than one that silently disables the check it configures — covering the CWT / OCSP-staple / ARC time checks, the WebSocket-client inbound size and handshake caps, and the inbox / flag-cache / audit-chain size and count caps, with deliberately-unbounded options (reconnect/retain indefinitely) unaffected. The shipped storefront passes finite values to these options, so its runtime, public API, and wire format are unchanged; the hardened framework simply rides along.
|
|
12
14
|
|
|
13
15
|
- v0.5.2 (2026-06-27) — **Vendored framework refreshed to 0.15.36.** Updates the vendored blamejs framework to 0.15.36. The upstream release is internal test-suite tooling only: it re-verifies the guard-suite class that allows a bare comma/semicolon split on token-only RFC header grammars (RRULE, RFC 9421 signature components, TLS-RPT rua, SCIM attribute paths), renames its marker to a descriptive token, and prunes five inert marker comments. No runtime code, public API, or wire format changed upstream, so there is no runtime or behavior change for this storefront and no operator action is required. **Changed:** *Vendored framework refreshed to 0.15.36* — Refreshes the vendored blamejs framework to 0.15.36, an upstream release scoped to test-suite tooling — a guard-suite class re-verified and its marker renamed, plus five inert marker comments removed. The shipped framework runtime, public API, and wire format are unchanged, so no operator action is required.
|
package/lib/asset-manifest.json
CHANGED
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
|
|
4
4
|
"packages": {
|
|
5
5
|
"blamejs": {
|
|
6
|
-
"version": "0.15.
|
|
7
|
-
"tag": "v0.15.
|
|
6
|
+
"version": "0.15.38",
|
|
7
|
+
"tag": "v0.15.38",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -860,6 +860,7 @@
|
|
|
860
860
|
"release-notes/v0.15.35.json": "lib/vendor/blamejs/release-notes/v0.15.35.json",
|
|
861
861
|
"release-notes/v0.15.36.json": "lib/vendor/blamejs/release-notes/v0.15.36.json",
|
|
862
862
|
"release-notes/v0.15.37.json": "lib/vendor/blamejs/release-notes/v0.15.37.json",
|
|
863
|
+
"release-notes/v0.15.38.json": "lib/vendor/blamejs/release-notes/v0.15.38.json",
|
|
863
864
|
"release-notes/v0.15.4.json": "lib/vendor/blamejs/release-notes/v0.15.4.json",
|
|
864
865
|
"release-notes/v0.15.5.json": "lib/vendor/blamejs/release-notes/v0.15.5.json",
|
|
865
866
|
"release-notes/v0.15.6.json": "lib/vendor/blamejs/release-notes/v0.15.6.json",
|
|
@@ -1568,7 +1569,7 @@
|
|
|
1568
1569
|
".npmrc": "sha256:66f104e7d07c496d2d0409988225e8c0e4ceb8d247dbcac3be75b2128d20ce66",
|
|
1569
1570
|
".pinact.yaml": "sha256:0213ffda55961dc49b64c0a5dfa3c0567419633b1499d57eaf7c8d842d7da6c7",
|
|
1570
1571
|
"ARCHITECTURE.md": "sha256:9b1c8d2b1b7a41838eb348b0a008e4b4369718fd72bfe2974b37155f7536d35b",
|
|
1571
|
-
"CHANGELOG.md": "sha256:
|
|
1572
|
+
"CHANGELOG.md": "sha256:d46b3114be4ce1b16f02a49559affdf7dcf61567153b2a93f1be3d01e90de015",
|
|
1572
1573
|
"CODE_OF_CONDUCT.md": "sha256:148a281960fff7c2fe6554dab66da572c72245ddeb00b0d14811558397bff386",
|
|
1573
1574
|
"CONTRIBUTING.md": "sha256:bb4dbdbc8598da31dbce653a8ed322e08ff46560173f2eb67a4d684653948332",
|
|
1574
1575
|
"GOVERNANCE.md": "sha256:906df6afb1f552b27b9acb50f7f96c47b917a2f1021cd4e987dbf4ee0e0a821b",
|
|
@@ -1578,7 +1579,7 @@
|
|
|
1578
1579
|
"NOTICE": "sha256:f487fa47a11aca0f89e2615cdd3c713e9842abf7a30d8d328eeeae1c864aa774",
|
|
1579
1580
|
"README.md": "sha256:3ddcc197b003da0b02db8bdd1aef1e943c94f7eab613c633d6a45bb11d0a80e9",
|
|
1580
1581
|
"SECURITY.md": "sha256:77d8c2bcc04b425a08ef30e51204cebddbd48954b028a259c7a8412dbfeab40a",
|
|
1581
|
-
"api-snapshot.json": "sha256:
|
|
1582
|
+
"api-snapshot.json": "sha256:179287667873f052b36735f3473fbce51aa51c9ed26836bbbdfa854e096a6c0c",
|
|
1582
1583
|
"assets/BlameJS_Logo.png": "sha256:3c65699753c771b48ef9ac7f45bb40815ec19a23afcdd0cd30ef4601bbbe293e",
|
|
1583
1584
|
"assets/BlameJS_Logo.svg": "sha256:dda44f3fb1343d5de9db6b1fcdb75fc649c57e7a99a8e8239fcf852e3841e1a8",
|
|
1584
1585
|
"bench/README.md": "sha256:74202f2507fd840bfc1ac6c681975d9273cf36cca6e0f72655f138337304033c",
|
|
@@ -1958,7 +1959,7 @@
|
|
|
1958
1959
|
"lib/flag-cache.js": "sha256:4333ce5105e2a2970b7277fa77f6664d1c6d2e3c6e7c3fa6094c228d305df4dc",
|
|
1959
1960
|
"lib/flag-evaluation-context.js": "sha256:fa8846a9a97a9d773dc08dd0da664d1d6d3fead4e12dc9982b33fa45b59d47a8",
|
|
1960
1961
|
"lib/flag-providers.js": "sha256:d85ff62c2d599634c3d2dc9d4e4a8cfcecec8178ce63a2800438434cc365f84a",
|
|
1961
|
-
"lib/flag-targeting.js": "sha256:
|
|
1962
|
+
"lib/flag-targeting.js": "sha256:70c1d14b672600322a472a6dca6e149cb78428f7f96d7e9733bea5e25ec42ac4",
|
|
1962
1963
|
"lib/flag.js": "sha256:3eab2c7e2f86fdf0a099d4ba287ec69486ee985c5496077ce79167979f6fccaf",
|
|
1963
1964
|
"lib/forms.js": "sha256:e8a90aee2497da53d62fed67a674aefda3b4665c75dcee284334ec762753b692",
|
|
1964
1965
|
"lib/framework-error.js": "sha256:6b707ad3cb0c1f180671165381ef228fa9ceb7187bd7202ff6eec0af03304a28",
|
|
@@ -2011,7 +2012,7 @@
|
|
|
2011
2012
|
"lib/guard-pdf.js": "sha256:7a739adea96a1d6624858232e9b5991071aee5a25253914bbcc1f51b70e06710",
|
|
2012
2013
|
"lib/guard-pop3-command.js": "sha256:f4c62cf6e5c4e4ccdaf7f39f8a738b6febeedd2b2ce65abf7d365e7a19fdfa95",
|
|
2013
2014
|
"lib/guard-posture-chain.js": "sha256:acf47bba9c1e5ad743965a509ff97c067f6fa157fbd08fc57b63965adcc52411",
|
|
2014
|
-
"lib/guard-regex.js": "sha256:
|
|
2015
|
+
"lib/guard-regex.js": "sha256:ef6100d7b68c017166454301d787f7bffc988e97bcc1b0b09981c045b55b0d16",
|
|
2015
2016
|
"lib/guard-saga-config.js": "sha256:b7e0031f95adea39cc4ac7a9c2487d62b49b833c058ea90e5962d6b0bcea82f3",
|
|
2016
2017
|
"lib/guard-shell.js": "sha256:2f7948748cb4bac489e973573f4a7b728306eda4d9e308e5c9a40e23b17dcfb3",
|
|
2017
2018
|
"lib/guard-smtp-command.js": "sha256:a4faf89c1582901a20976950191da8f7ecdeae1a2c47a429147f09f464ece146",
|
|
@@ -2108,7 +2109,7 @@
|
|
|
2108
2109
|
"lib/markup-escape.js": "sha256:a76e8c77f30ae456caa53c33afeb2dd0b27f7b8799ac857beac86ea038f17d9a",
|
|
2109
2110
|
"lib/markup-tokenizer.js": "sha256:8aa91d3e173c8d3d4df4dac00e892a359863be7295c5b1846cf322bc7bfc9510",
|
|
2110
2111
|
"lib/mcp-tool-registry.js": "sha256:df3480e6a0d29a9734bdb19d322f83bff5ebfdd756620f947cf593878dea8776",
|
|
2111
|
-
"lib/mcp.js": "sha256:
|
|
2112
|
+
"lib/mcp.js": "sha256:b7717a2e3928429c745e049512fa1c91f9ea44eb35c0f4324e2083d2f3fdab28",
|
|
2112
2113
|
"lib/mdoc.js": "sha256:bc31d38d6fdfac8db8f040dfa4d33ab40405f56340bb4ec978f3ba98e93f6433",
|
|
2113
2114
|
"lib/metrics.js": "sha256:55d445b738b2cfc8ceb1bfa7a1e69b306497e8065ea86d61e4f4087873acce1b",
|
|
2114
2115
|
"lib/middleware/age-gate.js": "sha256:4d4967beef100e2e58f1565de770e31fb8dd50dab2f04fa8251972fea5607826",
|
|
@@ -2349,7 +2350,7 @@
|
|
|
2349
2350
|
"oss-fuzz/projects/blamejs/README.md": "sha256:ae13b7bb79ed8d69b1b3276e5562807a0349fb6e6b7d11cf1f683aad1eafdb4b",
|
|
2350
2351
|
"oss-fuzz/projects/blamejs/build.sh": "sha256:0ced1cf21782c97be7f8d74faf5e27a308b60b2f858836fb5ca3b8c4e939a8f7",
|
|
2351
2352
|
"oss-fuzz/projects/blamejs/project.yaml": "sha256:59f2cb83aa622325a175b77416fe155be15b70a9c798bd1a78bba05763b1b03d",
|
|
2352
|
-
"package.json": "sha256:
|
|
2353
|
+
"package.json": "sha256:a1c6ea35b2eb0ec3bd7bc2dfb809260f5bad56202962bed60b939b0d1ad01d07",
|
|
2353
2354
|
"release-notes/v0.0.x.json": "sha256:7a49819f30068ee119000cad7010194882bb8bfaa12acbdab4dfc066efb7982f",
|
|
2354
2355
|
"release-notes/v0.1.x.json": "sha256:6742a8c17f947c5cb76f69dead7eea86b942d80621d914b774ba5488e09937e5",
|
|
2355
2356
|
"release-notes/v0.10.x.json": "sha256:fe498045daf88337bd3d987e5964aa42c99a50e1685b6f09e51f698b8687726f",
|
|
@@ -2389,6 +2390,7 @@
|
|
|
2389
2390
|
"release-notes/v0.15.35.json": "sha256:e317b2252734aace7a0f734e3ab40c3a0c4ac208d678c0e9834be34eb424d15b",
|
|
2390
2391
|
"release-notes/v0.15.36.json": "sha256:392c15724efc02fad5825aaf93b3f5dcef90a0cb9909d61f86e777f3061045ec",
|
|
2391
2392
|
"release-notes/v0.15.37.json": "sha256:b696debed024bed09c59eaeb467d6dde93d7f30ad33f46af8a37d07534df09d8",
|
|
2393
|
+
"release-notes/v0.15.38.json": "sha256:8a2e21eec92c95173689cdcf6055b9d3375ba0f765692c7cdb1e7313a5010ddc",
|
|
2392
2394
|
"release-notes/v0.15.4.json": "sha256:6ac7fa0ef1728c27e71b2050d1b07a810f9b4b1440ccddbf28ad56e2f54d8585",
|
|
2393
2395
|
"release-notes/v0.15.5.json": "sha256:cca1d0edd5d6fc41b512d19d98be224b990dcab41478622c11962f0fcb1bb09a",
|
|
2394
2396
|
"release-notes/v0.15.6.json": "sha256:0e3b9e5e43b70b61dd258c3003d1b8729cd3c26c62a34dedcca81bbec5d31077",
|
|
@@ -2739,7 +2741,7 @@
|
|
|
2739
2741
|
"test/layer-0-primitives/fido-mds3.test.js": "sha256:91893e57258f91386ea3baedf8727dac08b5c75725a74b74ecdfbc5d99bb4adc",
|
|
2740
2742
|
"test/layer-0-primitives/file-type.test.js": "sha256:ba6bb725857cbf7b165f4391e3aa126c9fc4286c5afb92d1964ff1d0dfd2b57c",
|
|
2741
2743
|
"test/layer-0-primitives/file-upload-content-safety-skip-audit.test.js": "sha256:ad13f7f84a33899b4f4e66b74c896f606e070bfe5df769b0acd109cfb7ab38c7",
|
|
2742
|
-
"test/layer-0-primitives/flag.test.js": "sha256:
|
|
2744
|
+
"test/layer-0-primitives/flag.test.js": "sha256:e834ee3ae6469ada26eb3783dbba943c8ebce6c34787d8e1e8f9ff4dd07a5176",
|
|
2743
2745
|
"test/layer-0-primitives/forensic-snapshot.test.js": "sha256:629a8af1ccd0396e2b30f8e46d76fe19eab9f33c156c0db78e3e76649e21030c",
|
|
2744
2746
|
"test/layer-0-primitives/fsm.test.js": "sha256:7037831083426478ed4efd7d1c17d4b96c3609816a41819b5c9da71d2b6187e8",
|
|
2745
2747
|
"test/layer-0-primitives/gate-contract-content-gate.test.js": "sha256:d9f8b94231efb0e02a94b7934bf58a3f8e343829ce11346b7302e69e13efd551",
|
|
@@ -2866,7 +2868,7 @@
|
|
|
2866
2868
|
"test/layer-0-primitives/mail-unsubscribe.test.js": "sha256:ffe070b31ae59b1f3cb1582b56f494501dc97a493e89111573a8383fabf1a269",
|
|
2867
2869
|
"test/layer-0-primitives/mail.test.js": "sha256:ef0dafe19c60852542f7cc523dfb0c92347261c43d706eeed58db9934d9f5c50",
|
|
2868
2870
|
"test/layer-0-primitives/mcp-tool-registry.test.js": "sha256:4d9a9a367489d93afcc8e0bca3442e0aefc683e40ddd29e6e2cddbe3a3b8732f",
|
|
2869
|
-
"test/layer-0-primitives/mcp.test.js": "sha256:
|
|
2871
|
+
"test/layer-0-primitives/mcp.test.js": "sha256:31a29bee340edac7f305e8dc18b44134140dc058c5bdc1dc137f1f18886ad2af",
|
|
2870
2872
|
"test/layer-0-primitives/mdoc.test.js": "sha256:2fe1086192385e71f858554d676b299b86594d34226e1d774ffefc97b5fa03a4",
|
|
2871
2873
|
"test/layer-0-primitives/metrics-shadow-registry.test.js": "sha256:a91a9efdb4aa28caed846b969d7c1e5ebfc57067afad85800daa430942d02725",
|
|
2872
2874
|
"test/layer-0-primitives/metrics-snapshot.test.js": "sha256:aa405be22acb6680b062d1de1aa7ce00029e414218a0e4e521e10a14b2254851",
|
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.15.x
|
|
10
10
|
|
|
11
|
+
- v0.15.38 (2026-06-27) — **Regex patterns supplied in feature-flag targeting rules and MCP tool input schemas are now screened for catastrophic-backtracking (ReDoS) shapes before compilation, so a pattern matched against request data can't pin a CPU.** Two places compiled a caller-supplied regex pattern and `.test()`'d it against request-controlled input with only a length bound as the stated defense: a feature-flag targeting condition (`op: "regex"`) matched against runtime attribute values, and an MCP tool's input-schema `pattern` matched against tool-call arguments. A length bound is not a ReDoS defense — a catastrophic-backtracking pattern such as `(a+)+$` is six characters and pins a CPU on a crafted input. Both patterns now pass through `b.guardRegex` (strict profile) before compilation, which refuses nested-quantifier, alternation-with-quantifier, and quantifier-inside-lookaround shapes. A ReDoS-shaped flag pattern is refused when the rules are validated; a ReDoS-shaped MCP schema pattern fails tool-input validation. Patterns built from the framework's own static tables, operator-owned JSON Schema patterns, the Sieve glob translator (which cannot express nested quantifiers), and the I-Regexp translator (linear by dialect) are unchanged. **Security:** *Feature-flag regex targeting conditions are screened for ReDoS before compilation* — A flag targeting rule with `op: "regex"` compiled the operator-supplied pattern and `.test()`'d it against runtime attribute values, guarded only by a 200-character length cap. Length does not bound catastrophic backtracking, so a pattern like `(a+)+$` combined with an attacker-controlled attribute value could pin a CPU during flag evaluation. The pattern is now screened through b.guardRegex (strict) when the rules are validated, and a catastrophic-backtracking shape is refused with a clear error. · *MCP tool input-schema patterns are screened for ReDoS before matching request input* — b.mcp.validateToolInput compiled a tool author's input-schema `pattern` and matched it against tool-call argument values; the 4096-character input cap does not bound backtracking (a `(a+)+$` pattern blows up at roughly forty input characters). The schema pattern is now screened through b.guardRegex (strict) before compilation, so a ReDoS-shaped pattern in a registered tool's schema fails input validation instead of letting hostile arguments hang the validator. · *b.guardRegex now catches wrapped nested-quantifier patterns* — The nested-quantifier detector matched a quantified group whose body contained a quantifier, but its inner match was paren-blind, so wrapping the inner quantifier in an extra group — `((a)+)+`, `(([a-z]+)*)*`, `((a+))+` — slipped past it while remaining catastrophic. A structural scan now tracks group nesting and refuses an unbounded-quantified group whose body itself contains an unbounded quantifier at any depth, so the wrapped forms are rejected alongside the bare `(a+)+`. Bounded repeats (`{n}`, `{n,m}`) are unaffected. This strengthens every b.guardRegex caller, including the flag-targeting and MCP screening above.
|
|
12
|
+
|
|
11
13
|
- v0.15.37 (2026-06-27) — **Several numeric options that silently accepted a non-finite value — disabling a clock-skew / freshness check or a resource cap — now reject it, so an `Infinity` skew or limit can no longer turn off the protection it configures.** A number of configuration options validated a numeric value with a bare `typeof === "number" && value >= 0` check, which accepts `Infinity`. Where the value is a clock-skew tolerance or a resource cap, an `Infinity` (or `NaN`) silently disabled the very check it tunes: a CWT / OCSP-staple / ARC clock-skew of `Infinity` made the expiry, freshness, and expiration comparisons unsatisfiable (an expired token / a replayed pre-revocation "good" response / an expired ARC seal would be accepted); a WebSocket-client `maxMessageBytes` / `maxFrameBytes` / `handshakeTimeoutMs` of `Infinity` disabled the inbound-OOM and stalled-handshake defenses; and inbox / flag-cache / audit-chain size and count caps of `Infinity` admitted unbounded data. These options now route through the finite-bounds validator: a present non-finite value is refused at the entry point (or falls back to the safe default on the result-returning paths). Options where an unbounded value is a deliberate intent — reconnect "retry indefinitely", inbox "retain indefinitely" — continue to accept `Infinity`. **Security:** *A non-finite clock-skew no longer disables CWT / OCSP / ARC time checks* — b.cwt.verify, the OCSP-staple freshness check in b.network.tls, and b.mail.arc.verify each took an operator clock-skew tolerance validated as `typeof === "number" && >= 0`, which accepts `Infinity`. Because each check is of the form `now > deadline + skew`, a skew of `Infinity` made it unsatisfiable and silently turned the check off: an expired CWT verified, a stale (post-nextUpdate) OCSP "good" response — the exact reply an attacker replays after the certificate is revoked — was accepted, and an expired ARC seal passed. A present clock-skew that is not a non-negative finite integer is now refused (b.cwt.verify throws cwt/bad-clock-skew; the OCSP and ARC paths fall back to their safe default). Regression tests assert an expired token / stale staple / expired ARC seal is still rejected when the skew is `Infinity`. · *WebSocket-client inbound caps can no longer be disabled by an Infinity value* — b.wsClient.connect validated maxMessageBytes, maxFrameBytes, and handshakeTimeoutMs (and the ping/pong keepalive intervals) with a bare numeric check that accepted `Infinity`, which disabled the inbound-message and frame size limits — the defenses against a malicious server sending an unbounded message — and the handshake timeout. A present non-finite value for these is now refused at connect time. The reconnect maxAttempts still accepts `Infinity` (a deliberate "reconnect indefinitely" intent). · *Inbox, flag-cache, and audit-chain caps reject a non-finite limit* — The inbox maxPayloadBytes / messageIdMaxLen / sourceMaxLen caps, the flag-cache ttlMs / maxEntries, and the audit-chain partition fan-out cap each accepted `Infinity`, disabling the cap (unbounded stored payloads, a never-expiring or unbounded cache, unbounded fan-out). These now require a positive finite integer — refused at create time, or clamped to the bounded default on the result-returning verify path. The inbox retentionDays still accepts `Infinity` (retain indefinitely).
|
|
12
14
|
|
|
13
15
|
- v0.15.36 (2026-06-27) — **Internal test-suite hardening only — the published library's runtime behavior and public API are unchanged (source-comment marker text aside).** The codebase-patterns guard class that allows a bare comma/semicolon split on token-only RFC header grammars was re-verified from scratch and renamed to a descriptive token, with its old name recorded as retired. Each live marked site (RRULE, RFC 9421 component identifiers, TLS-RPT rua, SCIM attribute paths) was re-read and confirmed to split a grammar with no quoted-string members. Five marker comments that suppressed nothing were removed or turned into plain explanatory comments. No runtime code, public API, or wire format changed. **Detectors:** *Bare token-only header-split suppression class re-verified, renamed, and pruned of inert markers* — Each marked bare `.split(",")` / `.split(";")` on an RFC header value was re-read and confirmed to operate on a token-only grammar (no quoted-string members, so a quote-aware splitter is unnecessary): RFC 5545 RRULE, RFC 9421 signature component identifiers, RFC 8460 TLS-RPT rua, and RFC 7644 SCIM attribute paths. The guard class was renamed to a descriptive token and its old name added to the retired-token set. Five marker comments that the detector never actually evaluated (two sat on a date-normalizing `.replace`, three in header parsers the guard intentionally does not scan) were removed or converted to plain comments. This is test-suite tooling plus source-comment text; no shipped framework behavior changed.
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
var validateOpts = require("./validate-opts");
|
|
30
|
+
var lazyRequire = require("./lazy-require");
|
|
31
|
+
var guardRegex = lazyRequire(function () { return require("./guard-regex"); });
|
|
30
32
|
var { defineClass } = require("./framework-error");
|
|
31
33
|
var FlagError = defineClass("FlagError", { alwaysPermanent: true });
|
|
32
34
|
|
|
@@ -171,8 +173,19 @@ function validateRules(rules, label) {
|
|
|
171
173
|
throw new FlagError("flag/bad-condition",
|
|
172
174
|
clabel + ".value: regex pattern must be <= 200 chars (DoS defense)");
|
|
173
175
|
}
|
|
176
|
+
// The compiled regex is .test()'d against runtime attribute values, so a
|
|
177
|
+
// catastrophic-backtracking (ReDoS) pattern is a DoS vector. A length
|
|
178
|
+
// bound does NOT defend ReDoS (`(a+)+$` is 6 chars); screen the pattern
|
|
179
|
+
// through b.guardRegex first — it refuses nested-quantifier / alternation-
|
|
180
|
+
// quantifier / lookaround-quantifier shapes before compilation.
|
|
174
181
|
try {
|
|
175
|
-
|
|
182
|
+
guardRegex().sanitize(cond.value, { profile: "strict" });
|
|
183
|
+
} catch (ge) {
|
|
184
|
+
throw new FlagError("flag/bad-condition",
|
|
185
|
+
clabel + ".value: regex pattern rejected as unsafe (ReDoS shape) - " + ge.message);
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
// allow:dynamic-regex — operator targeting pattern, ReDoS-screened via guardRegex.sanitize (strict) + length-bounded to 200 chars above
|
|
176
189
|
validatedCond._compiledRegex = new RegExp(cond.value);
|
|
177
190
|
} catch (e) {
|
|
178
191
|
throw new FlagError("flag/bad-condition",
|
|
@@ -128,19 +128,68 @@ var DEFAULTS = gateContract.strictDefaults(PROFILES);
|
|
|
128
128
|
|
|
129
129
|
var COMPLIANCE_POSTURES = gateContract.compliancePostures(PROFILES, { base: 256 });
|
|
130
130
|
|
|
131
|
+
// Structural nested-unbounded-quantifier detector. NESTED_QUANT_RE is paren-
|
|
132
|
+
// blind (its `[^()]*` can't span a nested group), so it misses WRAPPED forms
|
|
133
|
+
// like `((a)+)+` / `(([a-z]+)*)*` / `((a+))+` — adding one extra group around
|
|
134
|
+
// the inner quantifier bypasses the regex while the pattern stays catastrophic.
|
|
135
|
+
// This linear scan tracks group nesting and flags an unbounded-quantified group
|
|
136
|
+
// (`)+`, `)*`, `){n,}`) whose body itself contains an unbounded quantifier — the
|
|
137
|
+
// two-nested-unbounded-quantifier ReDoS class — at any group depth. Bounded
|
|
138
|
+
// repeats (`{n}`, `{n,m}`, `?`) are not unbounded, so they don't trip it (the
|
|
139
|
+
// large-bound case is handled separately by maxBoundedRepeat).
|
|
140
|
+
function _hasNestedQuantifier(src) {
|
|
141
|
+
var stack = []; // open groups: each { quant } — body has an unbounded quantifier
|
|
142
|
+
var inClass = false; // inside a [...] character class
|
|
143
|
+
var i = 0;
|
|
144
|
+
var n = src.length;
|
|
145
|
+
var UNBOUNDED_AFTER_GROUP = /^(?:[*+]\??|\{\d*,\})/; // )+ )* )+? )*? ){n,}
|
|
146
|
+
while (i < n) {
|
|
147
|
+
var c = src.charAt(i);
|
|
148
|
+
if (c === "\\") { i += 2; continue; } // escaped atom — skip both chars
|
|
149
|
+
if (inClass) { if (c === "]") inClass = false; i += 1; continue; }
|
|
150
|
+
if (c === "[") { inClass = true; i += 1; continue; }
|
|
151
|
+
if (c === "(") { stack.push({ quant: false }); i += 1; continue; }
|
|
152
|
+
if (c === ")") {
|
|
153
|
+
var grp = stack.pop() || { quant: false };
|
|
154
|
+
var qm = UNBOUNDED_AFTER_GROUP.exec(src.slice(i + 1)); // allow:regex-no-length-cap — bounded slice of a maxPatternBytes-capped input
|
|
155
|
+
var closeUnbounded = qm !== null;
|
|
156
|
+
if (grp.quant && closeUnbounded) return true; // nested unbounded quantifier → catastrophic
|
|
157
|
+
// The closing group contributes an unbounded quantifier to its PARENT's
|
|
158
|
+
// body if its own body had one, or if it is itself unbounded-quantified.
|
|
159
|
+
if (stack.length && (grp.quant || closeUnbounded)) stack[stack.length - 1].quant = true;
|
|
160
|
+
i += 1 + (qm ? qm[0].length : 0);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (c === "*" || c === "+") { // unbounded quantifier on the preceding atom
|
|
164
|
+
if (stack.length) stack[stack.length - 1].quant = true;
|
|
165
|
+
i += 1; continue;
|
|
166
|
+
}
|
|
167
|
+
if (c === "{") {
|
|
168
|
+
var open = /^\{\d*,\}/.exec(src.slice(i)); // allow:regex-no-length-cap — bounded slice // {n,} unbounded
|
|
169
|
+
if (open) { if (stack.length) stack[stack.length - 1].quant = true; i += open[0].length; continue; }
|
|
170
|
+
var bounded = /^\{\d+(?:,\d+)?\}/.exec(src.slice(i)); // allow:regex-no-length-cap — bounded slice // {n} / {n,m} bounded
|
|
171
|
+
if (bounded) { i += bounded[0].length; continue; }
|
|
172
|
+
i += 1; continue; // literal `{`
|
|
173
|
+
}
|
|
174
|
+
i += 1;
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
131
179
|
|
|
132
180
|
function _detectIssues(input, opts) {
|
|
133
181
|
var pre = gateContract.detectStringInput(input, opts, { name: "regex", noun: "regex pattern", cap: { bytes: opts.maxPatternBytes, kind: "pattern-cap", snippet: "regex pattern exceeds maxPatternBytes " + opts.maxPatternBytes } });
|
|
134
182
|
if (pre.done) return pre.issues;
|
|
135
183
|
var issues = pre.issues;
|
|
136
184
|
|
|
137
|
-
if (opts.nestedQuantPolicy !== "allow" &&
|
|
185
|
+
if (opts.nestedQuantPolicy !== "allow" &&
|
|
186
|
+
(NESTED_QUANT_RE.test(input) || _hasNestedQuantifier(input))) { // allow:regex-no-length-cap — input bounded by maxPatternBytes
|
|
138
187
|
issues.push({
|
|
139
188
|
kind: "nested-quantifier", severity: "critical",
|
|
140
189
|
ruleId: "regex.nested-quantifier",
|
|
141
190
|
snippet: "pattern contains nested-quantifier shape (e.g. " +
|
|
142
|
-
"`(a+)+`) — canonical ReDoS catastrophic-
|
|
143
|
-
"class (CVE-2024-21538 cross-spawn / CVE-2022-25929)",
|
|
191
|
+
"`(a+)+` / `((a)+)+`) — canonical ReDoS catastrophic-" +
|
|
192
|
+
"backtracking class (CVE-2024-21538 cross-spawn / CVE-2022-25929)",
|
|
144
193
|
});
|
|
145
194
|
}
|
|
146
195
|
|
|
@@ -37,6 +37,8 @@ var safeJson = require("./safe-json");
|
|
|
37
37
|
var safeBuffer = require("./safe-buffer");
|
|
38
38
|
var requestHelpers = require("./request-helpers");
|
|
39
39
|
var audit = require("./audit");
|
|
40
|
+
var lazyRequire = require("./lazy-require");
|
|
41
|
+
var guardRegex = lazyRequire(function () { return require("./guard-regex"); });
|
|
40
42
|
var { McpError } = require("./framework-error");
|
|
41
43
|
|
|
42
44
|
var TOOL_NAME_MAX = 64; // string-length cap, not bytes
|
|
@@ -642,8 +644,14 @@ function _validateValueAgainstSchema(value, schema, path) {
|
|
|
642
644
|
// cost scales with the number of code units the engine scans, so 4096
|
|
643
645
|
// chars is the correct ReDoS bound regardless of UTF-8 byte size.
|
|
644
646
|
if (value.length > 4096) return path + ": value exceeds 4096-char cap before regex test"; // ReDoS char cap (not bytes)
|
|
647
|
+
// The input-length cap above does NOT bound catastrophic backtracking
|
|
648
|
+
// (a `(a+)+$` pattern blows up at ~40 input chars). Screen the tool
|
|
649
|
+
// author's pattern through b.guardRegex so a ReDoS-shaped schema pattern
|
|
650
|
+
// can't pin a CPU when matched against request input.
|
|
651
|
+
try { guardRegex().sanitize(schema.pattern, { profile: "strict" }); }
|
|
652
|
+
catch (_ge) { return path + ": schema pattern rejected as unsafe (ReDoS shape)"; }
|
|
645
653
|
try {
|
|
646
|
-
var pat = new RegExp(schema.pattern); // allow:dynamic-regex — schema.pattern
|
|
654
|
+
var pat = new RegExp(schema.pattern); // allow:dynamic-regex — schema.pattern is ReDoS-screened via guardRegex.sanitize (strict) above + input length-capped
|
|
647
655
|
if (!pat.test(value)) return path + ": does not match pattern";
|
|
648
656
|
}
|
|
649
657
|
catch (_e) { return path + ": invalid pattern in schema"; }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.15.38",
|
|
4
|
+
"date": "2026-06-27",
|
|
5
|
+
"headline": "Regex patterns supplied in feature-flag targeting rules and MCP tool input schemas are now screened for catastrophic-backtracking (ReDoS) shapes before compilation, so a pattern matched against request data can't pin a CPU",
|
|
6
|
+
"summary": "Two places compiled a caller-supplied regex pattern and `.test()`'d it against request-controlled input with only a length bound as the stated defense: a feature-flag targeting condition (`op: \"regex\"`) matched against runtime attribute values, and an MCP tool's input-schema `pattern` matched against tool-call arguments. A length bound is not a ReDoS defense — a catastrophic-backtracking pattern such as `(a+)+$` is six characters and pins a CPU on a crafted input. Both patterns now pass through `b.guardRegex` (strict profile) before compilation, which refuses nested-quantifier, alternation-with-quantifier, and quantifier-inside-lookaround shapes. A ReDoS-shaped flag pattern is refused when the rules are validated; a ReDoS-shaped MCP schema pattern fails tool-input validation. Patterns built from the framework's own static tables, operator-owned JSON Schema patterns, the Sieve glob translator (which cannot express nested quantifiers), and the I-Regexp translator (linear by dialect) are unchanged.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Security",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "Feature-flag regex targeting conditions are screened for ReDoS before compilation",
|
|
13
|
+
"body": "A flag targeting rule with `op: \"regex\"` compiled the operator-supplied pattern and `.test()`'d it against runtime attribute values, guarded only by a 200-character length cap. Length does not bound catastrophic backtracking, so a pattern like `(a+)+$` combined with an attacker-controlled attribute value could pin a CPU during flag evaluation. The pattern is now screened through b.guardRegex (strict) when the rules are validated, and a catastrophic-backtracking shape is refused with a clear error."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "MCP tool input-schema patterns are screened for ReDoS before matching request input",
|
|
17
|
+
"body": "b.mcp.validateToolInput compiled a tool author's input-schema `pattern` and matched it against tool-call argument values; the 4096-character input cap does not bound backtracking (a `(a+)+$` pattern blows up at roughly forty input characters). The schema pattern is now screened through b.guardRegex (strict) before compilation, so a ReDoS-shaped pattern in a registered tool's schema fails input validation instead of letting hostile arguments hang the validator."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"title": "b.guardRegex now catches wrapped nested-quantifier patterns",
|
|
21
|
+
"body": "The nested-quantifier detector matched a quantified group whose body contained a quantifier, but its inner match was paren-blind, so wrapping the inner quantifier in an extra group — `((a)+)+`, `(([a-z]+)*)*`, `((a+))+` — slipped past it while remaining catastrophic. A structural scan now tracks group nesting and refuses an unbounded-quantified group whose body itself contains an unbounded quantifier at any depth, so the wrapped forms are rejected alongside the bare `(a+)+`. Bounded repeats (`{n}`, `{n,m}`) are unaffected. This strengthens every b.guardRegex caller, including the flag-targeting and MCP screening above."
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -240,6 +240,30 @@ function run() {
|
|
|
240
240
|
},
|
|
241
241
|
/invalid regex/);
|
|
242
242
|
|
|
243
|
+
// ReDoS-shaped pattern (nested quantifier) refused. The compiled regex is
|
|
244
|
+
// .test()'d against runtime (request) attribute values, so a catastrophic-
|
|
245
|
+
// backtracking pattern is a DoS vector; the 200-char length bound is NOT a
|
|
246
|
+
// ReDoS defense (`(a+)+$` is 6 chars). The pattern is screened through
|
|
247
|
+
// b.guardRegex before compilation.
|
|
248
|
+
rejects("targeting: ReDoS-shaped regex (nested quantifier) refused",
|
|
249
|
+
function () {
|
|
250
|
+
t.validateRules([{ variant: "v", conditions: [{ attribute: "x", op: "regex", value: "(a+)+$" }] }]);
|
|
251
|
+
},
|
|
252
|
+
/unsafe|ReDoS|backtrack|quantifier/i);
|
|
253
|
+
// Wrapping the inner quantified atom in an extra group — `((a)+)+$` — is the
|
|
254
|
+
// same catastrophic-backtracking class; a paren-blind `[^()]*` detector misses
|
|
255
|
+
// it. The structural screener must still refuse these nested-group forms.
|
|
256
|
+
rejects("targeting: wrapped nested-quantifier regex refused",
|
|
257
|
+
function () {
|
|
258
|
+
t.validateRules([{ variant: "v", conditions: [{ attribute: "x", op: "regex", value: "((a)+)+$" }] }]);
|
|
259
|
+
},
|
|
260
|
+
/unsafe|ReDoS|backtrack|quantifier/i);
|
|
261
|
+
rejects("targeting: deeper wrapped nested-quantifier regex refused",
|
|
262
|
+
function () {
|
|
263
|
+
t.validateRules([{ variant: "v", conditions: [{ attribute: "x", op: "regex", value: "(([a-z]+)*)*$" }] }]);
|
|
264
|
+
},
|
|
265
|
+
/unsafe|ReDoS|backtrack|quantifier/i);
|
|
266
|
+
|
|
243
267
|
// nested attribute path
|
|
244
268
|
var pathRules = t.validateRules([
|
|
245
269
|
{ variant: "v1", conditions: [{ attribute: "user.role", op: "eq", value: "admin" }] },
|
|
@@ -122,6 +122,21 @@ async function run() {
|
|
|
122
122
|
} catch (e) { threw = /tool-input-invalid/.test(e.code); }
|
|
123
123
|
check("mcp.validateToolInput: schema mismatch refused", threw);
|
|
124
124
|
|
|
125
|
+
// A tool-author schema pattern with a catastrophic-backtracking (ReDoS) shape
|
|
126
|
+
// is compiled and .test()'d against request input — the 4096-char input cap
|
|
127
|
+
// does not bound backtracking. The pattern is screened through b.guardRegex
|
|
128
|
+
// and a ReDoS shape is refused. (`"aaa"` matches `(a+)+$` instantly, so the
|
|
129
|
+
// pre-fix path does not hang.)
|
|
130
|
+
threw = false; var redosMsg = "";
|
|
131
|
+
try {
|
|
132
|
+
b.mcp.validateToolInput("t", { x: "aaa" }, {
|
|
133
|
+
type: "object",
|
|
134
|
+
properties: { x: { type: "string", pattern: "(a+)+$" } },
|
|
135
|
+
required: ["x"],
|
|
136
|
+
});
|
|
137
|
+
} catch (e) { threw = true; redosMsg = e.message || ""; }
|
|
138
|
+
check("mcp.validateToolInput: ReDoS-shaped schema pattern refused", threw && /unsafe|ReDoS/i.test(redosMsg));
|
|
139
|
+
|
|
125
140
|
// ---- v0.8.77: assertProtocolVersion / sampling / elicitation ----
|
|
126
141
|
threw = false;
|
|
127
142
|
try { b.mcp.assertProtocolVersion({ headers: {} }); }
|
package/package.json
CHANGED