diff --git a/changelog.d/gst-850-allowlist-docs-paths.md b/changelog.d/gst-850-allowlist-docs-paths.md index 8893842a4..920502c2a 100644 --- a/changelog.d/gst-850-allowlist-docs-paths.md +++ b/changelog.d/gst-850-allowlist-docs-paths.md @@ -1,5 +1,5 @@ -- **The security class reaches outbound URLs, archives and every `child_process` spelling** (GST-850). `lib/review-risk.ts` now flags `fetch`, `http(s).request`, axios, got, undici, `requests` and `urlopen` on non-literal URLs, `new URL` from input, redirect toggles, proxy settings and host allowlists; tar, zip and unzip extraction, `path.join` or `resolve` on request or entry input, and `'..'` checks; and Node `exec` through named, default, namespace, split-line, `require` and dynamic imports. The split-line import scan runs in one forward pass. +- **The security class reaches outbound URLs, archives and every `child_process` spelling** (GST-850). `lib/review-risk.ts` now flags any call to an outbound HTTP client (`fetch`, `http(s).get` or `.request`, axios, got, undici, `requests`, `httpx`, urllib), `new URL` from input, redirect toggles, proxy settings and host allowlists; tar, zip and unzip extraction, `path.join` or `resolve` on request or entry input, and `'..'` checks; and any changed line that names the `child_process` module. The new rules parse no import clause or call arguments, and a test holds each under 100 ms on 200,000-character hostile lines. - **The money class reads metering and charge code.** Paths and lines naming meter, metering, metered, charge, billable, billed or credits, plus token usage, a billing proxy and cost or charge clients, now route to the money reader. gbrain-evals #289, which changed a metering proxy and billing clients and read `required=none`, is now a regression test that reads money. "costume", a CLI's usage text and a plain proxy do not count. - **When a diff publishes numbers, the batched specialist reads the write-up's text files** instead of leaving docs out of its diff. The collector records the files a diff creates. - **UI text and SVGs stay code (light).** An allowlist docs path for chart SVGs and locale values was built and dropped for review cost: its first review round found a new bypass class in each version. Every bypass found so far is a test row that classes as code. -- Replayed on the 26 GSTA-167 no-loss units, no unit loses a reviewer role. The replay checked which reviewers get dispatched, not R51's re-scored findings. Over the last 100 main commits, 2 move from light to full and no commit with a gate or risk class drops depth. +- Replayed on the 26 GSTA-167 no-loss units, no unit loses a reviewer role. The replay checked which reviewers get dispatched, not R51's re-scored findings. Over the last 100 main commits, 3 move from light to full and no commit with a gate or risk class drops depth. diff --git a/docs/review-policy.md b/docs/review-policy.md index 3b5fbd8a1..e93acbd84 100644 --- a/docs/review-policy.md +++ b/docs/review-policy.md @@ -215,9 +215,12 @@ input, redirect toggles, proxy settings and host allowlists, archive extraction and path traversal (`tar.x`, adm-zip, yauzl, extract-zip, `extractall`, `tar -x`, `unzip`, `path.join` or `resolve` on request or entry input, a `'..'` check added or removed), and any changed line that names the `child_process` or -`node:child_process` module, in any import or require spelling. These rules are -coarse on purpose: they parse no import clause or call arguments, and every -quantifier is bounded, so they run in linear time on any line. **data** is user data: a path naming pii, gdpr, ccpa, privacy, +`node:child_process` module, in any import or require spelling. The GST-850 +rules are coarse on purpose and parse no import clause or call arguments; +`test/review-risk-linear.test.ts` holds each of them under 100 ms on +200,000-character hostile lines, its anchor followed by a long run of the +characters it repeats included. That claim covers only the rules GST-850 adds, +not R51's older ones. **data** is user data: a path naming pii, gdpr, ccpa, privacy, personal, account, customer, cookie, persistence, storage or database, or a line that drops a table or column, deletes rows or bulk-deletes records. Auth scope, migrations and deleted code paths were already gate class. `RISK_RULES` hashes diff --git a/test/review-risk-linear.test.ts b/test/review-risk-linear.test.ts index 0eb665942..7caf71633 100644 --- a/test/review-risk-linear.test.ts +++ b/test/review-risk-linear.test.ts @@ -17,11 +17,16 @@ const scan = (lines: string[]) => matchRisk({ codePaths: ['src/a.ts'], added: { describe('linear time on hostile input', () => { // Value: protects=gstack-review-plan and the queue verdict never hang on a diff (the R51 resource regex did at about 50 // backslashes; GST-850 rounds 1 and 2 found quadratic import and exec-binding scans); fails_when=any risk pattern - // backtracks on a long or minified line; seam=every pattern in SECURITY_LINE, DATA_LINE, SECRET_LINE and MONEY_LINE, and matchRisk + // backtracks on a long or minified line, or on its anchor followed by a long run of the class it repeats; seam=every + // pattern in SECURITY_LINE, DATA_LINE, SECRET_LINE and MONEY_LINE (R51's rm rule is a known pre-existing issue, filed apart), and matchRisk const L = 200_000; const fill = (s: string) => s.repeat(Math.ceil(L / s.length)); const hostile = [' ', '(', "'", '"', 'a', '{', '\\', '..', 'import a, {', 'fetch(', 'path.join(', 'rmSync(', ' exec', 'new URL("', 'child_process', 'http.get(', 'requests.', - 'tar -', 'unzip ', 'DELETE FROM ', 'delete from a ', 'process.env.', 'x_usd', 'aUsd'].map(fill).concat([' exec' + ' '.repeat(L) + '// c', 'import a, {' + 'b,'.repeat(L / 2)]); + 'tar -', 'unzip ', 'DELETE FROM ', 'delete from a ', 'process.env.', 'x_usd', 'aUsd'].map(fill).concat([' exec' + ' '.repeat(L) + '// c', 'import a, {' + 'b,'.repeat(L / 2)], + ['fetch', 'globalThis.fetch', 'http.get', 'https.request', 'got', 'urlopen', 'urljoin', 'url.resolve', 'exec', 'execSync', 'cp.exec', 'proxy', 'redirect', 'maxRedirects', 'tar.extract', 'unzip', 'tar'].map(a => a + ' '.repeat(L)), + ['got.', 'requests.', 'httpx.', 'tar -', 'tar x', 'rm -', 'allowed_', 'host_allow', 'node_modules/adm-'].map(a => a + 'x'.repeat(L)), + ['new' + ' '.repeat(L) + 'URL(', 'new URL(' + ' '.repeat(L), 'new URL("' + 'a'.repeat(L), "'" + 'node:'.repeat(L / 5), 'path.join(' + 'a'.repeat(L), 'path.join(' + ')'.repeat(L), + "includes(" + ' '.repeat(L), '==' + ' '.repeat(L) + "'..'", "'..'" + ' '.repeat(L) + '==', '\\.' + '\\'.repeat(L)]); test.each(Object.entries({ SECURITY_LINE, DATA_LINE, SECRET_LINE, MONEY_LINE }))('every %s pattern finishes under 100 ms on each 200k-character hostile line', (_n, patterns) => { const slow = patterns.flatMap(r => hostile.map((s, k) => [r.source.slice(0, 40), k, fast(() => r.test(s))] as const)).filter(([, , ms]) => ms >= 100); expect(slow).toEqual([]);