@blamejs/core 0.18.50 → 0.18.51
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 +54 -0
- package/NOTICE +1 -1
- package/README.md +1 -1
- package/lib/ai-input.js +25 -3
- package/lib/db-query.js +32 -4
- package/lib/gate-contract.js +11 -2
- package/lib/guard-filename.js +6 -2
- package/lib/guard-jwt.js +2 -2
- package/lib/json-schema.js +130 -13
- package/lib/mail-bimi.js +279 -6
- package/lib/markup-tokenizer.js +88 -0
- package/lib/vendor/MANIFEST.json +12 -12
- package/lib/vendor/blamejs-pki.cjs +835 -41
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,60 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.18.x
|
|
10
10
|
|
|
11
|
+
- v0.18.51 (2026-08-22) — **Five parsers could be made to spend their time on the thing they were parsing.** Three prompt-injection detectors, the BIMI logo parser and the raw-write data-residency gate each ran a pattern whose cost grew with the square or the cube of its input while an ordinary input of the same length cost a millisecond. A 64 KiB prompt took up to 4.5 seconds to classify; a 32 KiB logo took 409 milliseconds to parse; a 4 KB raw UPDATE took 7 seconds to get a verdict from a gate that accepts statements twenty-four times longer. All five now cost what ordinary input of that length costs, and each was checked against a corpus to confirm it still decides what it decided before. Separately, a verified VMC logo was reported as absent when its SVG began with an XML declaration and a DOCTYPE. **Changed:** *`b.guardFilename` no longer repairs a null byte, and five options that accept one value now say so* — 0.18.47 accepted `nullBytePolicy: "strip"` on `b.guardFilename` and removed the null byte from the name. 0.18.48 stopped accepting it. That removal was correct and was not written down, which is the part being fixed here.
|
|
12
|
+
|
|
13
|
+
It was correct because a null byte in a filename is a truncation attack, not a typo: the name the check reads and the name the operating system acts on differ at the byte, so repairing it produces a name nobody validated. The guard refuses with `filename.null-byte` and the message has said `null-byte truncation is never sanitizable` throughout. There is no replacement policy value, because there is no safe repair. A caller that was asking for the strip should refuse the input instead, or rename before validating.
|
|
14
|
+
|
|
15
|
+
Separately, five options across three guards accept exactly one value: `nullBytePolicy` and `traversalPolicy` on `b.guardFilename`, `algNonePolicy` and `kidTraversalPolicy` on `b.guardJwt`, and `svgzPolicy` on `b.guardSvg`. Each is a check where any disposition other than refusing is a hole, so the single value is a deliberate lock. They were refusing a wrong value with `must be one of reject`, which reads as a vocabulary that lost its other members and sends the reader looking for them. The refusal now says the option is fixed and not configurable. Passing the legal value still resolves, so nothing that spells it out breaks; what is enforced is unchanged. · *Vendored `@blamejs/pki` refreshed to 0.5.28* — The bundle behind `b.mtlsCa` and `b.auth.passkey`, moved on from 0.5.25 across three releases.
|
|
16
|
+
|
|
17
|
+
The one that matters is in 0.5.28: a `CryptoKey`'s `algorithm` is now immutable once the key exists. The engine reads `key.algorithm.hash` at sign time, and while that object could still be replaced — including from a microtask during the signing await — the hash checked against a JWS `alg` header could be rewritten between the check and the signature, producing a JWS whose signature does not match the algorithm it advertises.
|
|
18
|
+
|
|
19
|
+
0.5.27 makes an unrecognized option to `pki.trust.anchor` or the `pki.acme.client` constructor a named error rather than a silent default, so a misspelled security-relevant setting can no longer read as absent. 0.5.26 adds the RFC 9483 revocation and support-message exchanges to the CMP session, with each response bound to the request that asked for it. 0.5.28 also stops the toolkit printing Node experimental-feature warnings when it loads.
|
|
20
|
+
|
|
21
|
+
Nothing in this framework's surface changes; `lib/vendor/MANIFEST.json` carries the new version and hash. **Fixed:** *A VMC logo was not extracted when the SVG began with a declaration and a DOCTYPE* — `b.mail.bimi.fetchAndVerifyMark` documents that `mark` carries the SVG whenever the certificate's RFC 3709 logotype extension is present. It decided whether an extension payload was an SVG by looking for `<svg` in the first 64 characters, and a conformant SVG may put an XML declaration, a DOCTYPE and a comment ahead of its root element. In a document with the usual SVG 1.1 DOCTYPE the root sits 154 characters in, so the logo was signed into the certificate, served, verified, and then reported as absent: `mark.svg` came back null.
|
|
22
|
+
|
|
23
|
+
The scanner now steps over what XML actually permits ahead of a root element — whitespace and a byte-order mark, the declaration, processing instructions, comments, and a DOCTYPE including an internal subset — and then asks whether `<svg` begins there. A prologue has no length limit, so no fixed window is the right answer for this; whichever number were chosen, a legal document could put its root past it.
|
|
24
|
+
|
|
25
|
+
The check is also stricter than the one it replaces in two ways. A document that merely mentions `<svg` inside a comment is no longer taken for a logo, and a root whose name merely begins with those letters — `<svgfoo`, or `<svg.foo`, since `.` is a legal name character — is no longer taken for `<svg`. A payload with no root element at all still yields null however long it is.
|
|
26
|
+
|
|
27
|
+
The alternative pattern that stood beside the original search looked like it was meant for exactly this case, but it required `<svg` inside the same 64 characters the search already covered, so it could never have supplied an answer the search had not. It is gone. **Security:** *`b.ai.input.classify` could be made to spend seconds on a single 64 KiB prompt* — Three of the injection detectors ran a pattern whose leading run could begin at every offset inside a stretch of characters it accepts. At each offset the engine consumed the whole run before the part that had to follow it failed to arrive, so one call cost the length of the input squared.
|
|
28
|
+
|
|
29
|
+
`classify` accepts 64 KiB by default. Measured at that size, against 1 millisecond for ordinary text of exactly the same length:
|
|
30
|
+
|
|
31
|
+
- The base64-marker detector looks for a long base64-looking run followed by a word like `means` or `decodes to`. `/` is inside the run's character class, so text such as `a/a/a/…` feeds it: **4,536ms**.
|
|
32
|
+
- The role-tag detector matched `<`, optional whitespace, an optional `/`, optional whitespace. Two whitespace runs either side of an optional character means every way of dividing a run of spaces between them is retried: **1,642ms** on `<` followed by spaces.
|
|
33
|
+
- The event-handler detector matched `on` followed by word characters. `o` and `n` are themselves word characters, so `onononon…` begins a match at every other offset and each one walks to the end of the input: **870ms**.
|
|
34
|
+
|
|
35
|
+
All three cost the shape rather than the size, and an operator who raises `maxBytes` raises the cost with the square of it. This is the primitive whose entire premise is that its input is hostile, which is what makes it worth naming plainly: the scan meant to catch an attack was one.
|
|
36
|
+
|
|
37
|
+
Each run may now only begin where the preceding character is not one the run itself accepts, and the optional `/` in the role tag carries the whitespace that follows it so there is only one way to divide any input. All three now classify a hostile-shaped 64 KiB prompt in 1.3 to 1.5 milliseconds, which is what benign input of that length costs.
|
|
38
|
+
|
|
39
|
+
The role-tag detector accepts exactly the inputs it accepted before, confirmed across 5,214 combinations of spacing, slash placement and tag name. The base64 detector was checked against fifteen cases covering padding, the three trailing keywords, and blobs at the start of a string, after a newline, after a letter, after a slash and after an `=`.
|
|
40
|
+
|
|
41
|
+
One deliberate narrowing comes with the event-handler detector. It no longer matches an `on` that begins in the middle of a word, so `fooonclick="fetch(1)"` is no longer reported. An event-handler attribute begins a word, so that was a false report rather than coverage; `onclick=` still matches at the start of the input and after any character that is not a word character, which covers every place an attribute can actually appear, and every `<script` form is untouched. · *Compiling a JSON Schema could be made to hang before any instance was validated* — 0.18.50 added a pre-screen that walks every subschema position so a `pattern` is checked before it can run. The walk was bounded only by depth, and a depth bound limits how far one path runs while saying nothing about how many paths there are. A schema graph that reaches one object through a branching position was therefore walked once per path rather than once per object.
|
|
42
|
+
|
|
43
|
+
Building a schema out of reused JavaScript objects produces exactly that shape and needs no `$ref`, so nothing about the source reads as unusual: `anyOf: [shared, shared]` at each of twenty levels is twenty-one objects and a million paths. Measured, twenty-one objects took 2.6 seconds and each added level doubled it; the depth cap allowed 256. A schema that referred to itself never finished at all.
|
|
44
|
+
|
|
45
|
+
The same shape was present, and older, in the walk that indexes subschemas for `$ref` resolution, where it was the larger cost. That one cannot skip a repeated object, because it indexes each position by pointer and a shared object legitimately has one entry per pointer that reaches it. So the screen now visits each object once, and the index carries a ceiling on how many positions it will register, refusing beyond it with `json-schema/schema-too-large`. The ceiling is far above any authored schema: a thousand-endpoint OpenAPI document is tens of thousands of positions.
|
|
46
|
+
|
|
47
|
+
This is worth naming plainly. `SECURITY.md` promises that a content screen costs the length of its input and never a function of its shape, and names this call site as one of the places that promise is kept. The screen added to keep it was itself a function of shape.
|
|
48
|
+
|
|
49
|
+
Reported privately from downstream vendoring during a routine dependency refresh. · *A raw write to a data-residency table could hang the process in its own residency gate* — `b.db.runSql`, `execRaw` and `b.db.prepare(sql).run()` bypass the structured builder, so a residency gate parses the statement itself and refuses anything it cannot read. Both of its body patterns ended `\s*;?\s*$`, and the UPDATE one put a lazy run in front of that. The lazy run and both whitespace runs can all absorb the same trailing spaces, with an optional semicolon between two of them, so every division of a trailing whitespace run is a distinct path and the lazy run retries all of them at every length it takes.
|
|
50
|
+
|
|
51
|
+
The growth is cubic: 117 milliseconds on a 1 KB statement, 6.8 seconds on 4 KB, roughly eight times the cost for each doubling. Driven through `b.db.runSql` against a residency table, a 4 KB `UPDATE` carrying trailing whitespace took 7,071 milliseconds before the gate reached a verdict.
|
|
52
|
+
|
|
53
|
+
The gate caps its parse input at 100,000 characters and the comment above that cap says it exists to bound these scans. It does the opposite: it is the ceiling that lets a single statement reach a cost measured in hours. A statement long enough to matter is not exotic either, since the body is whatever SQL the application wrote.
|
|
54
|
+
|
|
55
|
+
The trailing semicolon and the whitespace around it are now walked off the end before either pattern runs, which leaves one greedy run with nothing after it to divide. The same parse costs 0.04 milliseconds at the full 100,000-character cap. Captures are unchanged across a corpus of qualified names, quoted identifiers, embedded semicolons inside string literals, repeated terminators and mixed casing.
|
|
56
|
+
|
|
57
|
+
One shape now parses differently: `UPDATE t SET` followed only by whitespace, which has no SET body at all. It previously parsed with a whitespace body and is now refused as unparseable, which is the direction this gate already documents for anything it cannot read.
|
|
58
|
+
|
|
59
|
+
This affects deployments that have declared per-row or per-column residency; a table without a residency declaration never enters the gate. · *A BIMI logo could cost 409 milliseconds to parse* — The Tiny-PS attribute parser reads names matching `[A-Za-z_:][A-Za-z0-9:._-]*`, and the same restart applied: inside a long run of name characters the name could begin at every offset, consuming the run before `\s*=\s*` failed.
|
|
60
|
+
|
|
61
|
+
A logo is fetched from a URL published in the sending domain's own DNS record, so those bytes are chosen by whoever controls the domain. `validateTinyPsSvg` refuses anything over 32 KiB before it parses, and at that size a hostile-shaped document took 409ms against 0.6ms for a well-formed SVG of the same size.
|
|
62
|
+
|
|
63
|
+
An attribute name may now only begin after a character that is not a name character, which is what XML requires in any case. Checked against a fifteen-case corpus covering namespaced names, dotted and dashed names, bare-token values, single quotes, empty values, extra whitespace around `=`, and attributes with no space between them: the same attributes are read, with the same values.
|
|
64
|
+
|
|
11
65
|
- 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
66
|
|
|
13
67
|
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.
|
package/NOTICE
CHANGED
|
@@ -68,7 +68,7 @@ Used for: FIPS 203 ML-KEM (ml_kem_512 / ml_kem_768 / ml_kem_1024),
|
|
|
68
68
|
reference implementation.
|
|
69
69
|
--------------------------------------------------------------------------------
|
|
70
70
|
Component: @blamejs/pki
|
|
71
|
-
Version: 0.5.
|
|
71
|
+
Version: 0.5.28
|
|
72
72
|
Source: https://github.com/blamejs/pki
|
|
73
73
|
License: Apache-2.0
|
|
74
74
|
Copyright: Copyright (c) blamejs contributors
|
package/README.md
CHANGED
|
@@ -323,7 +323,7 @@ All runtime dependencies are committed to the repo — no transitive npm install
|
|
|
323
323
|
| [`@noble/hashes`](https://github.com/paulmillr/noble-hashes) | 2.3.0 | [Paul Miller](https://github.com/paulmillr) | Browser (ESM) build only — SHAKE256 / SHA-3 / SHA-2 / HMAC / HKDF for the client half of a hybrid exchange. The server side reaches all of these through `node:crypto`, so there is no server bundle |
|
|
324
324
|
| [`@noble/curves`](https://github.com/paulmillr/noble-curves) | 2.3.0 (bundles @noble/hashes 2.3.0) | [Paul Miller](https://github.com/paulmillr) | RFC 9497 Oblivious Pseudo-Random Function (OPRF / VOPRF / POPRF) over ristretto255 / P-256 / P-384 / P-521, behind `b.crypto.oprf` |
|
|
325
325
|
| [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum) | 0.7.0 (bundles @noble/hashes, @noble/curves, @noble/ciphers 2.3.0) | [Paul Miller](https://github.com/paulmillr) | Pure-JS FIPS 203 ML-KEM (`ml_kem_512` / `ml_kem_768` / `ml_kem_1024`), FIPS 204 ML-DSA (`ml_dsa_44/65/87`), FIPS 205 SLH-DSA (`slh_dsa_*`). First-class on both server-side and client-side via `b.pqcSoftware` — security-first defaults pin to the highest cat-5 levels (ML-KEM-1024, ML-DSA-87, SLH-DSA-SHAKE-256f); interoperable with Node's built-in WebCrypto ML-KEM that `b.crypto.encrypt` / `b.middleware.apiEncrypt` use. A browser (ESM) build ships beside it carrying the KEM suites only — a client half encapsulates and does not sign |
|
|
326
|
-
| [`@blamejs/pki`](https://github.com/blamejs/pki) | 0.5.
|
|
326
|
+
| [`@blamejs/pki`](https://github.com/blamejs/pki) | 0.5.28 | [blamejs](https://github.com/blamejs) | Zero-dependency pure-JS X.509 / CRL / PKCS#12 / CSR / CMS toolkit backing `b.mtlsCa` — ML-DSA-87 (FIPS 204) post-quantum + ECDSA-P384 cert signing, PBMAC1 PKCS#12 packaging, chain validation (no openssl CLI) — and the WebAuthn attestation / assertion verification behind `b.auth.passkey` |
|
|
327
327
|
| [`SecLists` 10k-most-common.txt](https://github.com/danielmiessler/SecLists/blob/master/Passwords/Common-Credentials/10k-most-common.txt) | master snapshot | [Daniel Miessler / SecLists contributors](https://github.com/danielmiessler/SecLists) (CC-BY-3.0) | Top-10000 common-password dictionary read by `b.auth.password.policy()` for the NIST 800-63B §5.1.1.2 "previously breached" check |
|
|
328
328
|
| [`prismjs`](https://prismjs.com/) | 1.30.0 | [Lea Verou + contributors](https://github.com/PrismJS/prism) | Syntax highlighting in the example wiki's code blocks (browser-side) |
|
|
329
329
|
|
package/lib/ai-input.js
CHANGED
|
@@ -44,22 +44,44 @@ var PATTERNS = [
|
|
|
44
44
|
/\byou\s+(?:are|will\s+be|must\s+be)\s+(?:now|from\s+now\s+on)?\s*(?:a|an)\s+\w{2,40}/i },
|
|
45
45
|
{ id: "jailbreak-persona", severity: 3, re:
|
|
46
46
|
/\b(?:DAN|do\s+anything\s+now|developer\s+mode|sudo\s+mode|jailbroken|unfiltered|uncensored|unrestricted)\b/i },
|
|
47
|
+
// The `/` carries the whitespace that follows it. Written as `\s*\/?\s*` the
|
|
48
|
+
// two runs sit either side of an OPTIONAL character, so on `<` and a long run
|
|
49
|
+
// of spaces the engine retries every way of dividing that run between them:
|
|
50
|
+
// 1,642ms on a 64 KiB input against 0.02ms for benign text of the same
|
|
51
|
+
// length. Moving the `/` and its trailing run into one optional group leaves
|
|
52
|
+
// a single way to divide any input and accepts exactly the same tags.
|
|
47
53
|
{ id: "role-reset-marker", severity: 3, re:
|
|
48
|
-
/<\s
|
|
54
|
+
/<\s*(?:\/\s*)?(?:system|user|assistant|sys|im_(?:start|end)|\|im_(?:start|end)\|)\s*>/i },
|
|
49
55
|
{ id: "openai-system-tag", severity: 3, re:
|
|
50
56
|
/\b(?:<\|im_start\|>|<\|im_end\|>|\[INST\]|\[\/INST\]|<\|user\|>|<\|assistant\|>|<\|system\|>)\b/ },
|
|
51
57
|
{ id: "tool-call-injection", severity: 3, re:
|
|
52
58
|
/\b(?:tool|function|action)\s*[:=]\s*["']?(?:exec|eval|read_file|exfil|leak|extract)\b/i },
|
|
53
59
|
{ id: "exfil-callback", severity: 3, re:
|
|
54
60
|
/\b(?:send|post|fetch|exfil|leak|paste|forward)\b[\s\S]{0,40}(?:secret|key|token|password|cred|env|\.ssh|private)/i },
|
|
61
|
+
// The lookbehind is what keeps this linear, and it is also what the detector
|
|
62
|
+
// meant: a base64 TOKEN, not any 40-character window inside a longer run.
|
|
63
|
+
// Without it the run can start at every offset, and at each one the engine
|
|
64
|
+
// consumes the whole run before `\s+` fails — O(n) starts times O(n) work.
|
|
65
|
+
// `/` is inside the class, so `a/` repeated feeds it: 4,536ms on a 64 KiB
|
|
66
|
+
// prompt, against 1ms for benign input of the same length. The lookbehind
|
|
67
|
+
// fails in constant time at every offset except the one after a delimiter.
|
|
68
|
+
// The exclusion is exactly the run's own character class and nothing more.
|
|
69
|
+
// Adding `=` to it also looked reasonable and silently dropped a real shape,
|
|
70
|
+
// `token=<blob> means ...`, where the blob begins right after an `=`.
|
|
55
71
|
{ id: "base64-marker-around-instructions", severity: 2, re:
|
|
56
|
-
/(
|
|
72
|
+
/(?<![A-Za-z0-9+/])[A-Za-z0-9+/]{40,}={0,2}\s+(?:means|decodes?\s+to|=)/i }, // regex repetition floor, not bytes
|
|
57
73
|
{ id: "rot13-shape", severity: 2, re:
|
|
58
74
|
/\b(?:rot13|rotcipher|cipher|caesar)\s*[:=]\s*[a-zA-Z]{20,}/i },
|
|
59
75
|
{ id: "markdown-injection", severity: 2, re:
|
|
60
76
|
/!\[[^\]]{0,40}\]\((?:javascript:|data:|file:)/i },
|
|
77
|
+
// `\w` covers `o` and `n`, so without the boundary an input of `on` repeated
|
|
78
|
+
// starts a match at every other offset and each one walks `\w+` to the end of
|
|
79
|
+
// the subject before the `=` fails: 870ms on 64 KiB against 0.03ms for benign
|
|
80
|
+
// text of the same length. The boundary is also what the detector meant — an
|
|
81
|
+
// event-handler attribute begins a word, so `foonclick="fetch()"` was a match
|
|
82
|
+
// it should never have had.
|
|
61
83
|
{ id: "html-script-shape", severity: 2, re:
|
|
62
|
-
/<script[\s>]
|
|
84
|
+
/<script[\s>]|\bon\w+\s*=\s*["'][^"']*\b(?:fetch|xhr|eval|location)\b/i },
|
|
63
85
|
{ id: "stop-helping", severity: 2, re:
|
|
64
86
|
/\b(?:stop|cease|quit)\s+(?:helping|assisting|following)\b/i },
|
|
65
87
|
{ id: "now-instead", severity: 2, re:
|
package/lib/db-query.js
CHANGED
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
*/
|
|
45
45
|
var { Readable } = require("node:stream");
|
|
46
46
|
var C = require("./constants");
|
|
47
|
+
var codepointClass = require("./codepoint-class");
|
|
47
48
|
var cryptoField = require("./crypto-field");
|
|
48
49
|
var { generateToken } = require("./crypto");
|
|
49
50
|
var safeJson = require("./safe-json");
|
|
@@ -1397,8 +1398,34 @@ function _validateField(field) {
|
|
|
1397
1398
|
// and run it through the SAME gate; a write to a residency table the framework
|
|
1398
1399
|
// cannot parse fails CLOSED (refused) - a raw write never skips the check.
|
|
1399
1400
|
var _RAW_WRITE_KEYWORD_RE = /^\s*(?:INSERT|REPLACE|UPDATE)\b/i;
|
|
1400
|
-
|
|
1401
|
-
|
|
1401
|
+
// Both bodies used to end `\s*;?\s*$`, which put two whitespace runs either
|
|
1402
|
+
// side of an optional semicolon and, for UPDATE, a lazy body run in front of
|
|
1403
|
+
// them. All three could absorb the same trailing spaces, so every division of
|
|
1404
|
+
// that run was a distinct path and the lazy run retried all of them at every
|
|
1405
|
+
// length it took: cubic, 117ms on a 1 KB statement and 6.8 seconds on 4 KB.
|
|
1406
|
+
// The 100,000-character ceiling below is what the parse is allowed, so it set
|
|
1407
|
+
// the cost rather than bounding it.
|
|
1408
|
+
//
|
|
1409
|
+
// The terminator is now walked off the end by _stripStatementTail before either
|
|
1410
|
+
// pattern runs, which leaves one greedy run with nothing after it to divide.
|
|
1411
|
+
var _RAW_INSERT_RE = /^\s*(?:INSERT|REPLACE)\s+(?:OR\s+[A-Za-z]+\s+)?INTO\s+(?:[\x22\x27\x60]?[A-Za-z_]\w*[\x22\x27\x60]?\s*\.\s*){0,3}[\x22\x27\x60]?([A-Za-z_]\w*)[\x22\x27\x60]?\s*\(([^)]+)\)\s*VALUES\s*\(([\s\S]+)\)$/i;
|
|
1412
|
+
var _RAW_UPDATE_RE = /^\s*UPDATE\s+(?:[\x22\x27\x60]?[A-Za-z_]\w*[\x22\x27\x60]?\s*\.\s*){0,3}[\x22\x27\x60]?([A-Za-z_]\w*)[\x22\x27\x60]?\s+SET\s+([\s\S]+)$/i;
|
|
1413
|
+
|
|
1414
|
+
// The trailing `;` and the whitespace around it, removed by walking backwards
|
|
1415
|
+
// so no pattern has to express it. WHITESPACE_RANGES is what a regular
|
|
1416
|
+
// expression means by `\s`, so the two agree on U+00A0 and U+3000 as well as
|
|
1417
|
+
// the ASCII five; a hand-listed "space and tab" would leave a terminator the
|
|
1418
|
+
// old pattern removed.
|
|
1419
|
+
function _stripStatementTail(s) {
|
|
1420
|
+
var ws = codepointClass.WHITESPACE_RANGES;
|
|
1421
|
+
var end = s.length;
|
|
1422
|
+
while (end > 0 && codepointClass.inRanges(s.charCodeAt(end - 1), ws)) end -= 1;
|
|
1423
|
+
if (end > 0 && s.charCodeAt(end - 1) === 0x3B /* ; */) {
|
|
1424
|
+
end -= 1;
|
|
1425
|
+
while (end > 0 && codepointClass.inRanges(s.charCodeAt(end - 1), ws)) end -= 1;
|
|
1426
|
+
}
|
|
1427
|
+
return end === s.length ? s : s.slice(0, end);
|
|
1428
|
+
}
|
|
1402
1429
|
var _RAW_TABLE_RE = /^\s*(?:INSERT|REPLACE)\s+(?:OR\s+[A-Za-z]+\s+)?INTO\s+(?:[\x22\x27\x60]?[A-Za-z_]\w*[\x22\x27\x60]?\s*\.\s*){0,3}[\x22\x27\x60]?([A-Za-z_]\w*)[\x22\x27\x60]?|^\s*UPDATE\s+(?:[\x22\x27\x60]?[A-Za-z_]\w*[\x22\x27\x60]?\s*\.\s*){0,3}[\x22\x27\x60]?([A-Za-z_]\w*)[\x22\x27\x60]?/i;
|
|
1403
1430
|
|
|
1404
1431
|
function _unquoteIdent(s) {
|
|
@@ -1588,8 +1615,9 @@ function _assertRawWriteResidency(sql, boundParams) {
|
|
|
1588
1615
|
norm.length + " chars) - use b.db.from(\"" + table + "\") so residency is validated", true);
|
|
1589
1616
|
}
|
|
1590
1617
|
|
|
1591
|
-
var
|
|
1592
|
-
var
|
|
1618
|
+
var body = _stripStatementTail(norm);
|
|
1619
|
+
var mi = _RAW_INSERT_RE.exec(body); // allow:regex-no-length-cap — input length-capped above
|
|
1620
|
+
var mu = mi ? null : _RAW_UPDATE_RE.exec(body); // allow:regex-no-length-cap — input length-capped above
|
|
1593
1621
|
if (!mi && !mu) {
|
|
1594
1622
|
throw new DbQueryError("db-query/row-residency-raw-unparseable",
|
|
1595
1623
|
"raw write to residency table '" + table + "' cannot be parsed to validate its " +
|
package/lib/gate-contract.js
CHANGED
|
@@ -2745,9 +2745,18 @@ function resolveProfileAndPosture(opts, cfg) {
|
|
|
2745
2745
|
var v = resolved[k];
|
|
2746
2746
|
if (v === undefined || !Array.isArray(allowed)) return;
|
|
2747
2747
|
if (typeof v !== "string" || allowed.indexOf(v) === -1) {
|
|
2748
|
+
// A vocabulary of ONE is not a vocabulary, and saying "must be one of
|
|
2749
|
+
// reject" invites the reader to go looking for the members that are
|
|
2750
|
+
// missing. These are the checks where every disposition except refusing
|
|
2751
|
+
// is a hole — a traversal sequence, a null byte in a filename, `alg:
|
|
2752
|
+
// none` — so the single value is a deliberate lock rather than a
|
|
2753
|
+
// vocabulary that lost its siblings. The message says which.
|
|
2748
2754
|
throw ErrorClass.factory(prefix + ".bad-opt",
|
|
2749
|
-
|
|
2750
|
-
|
|
2755
|
+
allowed.length === 1
|
|
2756
|
+
? prefix + ": " + k + " is fixed at " + allowed[0] +
|
|
2757
|
+
" and is not configurable; got " + JSON.stringify(v)
|
|
2758
|
+
: prefix + ": " + k + " must be one of " + allowed.join(", ") +
|
|
2759
|
+
"; got " + JSON.stringify(v));
|
|
2751
2760
|
}
|
|
2752
2761
|
});
|
|
2753
2762
|
}
|
package/lib/guard-filename.js
CHANGED
|
@@ -703,10 +703,14 @@ function _sanitize(input, opts) {
|
|
|
703
703
|
* compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
|
|
704
704
|
* bidiPolicy: "reject"|"strip"|"allow",
|
|
705
705
|
* controlPolicy: "reject"|"strip"|"allow",
|
|
706
|
-
* nullBytePolicy: "reject", //
|
|
706
|
+
* nullBytePolicy: "reject", // fixed; not configurable
|
|
707
|
+
* // null-byte truncation
|
|
708
|
+
* // is never sanitizable
|
|
707
709
|
* zeroWidthPolicy: "reject"|"strip"|"allow",
|
|
708
710
|
* homoglyphPolicy: "reject"|"audit"|"allow",
|
|
709
|
-
* traversalPolicy: "reject", //
|
|
711
|
+
* traversalPolicy: "reject", // fixed; not configurable
|
|
712
|
+
* // no disposition other
|
|
713
|
+
* // than refusing is safe
|
|
710
714
|
* reservedCharPolicy: "reject"|"strip"|"allow",
|
|
711
715
|
* reservedNamePolicy: "reject"|"audit"|"allow",
|
|
712
716
|
* adsPolicy: "reject"|"allow", // reject here; "allow"
|
package/lib/guard-jwt.js
CHANGED
|
@@ -441,9 +441,9 @@ function _detectIssues(input, opts) {
|
|
|
441
441
|
* allowedAlgs: string[],
|
|
442
442
|
* requiredClaims: string[],
|
|
443
443
|
* knownCrit: string[],
|
|
444
|
-
* algNonePolicy: "reject", // alg=none is always critical
|
|
444
|
+
* algNonePolicy: "reject", // fixed; alg=none is always critical
|
|
445
445
|
* algAllowlistPolicy: "reject"|"audit"|"allow",
|
|
446
|
-
* kidTraversalPolicy: "reject", // kid traversal is always critical
|
|
446
|
+
* kidTraversalPolicy: "reject", // fixed; kid traversal is always critical
|
|
447
447
|
* typConfusionPolicy: "reject"|"audit"|"allow",
|
|
448
448
|
* expSanityPolicy: "reject"|"audit"|"allow",
|
|
449
449
|
* nbfSanityPolicy: "reject"|"audit"|"allow",
|
package/lib/json-schema.js
CHANGED
|
@@ -141,10 +141,52 @@ function _unescapePointerToken(t) { return t.replace(/~1/g, "/").replace(/~0/g,
|
|
|
141
141
|
|
|
142
142
|
// --- registry: indexes every subschema by canonical URI + anchors ---
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
// A ceiling on how many subschema POSITIONS one registry will index, across
|
|
145
|
+
// every document added to it.
|
|
146
|
+
//
|
|
147
|
+
// This is the total-work half of the bound, and it is a different guarantee
|
|
148
|
+
// from a depth ceiling: depth limits how far one path runs, and says nothing
|
|
149
|
+
// about how many paths there are. The walk below cannot use identity tracking
|
|
150
|
+
// instead, because it indexes each node by `base#pointer` and a shared object
|
|
151
|
+
// legitimately has one entry per pointer that reaches it — skipping the second
|
|
152
|
+
// visit would drop a `$ref` target rather than save work. So what gets bounded
|
|
153
|
+
// is the number of entries, which is the thing that actually grows.
|
|
154
|
+
//
|
|
155
|
+
// A schema graph that reaches one object through a branching position —
|
|
156
|
+
// `anyOf: [shared, shared]`, which is what building a schema out of reused
|
|
157
|
+
// JavaScript objects produces, and which needs no `$ref` — has 2^depth pointers
|
|
158
|
+
// over a handful of objects. A cyclic one has no end at all. Neither is refused
|
|
159
|
+
// by a depth cap.
|
|
160
|
+
//
|
|
161
|
+
// Far above any authored schema: the JSON Schema meta-schema is on the order of
|
|
162
|
+
// a hundred positions, and an OpenAPI document of a thousand endpoints is tens
|
|
163
|
+
// of thousands. A schema that reaches this is not one someone wrote out.
|
|
164
|
+
var MAX_REGISTRY_NODES = 100000; // subschema positions, not bytes
|
|
165
|
+
|
|
166
|
+
// How deep a schema may nest before it is refused. This is the OTHER half of
|
|
167
|
+
// the bound, and it is not the node ceiling in disguise: a chain ten thousand
|
|
168
|
+
// levels deep is ten thousand positions, far under that ceiling, and still runs
|
|
169
|
+
// the JavaScript stack out. What comes back then is `RangeError: Maximum call
|
|
170
|
+
// stack size exceeded` — the engine's error, which says nothing about the
|
|
171
|
+
// schema and is not catchable as a JsonSchemaError.
|
|
172
|
+
//
|
|
173
|
+
// It THROWS rather than stopping the walk. A walk that silently returns at the
|
|
174
|
+
// ceiling leaves a node looking examined when its descendants were skipped,
|
|
175
|
+
// which is how a pattern below one escapes screening entirely. Refusing is the
|
|
176
|
+
// only reading that cannot be mistaken for completion.
|
|
177
|
+
//
|
|
178
|
+
// Well above anything authored — the JSON Schema meta-schema nests about a
|
|
179
|
+
// dozen levels and an OpenAPI document a few dozen — and well below where the
|
|
180
|
+
// stack gives out.
|
|
181
|
+
var MAX_SCHEMA_NESTING = 1000; // nesting levels, not bytes
|
|
182
|
+
|
|
183
|
+
function _Registry() {
|
|
184
|
+
this.schemas = {}; this.dynamicAnchors = {}; this.baseByNode = new Map();
|
|
185
|
+
this.nodesIndexed = 0;
|
|
186
|
+
}
|
|
145
187
|
|
|
146
188
|
_Registry.prototype.add = function (schema, baseUri) {
|
|
147
|
-
this._walk(schema, baseUri || "", "");
|
|
189
|
+
this._walk(schema, baseUri || "", "", null, 0);
|
|
148
190
|
// A document retrieved from URI X is addressable by X even when its own
|
|
149
191
|
// $id is a different (canonical) URI — register the retrieval URI too.
|
|
150
192
|
if (baseUri && (_isObject(schema) || typeof schema === "boolean")) {
|
|
@@ -156,10 +198,43 @@ _Registry.prototype.add = function (schema, baseUri) {
|
|
|
156
198
|
// Walk a schema document, registering $id base changes, $anchor and
|
|
157
199
|
// $dynamicAnchor names, and indexing every subschema by its base URI +
|
|
158
200
|
// JSON-pointer fragment.
|
|
159
|
-
_Registry.prototype._walk = function (node, baseUri, pointer) {
|
|
201
|
+
_Registry.prototype._walk = function (node, baseUri, pointer, path, depth) {
|
|
160
202
|
if (!_isObject(node) && typeof node !== "boolean") return;
|
|
203
|
+
if (depth > MAX_SCHEMA_NESTING) {
|
|
204
|
+
throw new JsonSchemaError("json-schema/schema-too-deep",
|
|
205
|
+
"jsonSchema: schema nests deeper than " + MAX_SCHEMA_NESTING +
|
|
206
|
+
" levels — deeper than the walk can index without exhausting the stack");
|
|
207
|
+
}
|
|
208
|
+
this.nodesIndexed += 1;
|
|
209
|
+
if (this.nodesIndexed > MAX_REGISTRY_NODES) {
|
|
210
|
+
throw new JsonSchemaError("json-schema/schema-too-large",
|
|
211
|
+
"jsonSchema: schema indexes more than " + MAX_REGISTRY_NODES +
|
|
212
|
+
" subschema positions — a shared or cyclic schema graph reaches one " +
|
|
213
|
+
"object through many pointers; give the reused subschema an $id and " +
|
|
214
|
+
"reference it with $ref instead of embedding the same object twice");
|
|
215
|
+
}
|
|
161
216
|
if (typeof node === "boolean") { this.schemas[baseUri + "#" + pointer] = node; return; }
|
|
162
217
|
|
|
218
|
+
// A node already on the ACTIVE PATH is a cycle, and a cycle with one
|
|
219
|
+
// reference per level never reaches the ceiling above: it does not branch, so
|
|
220
|
+
// it recurses straight down and exhausts the JavaScript stack while the count
|
|
221
|
+
// is still in the thousands, surfacing as a RangeError from the engine rather
|
|
222
|
+
// than the refusal this validator documents.
|
|
223
|
+
//
|
|
224
|
+
// The test is the path and not a depth cap, because deep is not the same as
|
|
225
|
+
// cyclic: a schema nesting four hundred levels with no cycle and no `$ref`
|
|
226
|
+
// compiles today, and a depth cap would refuse it — under a `ref-loop` code
|
|
227
|
+
// naming a reference chain it does not have. The path set answers the
|
|
228
|
+
// question actually being asked, and leaves depth alone.
|
|
229
|
+
path = path || new Set();
|
|
230
|
+
var isOwnAncestor = path.has(node);
|
|
231
|
+
if (isOwnAncestor) {
|
|
232
|
+
throw new JsonSchemaError("json-schema/ref-loop",
|
|
233
|
+
"jsonSchema: schema contains a cycle — a subschema is its own ancestor, " +
|
|
234
|
+
"so indexing it has no end; break the cycle with $id + $ref");
|
|
235
|
+
}
|
|
236
|
+
path.add(node);
|
|
237
|
+
|
|
163
238
|
var thisBase = baseUri;
|
|
164
239
|
if (typeof node.$id === "string") {
|
|
165
240
|
thisBase = _resolveUri(node.$id, baseUri);
|
|
@@ -191,7 +266,7 @@ _Registry.prototype._walk = function (node, baseUri, pointer) {
|
|
|
191
266
|
// Recurse. Keywords whose values are schemas vs maps-of-schemas vs
|
|
192
267
|
// arrays-of-schemas are walked with the right shape.
|
|
193
268
|
var self = this;
|
|
194
|
-
function child(key, sub, ptr) { self._walk(sub, thisBase, ptr); }
|
|
269
|
+
function child(key, sub, ptr) { self._walk(sub, thisBase, ptr, path, depth + 1); }
|
|
195
270
|
SCHEMA_KEYWORDS.forEach(function (k) {
|
|
196
271
|
if (node[k] !== undefined) child(k, node[k], pointer + "/" + k);
|
|
197
272
|
});
|
|
@@ -207,6 +282,12 @@ _Registry.prototype._walk = function (node, baseUri, pointer) {
|
|
|
207
282
|
node[k].forEach(function (sub, idx) { child(k, sub, pointer + "/" + k + "/" + idx); });
|
|
208
283
|
}
|
|
209
284
|
});
|
|
285
|
+
|
|
286
|
+
// Off the active path on the way out. A node reached again through a SIBLING
|
|
287
|
+
// branch is shared rather than cyclic, and indexing it under its second
|
|
288
|
+
// pointer is the correct thing to do — leaving it in the set would call that
|
|
289
|
+
// a cycle and refuse a schema that merely reuses a subschema.
|
|
290
|
+
path.delete(node);
|
|
210
291
|
};
|
|
211
292
|
|
|
212
293
|
_Registry.prototype.resolve = function (uri) {
|
|
@@ -255,7 +336,13 @@ var SCHEMA_KEYWORDS = ["additionalProperties", "propertyNames", "items",
|
|
|
255
336
|
"unevaluatedProperties"];
|
|
256
337
|
var SCHEMA_MAP_KEYWORDS = ["$defs", "definitions", "properties",
|
|
257
338
|
"patternProperties", "dependentSchemas"];
|
|
258
|
-
|
|
339
|
+
// `items` appears in BOTH lists on purpose. Draft 2020-12 makes it a single
|
|
340
|
+
// schema, and the legacy tuple form makes it an array of them; each branch
|
|
341
|
+
// checks the type it wants, so exactly one of them walks any given `items`.
|
|
342
|
+
// Listed only as a single schema, an array-valued one was dropped at the
|
|
343
|
+
// `_isObject` test at the top of the walk and never indexed at all — so the
|
|
344
|
+
// subschemas inside a tuple were unreachable by `$ref`, and unbudgeted.
|
|
345
|
+
var SCHEMA_ARRAY_KEYWORDS = ["allOf", "anyOf", "oneOf", "prefixItems", "items"];
|
|
259
346
|
|
|
260
347
|
module.exports = _buildModule();
|
|
261
348
|
|
|
@@ -315,25 +402,55 @@ var _SUBSCHEMA_MAP_KEYS = ["$defs", "definitions", "dependentSchemas",
|
|
|
315
402
|
|
|
316
403
|
// Walk the subschema positions for the two keywords that carry a regular
|
|
317
404
|
// expression, and hand each to the same screen the compile site uses.
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
405
|
+
//
|
|
406
|
+
// Every object is visited ONCE, tracked by identity. The depth ceiling below is
|
|
407
|
+
// not a substitute for that and never was: it limits how far a single path runs
|
|
408
|
+
// and says nothing about how many paths there are. A schema graph reaching one
|
|
409
|
+
// object through a branching position — `anyOf: [shared, shared]`, which is what
|
|
410
|
+
// building a schema in JavaScript out of reused subschema objects produces, and
|
|
411
|
+
// which needs no `$ref` — was walked once per path. Twenty-one objects cost
|
|
412
|
+
// 2.6 seconds, doubling per added level, against a cap that allows 256; a cyclic
|
|
413
|
+
// schema never returned. This screen exists so a pattern cannot make the
|
|
414
|
+
// validator hang, and SECURITY.md promises a screen costs the length of its
|
|
415
|
+
// input and never a function of its shape, so a walk that is itself a function
|
|
416
|
+
// of shape contradicts the thing it was added to guarantee.
|
|
417
|
+
//
|
|
418
|
+
// Identity tracking bounds the total work by the number of distinct objects,
|
|
419
|
+
// which is the length of the schema — so the promise holds without a separate
|
|
420
|
+
// node budget.
|
|
421
|
+
//
|
|
422
|
+
// The nesting ceiling here THROWS, and that distinction is the whole point. A
|
|
423
|
+
// ceiling that silently stopped walking would leave a node looking examined
|
|
424
|
+
// when its descendants were skipped, so a later and shallower path would find
|
|
425
|
+
// it already marked and skip it too — and a catastrophic pattern below it would
|
|
426
|
+
// never be screened at all, which is the opposite of what this walk is for.
|
|
427
|
+
// Refusing cannot be mistaken for completion.
|
|
428
|
+
function _screenPatterns(node, seen, depth) {
|
|
429
|
+
if (!_isObject(node)) return;
|
|
430
|
+
if (depth > MAX_SCHEMA_NESTING) {
|
|
431
|
+
throw new JsonSchemaError("json-schema/schema-too-deep",
|
|
432
|
+
"jsonSchema: schema nests deeper than " + MAX_SCHEMA_NESTING +
|
|
433
|
+
" levels — deeper than the pattern screen can walk without exhausting " +
|
|
434
|
+
"the stack");
|
|
435
|
+
}
|
|
436
|
+
seen = seen || new WeakSet();
|
|
437
|
+
if (seen.has(node)) return;
|
|
438
|
+
seen.add(node);
|
|
323
439
|
|
|
324
440
|
if (typeof node.pattern === "string") _compileRegex(node.pattern);
|
|
325
441
|
if (_isObject(node.patternProperties)) {
|
|
326
442
|
Object.keys(node.patternProperties).forEach(function (p) { _compileRegex(p); });
|
|
327
443
|
}
|
|
328
444
|
|
|
329
|
-
|
|
445
|
+
var next = (depth || 0) + 1;
|
|
446
|
+
_SUBSCHEMA_KEYS.forEach(function (k) { _screenPatterns(node[k], seen, next); });
|
|
330
447
|
_SUBSCHEMA_LIST_KEYS.forEach(function (k) {
|
|
331
448
|
if (!Array.isArray(node[k])) return;
|
|
332
|
-
node[k].forEach(function (sub) { _screenPatterns(sub,
|
|
449
|
+
node[k].forEach(function (sub) { _screenPatterns(sub, seen, next); });
|
|
333
450
|
});
|
|
334
451
|
_SUBSCHEMA_MAP_KEYS.forEach(function (k) {
|
|
335
452
|
if (!_isObject(node[k])) return;
|
|
336
|
-
Object.keys(node[k]).forEach(function (name) { _screenPatterns(node[k][name],
|
|
453
|
+
Object.keys(node[k]).forEach(function (name) { _screenPatterns(node[k][name], seen, next); });
|
|
337
454
|
});
|
|
338
455
|
}
|
|
339
456
|
|