@blamejs/blamejs-shop 0.4.48 → 0.4.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 +4 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/inventory-audits.js +5 -0
- package/lib/inventory-locations.js +66 -11
- package/lib/inventory-writeoffs.js +4 -0
- package/lib/vendor/MANIFEST.json +58 -46
- package/lib/vendor/blamejs/.github/workflows/ci.yml +134 -1
- package/lib/vendor/blamejs/.gitignore +5 -1
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/README.md +1 -1
- package/lib/vendor/blamejs/SECURITY.md +3 -1
- package/lib/vendor/blamejs/api-snapshot.json +10 -2
- package/lib/vendor/blamejs/lib/bundler.js +2 -7
- package/lib/vendor/blamejs/lib/config-drift.js +17 -3
- package/lib/vendor/blamejs/lib/crypto-field.js +30 -0
- package/lib/vendor/blamejs/lib/db-declare-row-policy.js +20 -1
- package/lib/vendor/blamejs/lib/db-schema.js +29 -0
- package/lib/vendor/blamejs/lib/db.js +7 -0
- package/lib/vendor/blamejs/lib/guard-csv.js +13 -4
- package/lib/vendor/blamejs/lib/local-db-thin.js +23 -1
- package/lib/vendor/blamejs/lib/mail-bimi.js +16 -3
- package/lib/vendor/blamejs/lib/mail-scan.js +2 -5
- package/lib/vendor/blamejs/lib/mail.js +16 -9
- package/lib/vendor/blamejs/lib/mcp.js +28 -6
- package/lib/vendor/blamejs/lib/middleware/bot-disclose.js +7 -5
- package/lib/vendor/blamejs/lib/middleware/speculation-rules.js +6 -4
- package/lib/vendor/blamejs/lib/numeric-bounds.js +32 -0
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +12 -1
- package/lib/vendor/blamejs/lib/object-store/gcs.js +12 -1
- package/lib/vendor/blamejs/lib/object-store/http-put.js +11 -1
- package/lib/vendor/blamejs/lib/object-store/index.js +4 -0
- package/lib/vendor/blamejs/lib/object-store/local.js +11 -1
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +86 -5
- package/lib/vendor/blamejs/lib/parsers/safe-env.js +6 -3
- package/lib/vendor/blamejs/lib/parsers/safe-yaml.js +6 -6
- package/lib/vendor/blamejs/lib/safe-buffer.js +69 -1
- package/lib/vendor/blamejs/lib/safe-decompress.js +3 -12
- package/lib/vendor/blamejs/lib/seeders.js +33 -39
- package/lib/vendor/blamejs/lib/storage.js +71 -7
- package/lib/vendor/blamejs/lib/vault/rotate.js +4 -13
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.10.json +53 -0
- package/lib/vendor/blamejs/release-notes/v0.15.11.json +52 -0
- package/lib/vendor/blamejs/test/integration/object-store-worm-lock.test.js +90 -16
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +150 -39
- package/lib/vendor/blamejs/test/layer-0-primitives/config-drift.test.js +19 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-aad-downgrade.test.js +96 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-transaction.test.js +110 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/declare-row-policy.test.js +43 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/local-db-thin.test.js +28 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mcp.test.js +25 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/numeric-bounds.test.js +29 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/object-store-versioned-delete.test.js +97 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-buffer-linear-scans.test.js +94 -0
- package/lib/vendor/blamejs/test/layer-5-integration/bundler-output.test.js +52 -0
- package/package.json +1 -1
|
@@ -2852,6 +2852,15 @@ async function testNoDuplicateCodeBlocks() {
|
|
|
2852
2852
|
],
|
|
2853
2853
|
reason: "v0.15.4 — coincidental 50-tok normalized window across three unrelated domains: an archive adapter's close() (destroy a readable / closeSync an fd), the crypto-field per-row-residency enumerator (listPerRowResidency — map declared tables to {table, residencyColumn, allowedTags}), and the tracing spanSync delegator (type-check fn, delegate to span()). The shingle is the short-function / object-literal-return shell, not behaviour — one releases a handle, one projects a config map, one delegates a call. archive-adapters:close + tracing:spanSync were already a sub-threshold pair; listPerRowResidency (added so backup.create can see per-row cross-border regions a deployment-level check is blind to) became the third member tipping it over the 3-file STRONG-DUP floor.",
|
|
2854
2854
|
},
|
|
2855
|
+
{
|
|
2856
|
+
mode: "family-subset",
|
|
2857
|
+
files: [
|
|
2858
|
+
"lib/db.js:init",
|
|
2859
|
+
"lib/eat.js:verify",
|
|
2860
|
+
"lib/http-client-cookie-jar.js:getAll",
|
|
2861
|
+
],
|
|
2862
|
+
reason: "v0.15.11 — coincidental 50-tok normalized window across three unrelated domains: db.init's declarative schema -> cryptoField.registerTable forwarding (a fixed list of `key: t.key` property copies inside the per-table loop), eat.verify's EAT claim extraction, and the cookie jar's getAll iteration. The shingle is the property-copy / loop-body shell, not behaviour — one registers crypto-field schema, one verifies an attestation token, one collects cookies. db.init became the third member, tipping it over the 3-file STRONG-DUP floor, when `allowPlainMigration: t.allowPlainMigration` was added to the registerTable forward (Codex P2: the read-side pre-AAD migration opt-in must survive the db.init consumer path, not just direct registerTable callers).",
|
|
2863
|
+
},
|
|
2855
2864
|
{
|
|
2856
2865
|
mode: "family-subset",
|
|
2857
2866
|
files: [
|
|
@@ -6980,7 +6989,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
6980
6989
|
{
|
|
6981
6990
|
id: "sigv4-awsuriencode-utf16-unit-iteration",
|
|
6982
6991
|
primitive: "iterate awsUriEncode by code point (Array.from / codePointAt), not by UTF-16 index + charAt — a per-unit encodeURIComponent throws URIError on a non-BMP key's split surrogate pair",
|
|
6983
|
-
regex: /function awsUriEncode\([\s\S]{0,
|
|
6992
|
+
regex: /function awsUriEncode\((?:(?!\n\}|encodeURIComponent)[\s\S]){0,4000}?encodeURIComponent/,
|
|
6984
6993
|
requires: /Array\.from|codePointAt/,
|
|
6985
6994
|
allowlist: [],
|
|
6986
6995
|
reason: "Object-store correctness — encodeURIComponent on a lone surrogate throws 'URIError: URI malformed', so iterating awsUriEncode by str.charAt(i) and escaping each UTF-16 unit breaks any object key containing a non-BMP character (emoji, CJK Extension B, ...) before the request is signed. The encoder must walk Unicode code points (Array.from(str) keeps surrogate pairs together) so the whole character reaches encodeURIComponent as one UTF-8 sequence.",
|
|
@@ -6989,7 +6998,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
6989
6998
|
{
|
|
6990
6999
|
id: "sql-createtable-ddl-not-catalog-gated",
|
|
6991
7000
|
primitive: "route createTable's emitted CREATE TABLE through _assertCatalogEmittable (its quote-aware single-statement scan is the injection backstop for the one raw-emission position — the verbatim column type) — never return a bare { sql, params }",
|
|
6992
|
-
regex: /var sql = "CREATE TABLE " \+ ifNot[\s\S]{0,
|
|
7001
|
+
regex: /var sql = "CREATE TABLE " \+ ifNot(?:(?!\n\}|return \{ sql:)[\s\S]){0,8000}?return \{ sql:/,
|
|
6993
7002
|
allowlist: [],
|
|
6994
7003
|
reason: "SQL injection — _ddlType returns an unrecognised column type verbatim into the DDL; it is the one raw-emission position in an otherwise quote-by-construction builder (constraints route through _checkRawFragment, names through _quoteId). The injection backstop is the quote-aware _assertCatalogEmittable scan, which refuses a top-level ';' / comment / unbalanced quote / unbalanced paren while CORRECTLY allowing those characters inside a balanced quoted label (ENUM('needs;review')). createTable must therefore return _assertCatalogEmittable(sql, []) — a bare { sql, params } would let a type like 'text); DROP TABLE x; --' emit a stacked statement. A non-quote-aware pre-scan on the type was removed precisely because it over-rejected valid quoted labels.",
|
|
6995
7004
|
},
|
|
@@ -7030,7 +7039,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
7030
7039
|
{
|
|
7031
7040
|
id: "xml-parsename-no-prototype-key-rejection",
|
|
7032
7041
|
primitive: "reject element/attribute names __proto__/constructor/prototype in the XML name parser (lib/parsers/safe-xml.js parseName → FORBIDDEN_KEYS)",
|
|
7033
|
-
regex: /xml\/bad-name[\s\S]{0,
|
|
7042
|
+
regex: /xml\/bad-name(?:(?!\n {2}\}|return\s+input\.substring)[\s\S]){0,3000}?return\s+input\.substring/,
|
|
7034
7043
|
requires: /FORBIDDEN_KEYS\.has/,
|
|
7035
7044
|
skipCommentLines: true,
|
|
7036
7045
|
allowlist: [],
|
|
@@ -7039,7 +7048,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
7039
7048
|
{
|
|
7040
7049
|
id: "xml-make-wrapper-plain-object",
|
|
7041
7050
|
primitive: "build the XML element-name wrapper with Object.create(null) (lib/parsers/safe-xml.js _make), not a plain {} keyed by an attacker-influenced element name",
|
|
7042
|
-
regex: /function _make\(name, value\)[\s\S]{0,
|
|
7051
|
+
regex: /function _make\(name, value\)(?:(?!\n {2}\}|var out = \{\})[\s\S]){0,4000}?var out = \{\}/,
|
|
7043
7052
|
requires: /var out = Object\.create\(null\)/,
|
|
7044
7053
|
skipCommentLines: true,
|
|
7045
7054
|
allowlist: [],
|
|
@@ -7127,13 +7136,54 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
7127
7136
|
// is built without the limits shape.
|
|
7128
7137
|
{
|
|
7129
7138
|
id: "db-databasesync-without-sqlite-limits",
|
|
7130
|
-
primitive: "construct
|
|
7139
|
+
primitive: "construct every raw-SQL-exposing DatabaseSync opener (db.init's dbPath, the CLI's dbPath, localDb.thin's file) with the node:sqlite limits option (sqlLength) — a parse-time DoS floor complementary to streamLimit",
|
|
7131
7140
|
scanScope: "lib",
|
|
7132
|
-
regex: /new DatabaseSync\(dbPath\b/,
|
|
7133
|
-
requires: /
|
|
7141
|
+
regex: /new DatabaseSync\(\s*(?:dbPath|file)\b/,
|
|
7142
|
+
requires: /sqlLength\s*:/,
|
|
7134
7143
|
skipCommentLines: true,
|
|
7135
7144
|
allowlist: [],
|
|
7136
|
-
reason: "v0.15.9 Node-24.16 adoption. The framework parameterizes
|
|
7145
|
+
reason: "v0.15.9 Node-24.16 adoption, widened v0.15.10 (#320). The framework parameterizes builder values, so SQLITE_LIMIT_LENGTH (sqlLength) guards the surfaces that parse operator/application raw SQL — b.db.runSql, the CLI, and b.localDb.thin's prepare()/exec() — against an attacker-influenced megaquery the parser would otherwise chew (SQLite default is 1 GB). Fires when a raw-SQL opener (`new DatabaseSync(dbPath` / `new DatabaseSync(file`) is constructed without the `limits: { ... sqlLength` shape. v0.15.9 anchored only on `dbPath` and so missed localDb.thin's `file` handle (#320); the var set now covers all three. The ephemeral headroom probe `new DatabaseSync(p)` and the vault-rotation temp handles are intentionally not matched — they run fixed PRAGMAs / parameterized re-seals, no attacker statement text. (SQLITE_LIMIT_ATTACHED is left at the SQLite default — the snapshot/backup path uses ATTACH.)",
|
|
7146
|
+
},
|
|
7147
|
+
{
|
|
7148
|
+
id: "trailing-hspace-regex-replace-is-quadratic",
|
|
7149
|
+
primitive: "strip trailing horizontal whitespace via safeBuffer.stripTrailingHspace(s) — NOT s.replace(TRAILING_HSPACE_RE) / .replace(/[ \\t]+$/)",
|
|
7150
|
+
scanScope: "lib",
|
|
7151
|
+
regex: /\.replace\(\s*(?:safeBuffer\.)?TRAILING_HSPACE_RE\b|\.replace\(\s*\/\[ \\t\]\+\$\/|(?:\/\[ \\t\]\+\$\/[gimsuy]*|\bTRAILING_HSPACE_RE)\.test\b/,
|
|
7152
|
+
skipCommentLines: true,
|
|
7153
|
+
allowlist: [],
|
|
7154
|
+
reason: "v0.15.11 (CodeQL js/polynomial-redos). `/[ \\t]+$/` (TRAILING_HSPACE_RE) used with .replace() is O(n^2) in V8 on adversarial input — a long run of spaces/tabs then a non-space makes the engine retry the greedy match from every offset (200K spaces ~12s; the env/yaml parsers cap TOTAL bytes, not per-line, so one huge-whitespace line hangs). safeBuffer.stripTrailingHspace is a linear backward char-scan, byte-identical to the regex. Every internal trailing-whitespace strip (safe-buffer/safe-env x3/safe-yaml x7) routes through it; TRAILING_HSPACE_RE stays exported for `.test()`-style existence checks (linear) but a `.replace(...TRAILING_HSPACE_RE)` is the regression. Empty allowlist.",
|
|
7155
|
+
},
|
|
7156
|
+
// v0.15.11 — locating a document-structural tag (<body>/<html>/<head>) in a
|
|
7157
|
+
// response body via `str.match(/<tag[^>]*>/)` is O(n^2) in V8: a body
|
|
7158
|
+
// carrying many `<tag` starts with no closing `>` (rendered user content
|
|
7159
|
+
// can produce exactly that) makes the engine retry the greedy `[^>]*` from
|
|
7160
|
+
// every offset. A `<body`-repeated 200K body benchmarks in seconds. The
|
|
7161
|
+
// response-injection middleware (bot-disclose, speculation-rules) must use
|
|
7162
|
+
// the linear safeBuffer.indexAfterOpenTag(html, tag) instead.
|
|
7163
|
+
{
|
|
7164
|
+
id: "html-tag-find-via-greedy-bracket-match-is-quadratic",
|
|
7165
|
+
primitive: "find a <tag>'s insertion point via safeBuffer.indexAfterOpenTag(html, tag) (linear indexOf walk) — NOT str.match(/<tag[^>]*>/)",
|
|
7166
|
+
scanScope: "lib",
|
|
7167
|
+
regex: /\.(?:match|search)\(\s*\/<\w+\[\^>\]\*>/,
|
|
7168
|
+
skipCommentLines: true,
|
|
7169
|
+
allowlist: [],
|
|
7170
|
+
reason: "v0.15.11 (CodeQL js/polynomial-redos, the #168/#170 response-injection shape). `body.match(/<body[^>]*>/i)` is the WORM-the-body-tag find both bot-disclose and speculation-rules used to splice content after <body>; the greedy `[^>]*`-then-`>` retries from every `<body` offset, O(n^2) on a body with many `<body` and no `>` (an app rendering user content into its HTML reaches it). safeBuffer.indexAfterOpenTag is a single forward indexOf walk, linear, and stricter (requires a real tag boundary so `<bodyfoo>` isn't a false <body>). Fires on the str.match(/<tag[^>]*>/) form; the WCAG checker's `/<body\\b[^>]*>/i.exec(html)` is the regex.exec(str) form on operator-rendered audit input (dev/CI, not a request hot path) and is a different shape, so it stays silent. Empty allowlist.",
|
|
7171
|
+
},
|
|
7172
|
+
// v0.15.11 (Codex P2) — mcp sanitize-mode redaction must remove EVERY
|
|
7173
|
+
// dangerous token, not just the leftmost. A non-global String.replace on a
|
|
7174
|
+
// multi-alternation regex strips only the first match, so
|
|
7175
|
+
// `data:text/html,<script>...` would keep the executable <script> and
|
|
7176
|
+
// sanitize mode would return runnable HTML. The fix routes both the
|
|
7177
|
+
// dangerous-HTML and prompt-injection redactions through _redactAll(t,
|
|
7178
|
+
// <RE>_G) — a global replace looped to a fixpoint.
|
|
7179
|
+
{
|
|
7180
|
+
id: "mcp-sanitize-redact-must-be-global",
|
|
7181
|
+
primitive: "redact dangerous-HTML / injection tokens in mcp sanitize mode via _redactAll(t, DANGEROUS_HTML_RE_G / INJECTION_RE_G) — NOT t.replace(DANGEROUS_HTML_RE / INJECTION_RE, ...) (non-global leaves every match after the first)",
|
|
7182
|
+
scanScope: "lib",
|
|
7183
|
+
regex: /\.replace\(\s*(?:DANGEROUS_HTML_RE|INJECTION_RE)\s*,/,
|
|
7184
|
+
skipCommentLines: true,
|
|
7185
|
+
allowlist: [],
|
|
7186
|
+
reason: "v0.15.11 (Codex P2). A non-global String.replace removes only the LEFTMOST match; on `data:text/html,<script>alert(1)</script>` it strips `data:text/html` and leaves the executable <script>, so sanitize mode returns runnable HTML for the exact vector the vbscript:/data:text/html alternation was added to neutralize. The fix is _redactAll(t, <RE>_G): a global replace repeated to a fixpoint so every dangerous token is removed. The _G global variants do NOT match this regex (`RE_G,` has no `\\s*,` right after `RE`), so the fixed call stays silent; a reverted non-global `.replace(DANGEROUS_HTML_RE,` / `.replace(INJECTION_RE,` fires. Empty allowlist.",
|
|
7137
7187
|
},
|
|
7138
7188
|
// v0.15.9 — the RFC 9527 Clear-Site-Data header value must be built via the
|
|
7139
7189
|
// shared middleware/clear-site-data headerValue() helper (which validates
|
|
@@ -7397,7 +7447,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
7397
7447
|
// regex per shape keeps V8's backtracking engine happy on large
|
|
7398
7448
|
// files (an alternation `(?:A)|(?:B)` with backrefs + `[\s\S]{0,N}?`
|
|
7399
7449
|
// triggered an OOM on the first attempt).
|
|
7400
|
-
regex: /var\s+\w+\s*=\s*\w+\.get\s*\([^;]+\)\s*;\s*\n\s*if\s*\(\s*!\s*\w+\s*\)\s*\{[\s\S]{0,
|
|
7450
|
+
regex: /var\s+\w+\s*=\s*\w+\.get\s*\([^;]+\)\s*;\s*\n\s*if\s*\(\s*!\s*\w+\s*\)\s*\{(?:(?!\n\s{0,4}\}\s*\n|\.set\s*\()[\s\S]){0,4000}?\.set\s*\(/,
|
|
7401
7451
|
allowlist: [
|
|
7402
7452
|
"lib/cache.js", // tagIndex (Map<tag, Set<key>>) — Set factory
|
|
7403
7453
|
"lib/crypto-field.js", // _rateFailWindows (Map<actor:table:column, ts[]>) in _rateNoteFailure — timestamp-array factory
|
|
@@ -7424,7 +7474,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
7424
7474
|
// intermediate `var X = M.get(k)` binding). See the sibling entry.
|
|
7425
7475
|
id: "map-has-then-set-pre-node-26",
|
|
7426
7476
|
primitive: "Map.prototype.getOrInsertComputed(key, factory) (Node 26+); pre-floor-bump call sites are allowlisted below with per-site map+factory annotations — the floor-bump sweep walks the allowlist",
|
|
7427
|
-
regex: /if\s*\(\s*!\s*\w+\.has\s*\([^)]+\)\s*\)\s*\{[\s\S]{0,
|
|
7477
|
+
regex: /if\s*\(\s*!\s*\w+\.has\s*\([^)]+\)\s*\)\s*\{(?:(?!\n\s{0,4}\}\s*\n|\.set\s*\()[\s\S]){0,4000}?\.set\s*\(/,
|
|
7428
7478
|
allowlist: [
|
|
7429
7479
|
"lib/websocket-channels.js", // channelToConns (Map<channel, Set<conn>>) — Set factory; cluster-shared race window
|
|
7430
7480
|
// Edge cases — flagged structurally but do NOT migrate cleanly
|
|
@@ -7661,7 +7711,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
7661
7711
|
// parsers added to lib/ inherit the discipline automatically.
|
|
7662
7712
|
id: "slice1-optional-parseint-silent-default",
|
|
7663
7713
|
primitive: "after `var X = Y.slice(1)`, refuse empty-digit segment with an explicit throw BEFORE parseInt; never silently default to the no-suffix mask",
|
|
7664
|
-
regex: /\.slice\s*\(\s*1\s*\)\s*;[\s\S]{0,
|
|
7714
|
+
regex: /\.slice\s*\(\s*1\s*\)\s*;(?:(?!\n\s{0,2}\}|\bparseInt\s*\()[\s\S]){0,400}?if\s*\(\s*\w+\.length\s*>\s*0\s*\)\s*\{(?:(?!\n\s{0,2}\})[\s\S]){0,800}?\bparseInt\s*\(/,
|
|
7665
7715
|
requires: /(?:cidr-length is empty|prefix-length is empty|grammar requires 1\*DIGIT)/,
|
|
7666
7716
|
skipCommentLines: true,
|
|
7667
7717
|
allowlist: [],
|
|
@@ -7827,6 +7877,24 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
7827
7877
|
reason: "Codex P1 on v0.12.13 PR #164 — objectStoreAdapter.listKeys called client.list once and never followed the truncated/continuationToken pagination contract. The fix walks the loop with a PAGINATION_CAP safety net. Detector locks the shape so a future caller of client.list can't silently drop pagination.",
|
|
7828
7878
|
},
|
|
7829
7879
|
|
|
7880
|
+
{
|
|
7881
|
+
id: "storage-deletefile-drops-versionid-threading",
|
|
7882
|
+
primitive: "b.storage.deleteFile MUST thread { versionId, bypassGovernanceRetention } to backend.delete — a bare picked.backend.delete(key) reverts to the WORM-blind unversioned delete that only writes a delete-marker on an S3 Object-Lock bucket, masking that the data version survives (a fake erasure for GDPR Art. 17 / crypto-shred).",
|
|
7883
|
+
scanScope: "lib",
|
|
7884
|
+
regex: /function deleteFile\s*\(\s*key\s*,\s*opts\s*\)(?:(?!\n\})[\s\S]){0,800}?\.backend\.delete\s*\(\s*key\s*\)/,
|
|
7885
|
+
allowlist: [],
|
|
7886
|
+
reason: "v0.15.10 #88 — deleteFile threads versionId + bypassGovernanceRetention to backend.delete so b.storage.deleteFile(key, { versionId }) reaches the S3 versioned-delete (real erasure on an Object-Lock bucket); a versioned delete of a retained version is refused (throws), never a silent delete-marker success. A bare picked.backend.delete(key) is the pre-fix WORM-blind shape. Anchored on deleteFile(key, opts) + tempered on \\n} (deleteFile's own close) so it can't bleed into a sibling function; storage.js is the only deleteFile(key, opts) site. Empty allowlist — the bare form is the regression. The behavioral guard is test/integration/object-store-worm-lock.test.js (framework-API WORM proof on live MinIO); this is the cheap always-on structural backstop.",
|
|
7887
|
+
},
|
|
7888
|
+
|
|
7889
|
+
{
|
|
7890
|
+
id: "object-store-backend-deletekey-ignores-versionid-contract",
|
|
7891
|
+
primitive: "every object-store backend deleteKey MUST accept (key, opts) and handle opts.versionId — sigv4 threads it to the versioned delete, every other backend throws VERSIONID_UNSUPPORTED. A single-param deleteKey(key) silently ignores a versionId an erasure workflow passed and issues a plain delete, the WORM-blind footgun #88 closed.",
|
|
7892
|
+
scanScope: "lib",
|
|
7893
|
+
regex: /function deleteKey\s*\(\s*key\s*\)/,
|
|
7894
|
+
allowlist: [],
|
|
7895
|
+
reason: "v0.15.10 #88 — the b.storage.deleteFile -> backend.delete({ versionId }) contract reaches whichever backend is routed (sigv4 / azure-blob / gcs / local / http-put). A backend that defines deleteKey(key) with no opts param drops the versionId silently (the original http-put miss Codex P2 caught: it forwarded versionId to a deleteKey that ignored it, issuing a plain DELETE while an erasure workflow believed it targeted a version). All five backends now take (key, opts); the single-param shape is the regression. Empty allowlist — a new object-store backend must take (key, opts) and either thread versionId (S3 versioned delete) or throw VERSIONID_UNSUPPORTED.",
|
|
7896
|
+
},
|
|
7897
|
+
|
|
7830
7898
|
{
|
|
7831
7899
|
// Codex P1A on v0.12.12 PR #163 — "on-request" placement
|
|
7832
7900
|
// semantics collapsed into "always" when shouldEmit didn't
|
|
@@ -7897,7 +7965,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
7897
7965
|
// happens to follow an unrelated .map() in the same file.
|
|
7898
7966
|
id: "rag-source-classify-without-classifywithsources",
|
|
7899
7967
|
primitive: "to classify retrieval-augmented (RAG) sources alongside a prompt, compose b.ai.input.classifyWithSources — do NOT map b.ai.input.classify over a sources array by hand. classifyWithSources applies a tier-relative threshold (untrusted / internal sources escalate on a single severity-2 / any severity-3, vs classify's 2-severity-2 baseline) and computes the worst-of aggregate + tainted-source set. Mapping classify directly loses the stricter retrieved-data threshold (OWASP LLM01:2025 indirect prompt injection; CVE-2025-32711 EchoLeak).",
|
|
7900
|
-
regex: /\.map\(\s*(?:function\s*\([^)]*\)\s*\{|\([^)]*\)\s*=>|[A-Za-z_$][\w$]*\s*=>)[\s\S]{0,
|
|
7968
|
+
regex: /\.map\(\s*(?:function\s*\([^)]*\)\s*\{|\([^)]*\)\s*=>|[A-Za-z_$][\w$]*\s*=>)(?:(?!\n\s{0,2}\})[\s\S]){0,600}?(?<!function\s)\bclassify\s*\(/,
|
|
7901
7969
|
requires: /classifyWithSources|allow:rag-source-classify-without-classifywithsources/,
|
|
7902
7970
|
skipCommentLines: true,
|
|
7903
7971
|
allowlist: [],
|
|
@@ -7947,7 +8015,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
7947
8015
|
// is not flagged.
|
|
7948
8016
|
id: "c2pa-timestamp-bespoke-chain-check",
|
|
7949
8017
|
primitive: "verify a C2PA sigTst2 timestamp countersignature through b.tsa.verifyToken (full RFC 3161 CMS-signature + messageDigest + critical-sole-EKU check) — never a bespoke cert-chain-only walk on the timestamp token",
|
|
7950
|
-
regex: /(?:sigTst2|tstToken|tstContainer|CounterSignature)[\s\S]{0,
|
|
8018
|
+
regex: /(?:sigTst2|tstToken|tstContainer|CounterSignature)(?:(?!\n\}|\bverifyToken\s*\()[\s\S]){0,2000}?(?:\.checkIssued\s*\(|new\s+(?:nodeCrypto\.)?X509Certificate(?:(?!\n\})[\s\S]){0,400}?\.verify\s*\()/,
|
|
7951
8019
|
skipCommentLines: true,
|
|
7952
8020
|
allowlist: [],
|
|
7953
8021
|
reason: "CVE-2025-52556 (RFC 3161 timestamp-validation bypass) / CWE-347 (improper signature verification). The C2PA sigTst2 (RFC 9921) timestamp countersignature in lib/content-credentials.js MUST be verified through b.tsa.verifyToken, which performs the full RFC 3161 §2.4.2/§2.3 check (CMS signature over the signed attributes, messageDigest recompute, critical + sole id-kp-timeStamping EKU). A bespoke cert-chain-only check (checkIssued / X509Certificate(...).verify) on the timestamp token accepts a token whose CMS signature was never verified — a backdating / key-compromise forgery. The detector fires when a chain-walk appears near a timestamp token; route through tsa.verifyToken instead. Scoped to the timestamp context so the CAWG identity x509 chain (_verifyIdentityX509Chain) is not affected.",
|
|
@@ -8621,7 +8689,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
8621
8689
|
// Match `switch (alg) { ... default: return ... }` /
|
|
8622
8690
|
// `default: break` — the specific permissive shape. Throwing
|
|
8623
8691
|
// defaults pattern as `default:\s*throw` and are NOT matched.
|
|
8624
|
-
regex: /switch\s*\(\s*\w*[Aa]lg\w*\s*\)\s*\{[\s\S]{0,
|
|
8692
|
+
regex: /switch\s*\(\s*\w*[Aa]lg\w*\s*\)\s*\{(?:(?!\n\s{0,2}\}|\bdefault:)[\s\S]){0,6000}?default:\s*(?:return|break|\/\/[^\n]*\n\s*\})/,
|
|
8625
8693
|
allowlist: [
|
|
8626
8694
|
// sd-jwt-vc.js's _resolveSigAlgo DOES throw in the default
|
|
8627
8695
|
// branch, so it doesn't match. Other auth files use
|
|
@@ -8632,28 +8700,28 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
8632
8700
|
{
|
|
8633
8701
|
id: "inline-codepoint-class-table",
|
|
8634
8702
|
primitive: "codepointClass.BIDI_RE / C0_CTRL_RE / ZERO_WIDTH_RE / NULL_RE_G / hex4 / charClass / fromCp",
|
|
8635
|
-
regex: /var\s+BIDI_RANGES\s*=\s*\[\s*0x200E[\s\S]{0,
|
|
8703
|
+
regex: /var\s+BIDI_RANGES\s*=\s*\[\s*0x200E(?:(?!\n\})[\s\S]){0,2000}?new RegExp\(\s*["']\[["']\s*\+\s*charClass\(/,
|
|
8636
8704
|
allowlist: ["lib/codepoint-class.js"],
|
|
8637
8705
|
reason: "Extracted across guard-csv / guard-html / guard-svg. The BIDI_RANGES + C0_CTRL_RANGES + ZERO_WIDTH_RANGES literal tables plus the _hex4 / _charClass / _fromCp helpers plus the `new RegExp(\"[\" + _charClass(...) + \"]\")` regex compilations were identical across 3 guard primitives by design. Centralized so the codepoint catalog has a single source of truth and future guards (filename / archive / mime / ...) consume the shared module instead of re-defining the tables.",
|
|
8638
8706
|
},
|
|
8639
8707
|
{
|
|
8640
8708
|
id: "inline-resolve-profile-and-posture",
|
|
8641
8709
|
primitive: "gateContract.resolveProfileAndPosture(opts, { profiles, compliancePostures, defaults, errorClass, errCodePrefix })",
|
|
8642
|
-
regex: /typeof\s+opts\.profile\s*===\s*["']string["'][\s\S]{0,
|
|
8710
|
+
regex: /typeof\s+opts\.profile\s*===\s*["']string["'](?:(?!\n\})[\s\S]){0,1600}?compliancePosture(?:(?!\n\})[\s\S]){0,1600}?Object\.assign\(\{\}\s*,\s*[A-Z]+/,
|
|
8643
8711
|
allowlist: ["lib/gate-contract.js"],
|
|
8644
8712
|
reason: "Extracted across guard-csv / guard-html / guard-svg. Every guard primitive's _resolveOpts opens with the identical `if (opts.profile) overlay = PROFILES[opts.profile]; if (opts.compliancePosture) overlay = Object.assign(overlay, COMPLIANCE_POSTURES[...]); return Object.assign({}, DEFAULTS, overlay, opts);` cascade. Centralized in gateContract so future guards consume the shared resolver — keeps the family resolution shape identical across members.",
|
|
8645
8713
|
},
|
|
8646
8714
|
{
|
|
8647
8715
|
id: "inline-char-strip-policy-cascade",
|
|
8648
8716
|
primitive: "codepointClass.applyCharStripPolicies(text, opts)",
|
|
8649
|
-
regex: /opts\.bidiPolicy\s*===\s*["']strip["'][\s\S]{0,
|
|
8717
|
+
regex: /opts\.bidiPolicy\s*===\s*["']strip["'](?:(?!\n\})[\s\S]){0,800}?opts\.controlPolicy\s*===\s*["']strip["'](?:(?!\n\})[\s\S]){0,800}?opts\.nullBytePolicy/,
|
|
8650
8718
|
allowlist: ["lib/codepoint-class.js"],
|
|
8651
8719
|
reason: "Extracted across guard-html / guard-svg sanitize paths — the 4-line `if (opts.bidiPolicy === 'strip') s = s.replace(BIDI_RE_G, '')` cascade was identical. guard-csv uses different opt-name vocabulary (bidiCharPolicy / nullByteHandling) so it keeps its inline strip block; that's a single-vendor occurrence, below the duplicate-detector floor.",
|
|
8652
8720
|
},
|
|
8653
8721
|
{
|
|
8654
8722
|
id: "inline-detect-char-threats",
|
|
8655
8723
|
primitive: "codepointClass.detectCharThreats(text, opts, codePrefix)",
|
|
8656
|
-
regex: /var\s+bidiMatch\s*=\s*\w+\.match\(BIDI_RE\)[\s\S]{0,
|
|
8724
|
+
regex: /var\s+bidiMatch\s*=\s*\w+\.match\(BIDI_RE\)(?:(?!\n\})[\s\S]){0,1200}?bidi-override(?:(?!\n\})[\s\S]){0,1600}?nullBytePolicy(?:(?!\n\})[\s\S]){0,1200}?null-byte/,
|
|
8657
8725
|
allowlist: ["lib/codepoint-class.js"],
|
|
8658
8726
|
reason: "Extracted across guard-html / guard-svg detection passes — the bidi/null-byte/control-char issue-emit cascade was identical at the head of every _detectIssues. guard-csv keeps its inline form because it uses different opt-name vocabulary (bidiCharPolicy / nullByteHandling) and additionally classifies homoglyphs as a CSV-specific threat.",
|
|
8659
8727
|
},
|
|
@@ -8698,7 +8766,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
8698
8766
|
{
|
|
8699
8767
|
id: "inline-rule-pack-loader",
|
|
8700
8768
|
primitive: "gateContract.makeRulePackLoader(errorClass, codePrefix)",
|
|
8701
|
-
regex: /var\s+_\w*[Rr]ulePacks?\s*=\s*\{\}[\s\S]{0,80}function\s+loadRulePack\s*\(\s*pack\s*\)\s*\{[\s\S]{0,
|
|
8769
|
+
regex: /var\s+_\w*[Rr]ulePacks?\s*=\s*\{\}[\s\S]{0,80}function\s+loadRulePack\s*\(\s*pack\s*\)\s*\{(?:(?!\n\})[\s\S]){0,1200}?validateOpts\.requireObject(?:(?!\n\})[\s\S]){0,1200}?validateOpts\.requireNonEmptyString(?:(?!\n\})[\s\S]){0,1200}?_\w*[Rr]ulePacks?\[pack\.id\]\s*=\s*pack/,
|
|
8702
8770
|
allowlist: ["lib/gate-contract.js"],
|
|
8703
8771
|
reason: "Extracted across guard-csv / guard-html / guard-svg loadRulePack(pack) entry. Identical scaffolding (closed-over store + validateOpts cascade + pack.id keyed insert) consolidated into a closure factory.",
|
|
8704
8772
|
},
|
|
@@ -8712,9 +8780,9 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
8712
8780
|
{
|
|
8713
8781
|
id: "inline-build-guard-gate-forwarder",
|
|
8714
8782
|
primitive: "gateContract.buildGuardGate(name, opts, check)",
|
|
8715
|
-
regex: /forensicEvidenceStore:\s*opts\.forensicEvidenceStore[\s\S]{0,
|
|
8716
|
-
allowlist: ["lib/gate-contract.js"],
|
|
8717
|
-
reason: "Extracted across guard-csv / guard-html / guard-svg gate(opts) factories. Every guard's gate() body forwarded the same ~16-key opts bag (mode / audit / observability / forensicEvidenceStore / cache / hooks / runtime cap / ...) to gateContract.defineGate; centralized so each guard's gate() body is just the check function plus a label.",
|
|
8783
|
+
regex: /forensicEvidenceStore:\s*opts\.forensicEvidenceStore(?:(?!\n\s*\}|\bonAudit:)[\s\S]){0,2000}?onAudit:\s*opts\.onAudit/,
|
|
8784
|
+
allowlist: ["lib/gate-contract.js", "lib/guard-all.js"],
|
|
8785
|
+
reason: "Extracted across guard-csv / guard-html / guard-svg gate(opts) factories. Every guard's gate() body forwarded the same ~16-key opts bag (mode / audit / observability / forensicEvidenceStore / cache / hooks / runtime cap / ...) to gateContract.defineGate; centralized so each guard's gate() body is just the check function plus a label. guard-all.js is allowlisted on a different axis: _resolveActiveGuards assembles this same opts bag as the BASE that is then merged with per-guard overrides and fanned out to every member guard's already-built gate — it is the aggregate dispatcher's base-opts, not a single guard's gate forwarder, so buildGuardGate (which builds one gate) does not apply.",
|
|
8718
8786
|
},
|
|
8719
8787
|
{
|
|
8720
8788
|
id: "inline-bad-input-issue-result",
|
|
@@ -8733,7 +8801,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
8733
8801
|
{
|
|
8734
8802
|
id: "inline-issue-validator-entry",
|
|
8735
8803
|
primitive: "gateContract.runIssueValidator(input, opts, detector)",
|
|
8736
|
-
regex: /typeof\s+input\s*===\s*["']string["'][\s\S]{0,
|
|
8804
|
+
regex: /typeof\s+input\s*===\s*["']string["'](?:(?!\n\})[\s\S]){0,400}?Buffer\.isBuffer\(input\)(?:(?!\n\})[\s\S]){0,600}?bad-input(?:(?!\n\})[\s\S]){0,800}?return\s*\{(?:(?!\n\})[\s\S]){0,400}?ok:\s*!issues\.some/,
|
|
8737
8805
|
allowlist: ["lib/gate-contract.js"],
|
|
8738
8806
|
reason: "Extracted across guard-csv / guard-html validate() entry points. The string|Buffer normalization + bad-input fallback + issue-aggregation return shape was identical across guards; centralized into gate-contract. guard-svg keeps its inline form because SVGZ magic-byte detection needs the raw Buffer (utf8 conversion would lose the gzip header).",
|
|
8739
8807
|
},
|
|
@@ -8747,7 +8815,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
8747
8815
|
{
|
|
8748
8816
|
id: "inline-assert-no-char-threats",
|
|
8749
8817
|
primitive: "codepointClass.assertNoCharThreats(text, opts, errorFactory, codePrefix)",
|
|
8750
|
-
regex: /opts\.bidiPolicy\s*===\s*["']reject["'][\s\S]{0,
|
|
8818
|
+
regex: /opts\.bidiPolicy\s*===\s*["']reject["'](?:(?!\n\})[\s\S]){0,800}?BIDI_RE\.test(?:(?!\n\})[\s\S]){0,800}?opts\.nullBytePolicy\s*===\s*["']reject["']/,
|
|
8751
8819
|
allowlist: ["lib/codepoint-class.js"],
|
|
8752
8820
|
reason: "Extracted across guard-html / guard-svg sanitize entry — every guard's reject-on-character-class threats opens with the same `if (opts.bidiPolicy === 'reject' && BIDI_RE.test(s)) throw; if (opts.nullBytePolicy === 'reject' && s.indexOf(NULL_BYTE) !== -1) throw; if (opts.controlPolicy === 'reject' && C0_CTRL_RE.test(s)) throw;` cascade. Centralized so the reject-policy contract is identical across the family. guard-csv keeps its own inline cell-level reject for opt-name vocabulary reasons (bidiCharPolicy etc.).",
|
|
8753
8821
|
},
|
|
@@ -8761,7 +8829,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
8761
8829
|
{
|
|
8762
8830
|
id: "inline-observability-shape-validation",
|
|
8763
8831
|
primitive: "validateOpts.observabilityShape(observability, label, ErrorClass)",
|
|
8764
|
-
regex: /opts\.observability\s*!==\s*undefined\s*&&\s*opts\.observability\s*!==\s*null[\s\S]{0,
|
|
8832
|
+
regex: /opts\.observability\s*!==\s*undefined\s*&&\s*opts\.observability\s*!==\s*null(?:(?!\n\s*\})[\s\S]){0,1500}?event\s*!==\s*["']function["']/,
|
|
8765
8833
|
allowlist: [],
|
|
8766
8834
|
reason: "Extracted parallel to auditShape — opts.observability shape validation across i18n / cache / auth.lockout.",
|
|
8767
8835
|
},
|
|
@@ -8878,7 +8946,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
8878
8946
|
// !== "string" || opts.X[i].length === 0) throw }` — recurring across
|
|
8879
8947
|
// api-key (scopes), file-upload (allowedFileTypes), seeders (dependsOn),
|
|
8880
8948
|
// i18n (rtlLanguages / eagerLocales), and others.
|
|
8881
|
-
regex: /!\s*Array\.isArray\s*\(\s*\w+\.\w+\s*\)[\s\S]{0,
|
|
8949
|
+
regex: /!\s*Array\.isArray\s*\(\s*\w+\.\w+\s*\)(?:(?!\n\}|!\s*Array\.isArray)[\s\S]){0,3000}?typeof\s+\w+\.\w+\s*\[\s*\w+\s*\]\s*!==\s*["']string["']\s*\|\|\s*\w+\.\w+\s*\[\s*\w+\s*\]\.length\s*===\s*0/,
|
|
8882
8950
|
allowlist: ["lib/validate-opts.js"],
|
|
8883
8951
|
reason: "Extracted to validateOpts.optionalNonEmptyStringArray. Replaces the per-file `if (X !== undefined) { if (!Array.isArray) throw; for (i) if (typeof !== string || === '') throw }` cascade with one call.",
|
|
8884
8952
|
},
|
|
@@ -8954,7 +9022,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
8954
9022
|
primitive: "safeAsync.safeInvoke(callback, payload, onError?)",
|
|
8955
9023
|
// Detect the literal `onDrop({...})` call wrapped in try/catch — the
|
|
8956
9024
|
// shape every log-stream sink previously rolled by hand.
|
|
8957
|
-
regex: /try\s*\{\s*onDrop\s*\(\s*\{[\s\S]{0,
|
|
9025
|
+
regex: /try\s*\{\s*onDrop\s*\(\s*\{(?:(?!onDrop\s*\(|\}\s*\)\s*;?\s*\}\s*catch)[\s\S]){0,2000}?\}\s*\)\s*;?\s*\}\s*catch/,
|
|
8958
9026
|
allowlist: [],
|
|
8959
9027
|
reason: "Extracted to safeAsync.safeInvoke — operator-supplied callbacks must invoke through the framework wrapper so throws can't cascade into the sink's flush loop.",
|
|
8960
9028
|
},
|
|
@@ -8978,7 +9046,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
8978
9046
|
// Hard to match _STR contents post-tokenization; match the
|
|
8979
9047
|
// surrounding shape instead: a `try { ... "BEGIN" ... "COMMIT" ...
|
|
8980
9048
|
// } catch ... "ROLLBACK"` shape.
|
|
8981
|
-
regex: /"BEGIN"[\s\S]{0,
|
|
9049
|
+
regex: /"BEGIN"(?:(?!"BEGIN"|"ROLLBACK"|"COMMIT")[\s\S]){0,4000}?"COMMIT"(?:(?!"BEGIN"|"ROLLBACK")[\s\S]){0,1200}?\}\s*catch(?:(?!"BEGIN")[\s\S]){0,1200}?"ROLLBACK"/,
|
|
8982
9050
|
allowlist: [
|
|
8983
9051
|
"lib/db-schema.js", // definition site (runInTransaction itself)
|
|
8984
9052
|
// db.js's `transaction(fn)` is the framework's PUBLIC transaction
|
|
@@ -9006,7 +9074,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
9006
9074
|
// shape that every primitive's create() rolled by hand. Tokenized:
|
|
9007
9075
|
// `! _ID . _ID ( _ID . _ID ) ) { throw new _ID ( _STR , _STR + _ID . _ID ( _ID . _ID )`
|
|
9008
9076
|
// — the distinctive `+ nb.shape(opts.X)` tail fingerprints it.
|
|
9009
|
-
regex: /!\s*\w+\.is\w*FiniteInt\s*\(\s*\w+\.\w+\s*\)[\s\S]{0,
|
|
9077
|
+
regex: /!\s*\w+\.is\w*FiniteInt\s*\(\s*\w+\.\w+\s*\)(?:(?!\n {2}\}|\.shape\s*\()[\s\S]){0,800}?\w+\.shape\s*\(\s*\w+\.\w+\s*\)/,
|
|
9010
9078
|
allowlist: [
|
|
9011
9079
|
"lib/numeric-bounds.js", // definition site
|
|
9012
9080
|
// The helper signature is `new errorClass(code, message)`. Sites
|
|
@@ -9057,7 +9125,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
9057
9125
|
primitive: "safeAsync.makeScheduledFlush(delayMs, flushFn)",
|
|
9058
9126
|
// The literal `var flushTimer = null;` followed by setTimeout idempotent-schedule shape
|
|
9059
9127
|
// every batched-write sink previously rolled by hand.
|
|
9060
|
-
regex: /var\s+flushTimer\s*=\s*null\s*;[\s\S]{0,
|
|
9128
|
+
regex: /var\s+flushTimer\s*=\s*null\s*;(?:(?!\n\}|if\s*\(\s*flushTimer)[\s\S]){0,1200}?if\s*\(\s*flushTimer/,
|
|
9061
9129
|
allowlist: ["lib/safe-async.js"],
|
|
9062
9130
|
reason: "Extracted to safeAsync.makeScheduledFlush — idempotent setTimeout coalesce-and-flush helper used by every log-stream sink.",
|
|
9063
9131
|
},
|
|
@@ -9068,7 +9136,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
9068
9136
|
// instead of calling the framework helper. The shape is symmetric
|
|
9069
9137
|
// across every consumer module that needs hot-path emission with
|
|
9070
9138
|
// drop-silent semantics — extraction was complete, no allowlist.
|
|
9071
|
-
regex: /try\s*\{[\s\S]{0,
|
|
9139
|
+
regex: /try\s*\{(?:(?!\}\s*catch|observability\.event)[\s\S]){0,800}?observability\.event\s*\([^)]*\)\s*;?\s*\}\s*catch/,
|
|
9072
9140
|
allowlist: [],
|
|
9073
9141
|
reason: "Extracted to observability.safeEvent — drop-silent semantics for hot-path event emission. Any module wrapping observability.event in try/catch should call observability.safeEvent instead.",
|
|
9074
9142
|
},
|
|
@@ -9286,7 +9354,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
9286
9354
|
{
|
|
9287
9355
|
id: "mountinfo-options-bind-check",
|
|
9288
9356
|
primitive: "parse /proc/self/mountinfo field 4 (root within source FS) and check != \"/\" for bind detection",
|
|
9289
|
-
regex: /mountinfo[\s\S]{0,
|
|
9357
|
+
regex: /mountinfo(?:(?!\n\})[\s\S]){0,6000}?\boptions\b(?:(?!\n\})[\s\S]){0,400}?indexOf\(["']bind["']\)/,
|
|
9290
9358
|
allowlist: [],
|
|
9291
9359
|
reason: "Per Documentation/filesystems/proc.rst §3.5, /proc/self/mountinfo field 6 (mount options) does NOT carry a 'bind' tag — the kernel exposes bind-mount provenance via field 4 ('root within source filesystem'), which is '/' for a regular mount and the bound source path for a bind mount. Checking the options field for 'bind' never fires for actual bind mounts and silently misses the failure mode it claims to defend. Detector catches the mis-parse shape at n=1.",
|
|
9292
9360
|
},
|
|
@@ -9468,7 +9536,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
9468
9536
|
// entry point (function called with `({` opts-object first arg).
|
|
9469
9537
|
// The fix in mail-crypto-smime extracted `_verifySignerInfo(si, ...)`
|
|
9470
9538
|
// which takes the item positionally — that doesn't match the regex.
|
|
9471
|
-
regex: /for\s*\(\s*var\s+\w+\s*=\s*0[^)]*\.(?:signerInfos|signers|recipients|items|entries)\.length[^)]*\)\s*\{[\s\S]{0,
|
|
9539
|
+
regex: /(?:^|\n)([ \t]*)for\s*\(\s*var\s+\w+\s*=\s*0[^)]*\.(?:signerInfos|signers|recipients|items|entries)\.length[^)]*\)\s*\{(?:(?!\n\1\}|\bverify\s*\()[\s\S]){0,4000}?\bverify\s*\(\s*\{/,
|
|
9472
9540
|
allowlist: [
|
|
9473
9541
|
// mail-crypto-smime.js verifyAll was fixed v0.11.0 to call
|
|
9474
9542
|
// _verifySignerInfo(si, ...) (positional `si`), not
|
|
@@ -9631,7 +9699,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
9631
9699
|
id: "test-microtask-drain-loop-sleep",
|
|
9632
9700
|
primitive: "helpers.waitUntil(predicate, { timeoutMs, label }) — poll the observable condition; never drain a fixed count of microtasks/ticks by reassigning a promise to its own .then() in a loop",
|
|
9633
9701
|
scanScope: "test",
|
|
9634
|
-
regex: /\b(\w+)\s*=\s*\1\.then\([\s\S]{0,
|
|
9702
|
+
regex: /\b(\w+)\s*=\s*\1\.then\((?:(?!\.then\(|set(?:Immediate|Timeout)\s*\()[\s\S]){0,400}?set(?:Immediate|Timeout)\s*\(/,
|
|
9635
9703
|
skipCommentLines: true,
|
|
9636
9704
|
allowlist: [
|
|
9637
9705
|
// The catalog itself carries this pattern as a regex literal +
|
|
@@ -9761,7 +9829,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
9761
9829
|
primitive: "a bespoke b.db.init with a tmpDir must pass allowNonTmpfsTmpDir:true (as setupTestDb does), base the scratch on os.tmpdir(), or run atRest:\"plain\" — so the encrypted-at-rest non-tmpfs gate does not fail it on the Linux/macOS CI floor",
|
|
9762
9830
|
scanScope: "test",
|
|
9763
9831
|
skipCommentLines: true,
|
|
9764
|
-
regex: /\bb\.db\.init\s*\([\s\S]{0,
|
|
9832
|
+
regex: /\bb\.db\.init\s*\(\s*\{(?:(?!\}\s*\)|\bb\.db\.init\s*\()[\s\S]){0,4000}?\btmpDir\s*:/,
|
|
9765
9833
|
requires: /allowNonTmpfsTmpDir|atRest\s*:\s*["']plain["']|os\.tmpdir\s*\(/,
|
|
9766
9834
|
allowlist: [],
|
|
9767
9835
|
reason: "v0.15.0 — the encrypted-at-rest db.init disk-residency gate refuses a non-tmpfs tmpDir on Linux/macOS but is a no-op on win32, so a bespoke db.init with a repo-local (.test-output) scratch dir passes on Windows and fails on CI. setupTestDb / setupTestDbForMW already pass allowNonTmpfsTmpDir:true; a bespoke db.init must do the same, base its scratch on os.tmpdir() (/tmp = recognized tmpfs), or run atRest:\"plain\". The requires-marker confirms one mitigation is present in the file.",
|
|
@@ -9811,7 +9879,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
9811
9879
|
// closes those variants.
|
|
9812
9880
|
id: "map-get-falsy-then-set-pre-node-26",
|
|
9813
9881
|
primitive: "Node 26 `Map.prototype.getOrInsertComputed(key, factory)` collapses falsy-check + insert into one atomic call",
|
|
9814
|
-
regex: /if\s*\(\s*(?:!\s*\w+\.get\s*\([^)]+\)|\w+\.get\s*\([^)]+\)\s*===\s*(?:undefined|null))\s*\)\s*\{[\s\S]{0,
|
|
9882
|
+
regex: /(?:^|\n)([ \t]*)if\s*\(\s*(?:!\s*\w+\.get\s*\([^)]+\)|\w+\.get\s*\([^)]+\)\s*===\s*(?:undefined|null))\s*\)\s*\{(?:(?!\n\1\}|\.set\s*\()[\s\S]){0,3000}?\.set\s*\(/,
|
|
9815
9883
|
skipCommentLines: true,
|
|
9816
9884
|
allowlist: [],
|
|
9817
9885
|
reason: "Companion to map-has-then-set-pre-node-26 — same Node 26 getOrInsertComputed migration target, captures the `!M.get(k)` / `M.get(k) === undefined|null` syntactic variants. v0.11.3 audit identified the original map-has-then-set detector as bypassable by switching `.has(k)` to `.get(k)` falsy-check; this entry closes that gap.",
|
|
@@ -9827,7 +9895,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
9827
9895
|
// `lib/atomic-file.js` opens-by-fd as the canonical safe-read.
|
|
9828
9896
|
id: "fs-existssync-then-read-toctou",
|
|
9829
9897
|
primitive: "open-by-fd first, then operate on the fd; never check-then-read against the same path (CodeQL js/file-system-race)",
|
|
9830
|
-
regex: /\bfs\.(?:existsSync|statSync|lstatSync)\s*\(\s*(\w+)\s*\)[\s\S]{0,
|
|
9898
|
+
regex: /\bfs\.(?:existsSync|statSync|lstatSync)\s*\(\s*(\w+)\s*\)(?:(?!\n\}|\bfs\.(?:readFile|readFileSync|open|openSync|createReadStream|writeFile|writeFileSync)\s*\(\s*\1\b)[\s\S]){0,4000}?\bfs\.(?:readFile|readFileSync|open|openSync|createReadStream|writeFile|writeFileSync)\s*\(\s*\1\b/,
|
|
9831
9899
|
skipCommentLines: true,
|
|
9832
9900
|
allowlist: [
|
|
9833
9901
|
// The canonical safe-read primitive — its job IS the existsSync-
|
|
@@ -10027,7 +10095,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
10027
10095
|
// allowlist with a documented reason.
|
|
10028
10096
|
id: "safedecompress-omits-max-compressed-bytes",
|
|
10029
10097
|
primitive: "safeDecompress({ maxOutputBytes, maxCompressedBytes: <operator bound>, ... }) — align both caps with the caller's intent; never rely on the 4 MiB default when maxOutputBytes is operator-configurable",
|
|
10030
|
-
regex: /safeDecompress\s*\([\s\S]{0,
|
|
10098
|
+
regex: /safeDecompress\s*\((?:(?!;|maxOutputBytes\s*:)[\s\S]){0,3000}?maxOutputBytes\s*:/,
|
|
10031
10099
|
requires: /\bmaxCompressedBytes\b/,
|
|
10032
10100
|
skipCommentLines: true,
|
|
10033
10101
|
allowlist: [],
|
|
@@ -10135,7 +10203,7 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
10135
10203
|
// NOT this bug and does not match — neither shape names `sha3Hash`.
|
|
10136
10204
|
id: "derived-hash-handrolled-outside-crypto-field",
|
|
10137
10205
|
primitive: "cryptoField derived/lookup hashes — compute via _computeDerivedHash / b.cryptoField.computeNamespacedHash (honours salted-sha3 vs hmac-shake256 mode + vault.getDerivedHashMacKey); do not hand-roll sha3Hash(getDerivedHashSalt() + ns + value) at call sites",
|
|
10138
|
-
regex: /getDerivedHashSalt\s*\(\s*\)\s*\.toString\s*\(\s*["']hex["']\s*\)\s*\+|var\s+(\w+)\s*=\s*[\w.]*getDerivedHashSalt\s*\(\s*\)\s*;[\s\S]{0,
|
|
10206
|
+
regex: /getDerivedHashSalt\s*\(\s*\)\s*\.toString\s*\(\s*["']hex["']\s*\)\s*\+|var\s+(\w+)\s*=\s*[\w.]*getDerivedHashSalt\s*\(\s*\)\s*;(?:(?!\n\})[\s\S]){0,2000}?\b(\w+)\s*=\s*\1\s*\.toString\s*\(\s*["']hex["']\s*\)\s*;(?:(?!\n\})[\s\S]){0,2000}?sha3Hash\s*\(\s*\2\s*\+/,
|
|
10139
10207
|
skipCommentLines: true,
|
|
10140
10208
|
allowlist: [
|
|
10141
10209
|
// The canonical helper — _computeDerivedHash branches on mode here.
|
|
@@ -12286,6 +12354,48 @@ function testWikiPortAgreesAcrossArtifacts() {
|
|
|
12286
12354
|
bad);
|
|
12287
12355
|
}
|
|
12288
12356
|
|
|
12357
|
+
// The esbuild dev-tool version is pinned in THREE artifacts that carry no
|
|
12358
|
+
// lockfile to keep them in sync: package.json devDependencies (the
|
|
12359
|
+
// source-of-truth), ci.yml's exact `npm install esbuild@<v>` for the
|
|
12360
|
+
// bundler-output gate, and bundler-output.test.js's ESBUILD_BINARY_SHA256
|
|
12361
|
+
// map (the reviewed binary hashes, keyed by version). A bump that updates
|
|
12362
|
+
// one and not the others is the v0.11.40 silent-drift class: ci.yml tested
|
|
12363
|
+
// 0.28.0 while package.json declared 0.28.1, so CI verified an unreviewed
|
|
12364
|
+
// version. This check enforces 3-way agreement so the drift can't recur.
|
|
12365
|
+
function testEsbuildPinAgreesAcrossArtifacts() {
|
|
12366
|
+
var bad = [];
|
|
12367
|
+
var pkg;
|
|
12368
|
+
try { pkg = JSON.parse(fs.readFileSync("package.json", "utf8")); }
|
|
12369
|
+
catch (_e) { return; }
|
|
12370
|
+
var pkgVer = pkg.devDependencies && pkg.devDependencies.esbuild;
|
|
12371
|
+
if (!pkgVer) return;
|
|
12372
|
+
|
|
12373
|
+
var ci;
|
|
12374
|
+
try { ci = fs.readFileSync(".github/workflows/ci.yml", "utf8"); }
|
|
12375
|
+
catch (_e) { return; }
|
|
12376
|
+
var ciMatch = /esbuild@([0-9][^\s'"]*)/.exec(ci);
|
|
12377
|
+
if (ciMatch && ciMatch[1] !== pkgVer) {
|
|
12378
|
+
bad.push({ file: ".github/workflows/ci.yml", line: 0,
|
|
12379
|
+
content: "ci.yml installs esbuild@" + ciMatch[1] +
|
|
12380
|
+
" but package.json devDependencies pins esbuild " + pkgVer +
|
|
12381
|
+
" — CI would test an unreviewed version" });
|
|
12382
|
+
}
|
|
12383
|
+
|
|
12384
|
+
var bundlerTest;
|
|
12385
|
+
try { bundlerTest = fs.readFileSync("test/layer-5-integration/bundler-output.test.js", "utf8"); }
|
|
12386
|
+
catch (_e) { return; }
|
|
12387
|
+
if (bundlerTest.indexOf('"' + pkgVer + '":') === -1 && bundlerTest.indexOf("'" + pkgVer + "':") === -1) {
|
|
12388
|
+
bad.push({ file: "test/layer-5-integration/bundler-output.test.js", line: 0,
|
|
12389
|
+
content: "ESBUILD_BINARY_SHA256 has no reviewed-hash entry for esbuild " + pkgVer +
|
|
12390
|
+
" (package.json devDep) — re-review the published-tarball diff + add the binary hashes on bump" });
|
|
12391
|
+
}
|
|
12392
|
+
|
|
12393
|
+
bad = _filterMarkers(bad, "esbuild-pin-cross-artifact-drift");
|
|
12394
|
+
_report("esbuild pin agrees across package.json devDep + ci.yml install + bundler-output.test.js " +
|
|
12395
|
+
"binary-hash map (prevent a workflow / test pin silently drifting from the reviewed version)",
|
|
12396
|
+
bad);
|
|
12397
|
+
}
|
|
12398
|
+
|
|
12289
12399
|
// v1 — error codes are the operator-grep contract and must be
|
|
12290
12400
|
// `namespace/kebab-case`. The first string argument to `new XError(...)`
|
|
12291
12401
|
// and `XError.factory(...)` IS the code (defineClass constructor signature
|
|
@@ -13246,6 +13356,7 @@ async function run() {
|
|
|
13246
13356
|
// WIKI_PORT default must match the release-container.yml smoke
|
|
13247
13357
|
// step's port mapping + curl host.
|
|
13248
13358
|
testWikiPortAgreesAcrossArtifacts();
|
|
13359
|
+
testEsbuildPinAgreesAcrossArtifacts();
|
|
13249
13360
|
testNoTrackedInternalNotes();
|
|
13250
13361
|
testResidencyGatesWired();
|
|
13251
13362
|
testWikiStopGraceExceedsShutdownBudget();
|
|
@@ -85,6 +85,25 @@ async function _testVerifyVendorIntegrity() {
|
|
|
85
85
|
result && typeof result.ok === "boolean" && Array.isArray(result.mismatches));
|
|
86
86
|
check("configDrift.verifyVendorIntegrity: vendored files match manifest",
|
|
87
87
|
result.ok === true && result.mismatches.length === 0);
|
|
88
|
+
|
|
89
|
+
// #321: the check must be cwd-INDEPENDENT — per-file manifest paths resolve
|
|
90
|
+
// under the framework's vendor dir (or an explicit libVendorDir), not
|
|
91
|
+
// process.cwd(). Run it from a different working directory and it must still
|
|
92
|
+
// verify the actual loaded tree (the old code read-failed every entry, or
|
|
93
|
+
// under a crafted cwd could hash a different tree).
|
|
94
|
+
var origCwd = process.cwd();
|
|
95
|
+
var elsewhere = fs.mkdtempSync(path.join(os.tmpdir(), "config-drift-cwd-"));
|
|
96
|
+
try {
|
|
97
|
+
process.chdir(elsewhere);
|
|
98
|
+
var fromElsewhere = b.configDrift.verifyVendorIntegrity();
|
|
99
|
+
check("configDrift.verifyVendorIntegrity is cwd-independent (default vendor dir)",
|
|
100
|
+
fromElsewhere.ok === true &&
|
|
101
|
+
fromElsewhere.checkedCount === result.checkedCount &&
|
|
102
|
+
fromElsewhere.mismatches.length === 0);
|
|
103
|
+
} finally {
|
|
104
|
+
process.chdir(origCwd);
|
|
105
|
+
try { fs.rmSync(elsewhere, { recursive: true, force: true }); } catch (_e) { /* best-effort */ }
|
|
106
|
+
}
|
|
88
107
|
}
|
|
89
108
|
|
|
90
109
|
module.exports = { run: async function () { await run(); await _testVerifyVendorIntegrity(); } };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.cryptoField — read-side AAD-downgrade refusal (#28).
|
|
4
|
+
*
|
|
5
|
+
* An aad:true (or per-row-key) table seals each cell AEAD-bound to
|
|
6
|
+
* (table, row, column) so a ciphertext can't be relocated to another row or
|
|
7
|
+
* column. A PLAIN `vault:` envelope carries no AAD and IS relocatable — so if
|
|
8
|
+
* the read path accepted a plain cell on an aad table, a DB-write attacker
|
|
9
|
+
* could copy a plain cell from anywhere under the same vault root into an aad
|
|
10
|
+
* column and the read would silently surface it, defeating the cross-row /
|
|
11
|
+
* cross-column copy-protection the AAD binding advertises.
|
|
12
|
+
*
|
|
13
|
+
* unsealRow now refuses a plain cell on an aad/per-row-key table (nulls the
|
|
14
|
+
* field) unless the table opted into the documented pre-AAD migration window
|
|
15
|
+
* with registerTable({ allowPlainMigration: true }).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
var helpers = require("../helpers");
|
|
19
|
+
var b = helpers.b;
|
|
20
|
+
var check = helpers.check;
|
|
21
|
+
var fs = require("fs");
|
|
22
|
+
var os = require("os");
|
|
23
|
+
var path = require("path");
|
|
24
|
+
|
|
25
|
+
async function run() {
|
|
26
|
+
var tmp = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-cf-aaddt-"));
|
|
27
|
+
await b.vault.init({ dataDir: tmp, mode: "plaintext" });
|
|
28
|
+
|
|
29
|
+
// ---- aad:true table: plain cell refused on read ----
|
|
30
|
+
b.cryptoField.registerTable("aaddt_secure", {
|
|
31
|
+
aad: true, sealedFields: ["secret"], rowIdField: "id",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// A legitimately AAD-sealed row reads back.
|
|
35
|
+
var sealed = b.cryptoField.sealRow("aaddt_secure", { id: "row1", secret: "legit-value" });
|
|
36
|
+
var ok = b.cryptoField.unsealRow("aaddt_secure", Object.assign({}, sealed), { type: "system" });
|
|
37
|
+
check("aad: a legitimately AAD-sealed cell reads back", ok.secret === "legit-value");
|
|
38
|
+
|
|
39
|
+
// Forge a RELOCATED plain cell: a bare vault.seal envelope (no AAD) placed in
|
|
40
|
+
// the aad column — exactly what a DB-write attacker would copy in from
|
|
41
|
+
// elsewhere under the same vault root.
|
|
42
|
+
var plainEnvelope = b.vault.seal("relocated-attacker-value");
|
|
43
|
+
check("the forged cell is a plain vault: envelope (no AAD)",
|
|
44
|
+
typeof plainEnvelope === "string" && plainEnvelope.indexOf("vault.aad:") !== 0 &&
|
|
45
|
+
plainEnvelope.indexOf("vault:") === 0);
|
|
46
|
+
|
|
47
|
+
var forged = b.cryptoField.unsealRow("aaddt_secure",
|
|
48
|
+
{ id: "row1", secret: plainEnvelope }, { type: "system" });
|
|
49
|
+
check("aad-downgrade REFUSED: the relocated plain cell is NOT surfaced (field nulled)",
|
|
50
|
+
forged.secret !== "relocated-attacker-value");
|
|
51
|
+
check("aad-downgrade REFUSED: the field is null, not the attacker value",
|
|
52
|
+
forged.secret === null);
|
|
53
|
+
|
|
54
|
+
// ---- allowPlainMigration: the pre-AAD lazy-migration window accepts it ----
|
|
55
|
+
b.cryptoField.registerTable("aaddt_migrating", {
|
|
56
|
+
aad: true, sealedFields: ["secret"], rowIdField: "id", allowPlainMigration: true,
|
|
57
|
+
});
|
|
58
|
+
var migrated = b.cryptoField.unsealRow("aaddt_migrating",
|
|
59
|
+
{ id: "row1", secret: b.vault.seal("legacy-pre-aad-value") }, { type: "system" });
|
|
60
|
+
check("allowPlainMigration: a plain pre-AAD cell still reads (documented migration window)",
|
|
61
|
+
migrated.secret === "legacy-pre-aad-value");
|
|
62
|
+
|
|
63
|
+
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (_e) { /* best-effort */ }
|
|
64
|
+
|
|
65
|
+
// ---- the REAL consumer path: db.init({ schema }) must forward allowPlainMigration ----
|
|
66
|
+
// Applications register AAD tables declaratively through db.init, not a
|
|
67
|
+
// direct cryptoField.registerTable call. The migration opt-in has to survive
|
|
68
|
+
// that path — otherwise a schema declaring { aad: true, allowPlainMigration:
|
|
69
|
+
// true } registers with the default (false) and legacy plain cells are nulled
|
|
70
|
+
// despite the operator opting in.
|
|
71
|
+
var tmp2 = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-cf-apm-dbinit-"));
|
|
72
|
+
helpers.setTestPassphraseEnv();
|
|
73
|
+
await helpers.setupTestDb(tmp2, [{
|
|
74
|
+
name: "apm_via_schema",
|
|
75
|
+
columns: { id: "TEXT PRIMARY KEY", secret: "TEXT" },
|
|
76
|
+
aad: true,
|
|
77
|
+
sealedFields: ["secret"],
|
|
78
|
+
rowIdField: "id",
|
|
79
|
+
allowPlainMigration: true,
|
|
80
|
+
}]);
|
|
81
|
+
var sch = b.cryptoField.getSchema("apm_via_schema");
|
|
82
|
+
check("db.init schema path forwards allowPlainMigration to registerTable",
|
|
83
|
+
!!sch && sch.allowPlainMigration === true);
|
|
84
|
+
var viaSchema = b.cryptoField.unsealRow("apm_via_schema",
|
|
85
|
+
{ id: "row1", secret: b.vault.seal("legacy-via-db-init") }, { type: "system" });
|
|
86
|
+
check("allowPlainMigration honored through the db.init schema consumer path",
|
|
87
|
+
viaSchema.secret === "legacy-via-db-init");
|
|
88
|
+
|
|
89
|
+
await helpers.teardownTestDb(tmp2);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// No standalone CLI runner: this test drives db.init with a test passphrase,
|
|
93
|
+
// and a `console.error(e.stack)` failure footer is a clear-text-logging sink
|
|
94
|
+
// CodeQL flags on the (test-fixture) passphrase. The smoke runner invokes
|
|
95
|
+
// run() directly — matching db-key-aad.test.js and the other db.init tests.
|
|
96
|
+
module.exports = { run: run };
|