From f34af2b618590c01e9c552a1758f4b11a1fb02f9 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 17 Aug 2026 11:41:51 -0700 Subject: [PATCH 1/5] fix: redact non-SQL content echoed in safety threat messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The main-only Sanity (Verdaccio) security test 6/15 (`altimate check ../../../../etc/passwd` must not reflect file contents) fails since #1090: surfacing real `ThreatFinding` messages exposed that the engine's `multi_statement` rule quotes the raw "statement type" token verbatim — for non-SQL input that is the file's content (`Disallowed statement type: ROOT:X:0:0:…`), an information-disclosure echo. The sanity job is skipped on PRs, so #1090's CI could not catch it. Fix at the dispatcher boundary (covers CLI, tools, and review): - `EngineCoerce.redactThreatText`: keeps SQL-keyword-like statement types (`DROP`, `ALTER TABLE`) and replaces arbitrary content with `` in threat message/detail. - `multi_statement` threats also redact `matched_pattern` — for that rule the pattern IS the raw input line. Injection rules keep their SQL-shaped patterns. Diff-scoping keys stay consistent (base and head redact identically). - Applied in the `altimate_core.safety` handler and the composite check. Tests: unit coverage for the redaction (keyword kept / content redacted / unrelated messages untouched) plus real-engine tests proving passwd-like input no longer echoes through either handler while `DROP` survives in messages. Verified the exact sanity scenario locally: CLI output now shows ``. Filed upstream for the engine-side echo: to be linked. Closes #1110 Co-Authored-By: Claude Fable 5 --- .../src/altimate/native/altimate-core.ts | 18 ++++- .../src/altimate/native/engine-coerce.ts | 19 +++++ .../test/altimate/threat-redaction.test.ts | 71 +++++++++++++++++++ 3 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 packages/opencode/test/altimate/threat-redaction.test.ts diff --git a/packages/opencode/src/altimate/native/altimate-core.ts b/packages/opencode/src/altimate/native/altimate-core.ts index 780f4ce21..20675dbac 100644 --- a/packages/opencode/src/altimate/native/altimate-core.ts +++ b/packages/opencode/src/altimate/native/altimate-core.ts @@ -42,6 +42,20 @@ function fail(error: unknown): AltimateCoreResult { return { success: false, data: {}, error: String(error) } } +/** Redact raw-input echoes from a scan result's threats (see EngineCoerce.redactThreatText). */ +function redactScan(scan: Record): Record { + const threats = (scan.threats as any[] | undefined)?.map((t: any) => ({ + ...t, + message: typeof t.message === "string" ? EngineCoerce.redactThreatText(t.message) : t.message, + detail: typeof t.detail === "string" ? EngineCoerce.redactThreatText(t.detail) : t.detail, + // For multi_statement the matched pattern IS the raw input line — an + // arbitrary-file content echo. Injection rules keep their SQL-shaped + // patterns (useful, and inherently query-derived). + matched_pattern: t.rule === "multi_statement" ? "" : t.matched_pattern, + })) + return threats ? { ...scan, threats } : scan +} + // --------------------------------------------------------------------------- // IFF / QUALIFY transpile transforms (ported from Python guard.py) // --------------------------------------------------------------------------- @@ -114,7 +128,7 @@ export function registerAll(): void { register("altimate_core.safety", async (params) => { try { const raw = core.scanSql(params.sql) - const data = toData(raw) + const data = redactScan(toData(raw)) return ok(true, data) } catch (e) { return fail(e) @@ -193,7 +207,7 @@ export function registerAll(): void { // occurrence, so a PR that ADDS a second identical injection still // reports it. Recompute safe/risk_score from the surviving threats so a // fully pre-existing threat set doesn't leave a stale unsafe verdict. - let safety: Record = toData(core.scanSql(params.sql)) + let safety: Record = redactScan(toData(core.scanSql(params.sql))) if (params.base_sql) { try { const baseCounts = new Map() diff --git a/packages/opencode/src/altimate/native/engine-coerce.ts b/packages/opencode/src/altimate/native/engine-coerce.ts index b72ffae04..552f92e66 100644 --- a/packages/opencode/src/altimate/native/engine-coerce.ts +++ b/packages/opencode/src/altimate/native/engine-coerce.ts @@ -59,4 +59,23 @@ export function piiColumnsFromReport(piiData: unknown): Array c && c.classification !== "None") } +/** + * Redact raw input echoed inside engine threat messages. + * + * The engine's `multi_statement` rule quotes the offending "statement type" + * verbatim — for non-SQL input (e.g. `altimate check /etc/passwd`) that + * reflects arbitrary file content back into CLI/tool output. Keep the token + * only when it looks like a SQL keyword phrase; otherwise redact. + */ +const SQL_KEYWORD_LIKE = /^[A-Za-z_][A-Za-z0-9_$ ]{0,31}$/ +export function redactThreatText(text: string): string { + return text + .replace(/(Disallowed statement type: )(.+)$/i, (_m, prefix: string, tok: string) => + SQL_KEYWORD_LIKE.test(tok.trim()) ? `${prefix}${tok}` : `${prefix}`, + ) + .replace(/(Statement type ')([^']*)(')/i, (_m, a: string, tok: string, c: string) => + SQL_KEYWORD_LIKE.test(tok) ? `${a}${tok}${c}` : `${a}${c}`, + ) +} + export * as EngineCoerce from "./engine-coerce" diff --git a/packages/opencode/test/altimate/threat-redaction.test.ts b/packages/opencode/test/altimate/threat-redaction.test.ts new file mode 100644 index 000000000..0c37c93b8 --- /dev/null +++ b/packages/opencode/test/altimate/threat-redaction.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, test, beforeAll } from "bun:test" +import { EngineCoerce } from "../../src/altimate/native/engine-coerce" + +let coreAvailable = false +try { + require.resolve("@altimateai/altimate-core") + coreAvailable = true +} catch {} +const describeIf = coreAvailable ? describe : describe.skip + +describe("redactThreatText", () => { + test("keeps SQL-keyword-like statement types", () => { + expect(EngineCoerce.redactThreatText("Disallowed statement type: DROP")).toBe("Disallowed statement type: DROP") + expect(EngineCoerce.redactThreatText("Statement type 'ALTER TABLE' is not in the allowed list")).toBe( + "Statement type 'ALTER TABLE' is not in the allowed list", + ) + }) + + test("redacts non-SQL content (e.g. /etc/passwd lines)", () => { + const msg = "Disallowed statement type: ROOT:X:0:0:ROOT:/ROOT:/BIN/BASH" + expect(EngineCoerce.redactThreatText(msg)).toBe("Disallowed statement type: ") + const detail = "Statement type 'ROOT:X:0:0:ROOT:/ROOT:/BIN/BASH' is not in the allowed list: [\"SELECT\"]" + expect(EngineCoerce.redactThreatText(detail)).toContain("''") + expect(EngineCoerce.redactThreatText(detail)).not.toContain("ROOT:X:0") + }) + + test("leaves unrelated messages untouched", () => { + expect(EngineCoerce.redactThreatText("Tautology attack detected (numeric constant comparison)")).toBe( + "Tautology attack detected (numeric constant comparison)", + ) + }) +}) + +// Sanity security test [6/15] regression: `altimate check /etc/passwd` must not +// reflect file contents in threat messages (found by the main-only Verdaccio +// sanity job after PR #1090 surfaced real threat messages). +describeIf("safety handlers redact raw-input echoes (real engine)", () => { + beforeAll(async () => { + process.env.ALTIMATE_TELEMETRY_DISABLED = "true" + const { registerAll } = await import("../../src/altimate/native/altimate-core") + registerAll() + }) + + const PASSWD = "root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n" + + test("altimate_core.safety does not echo non-SQL file content", async () => { + const { Dispatcher } = await import("../../src/altimate/native") + const r = await Dispatcher.call("altimate_core.safety", { sql: PASSWD }) + const text = JSON.stringify((r.data as any).threats ?? []) + expect(text.toLowerCase()).not.toContain("root:x:0") + expect(text).toContain("redacted") + }) + + test("composite check does not echo non-SQL file content in threats", async () => { + const { Dispatcher } = await import("../../src/altimate/native") + const r = await Dispatcher.call("altimate_core.check", { sql: PASSWD }) + const threats = JSON.stringify(((r.data as any).safety?.threats ?? [])) + expect(threats.toLowerCase()).not.toContain("root:x:0") + }) + + test("real SQL statement types are preserved in messages", async () => { + const { Dispatcher } = await import("../../src/altimate/native") + const r = await Dispatcher.call("altimate_core.safety", { sql: "SELECT 1; DROP TABLE users;" }) + const threats = ((r.data as any).threats ?? []) as any[] + const messages = threats.map((t) => `${t.message} ${t.detail}`).join(" ") + // SQL-keyword statement types stay useful in messages/details + // (matched_pattern is always redacted for multi_statement by design). + expect(messages).toContain("DROP") + expect(messages).not.toContain("redacted") + }) +}) From c9a15977093eef69a5635dd2c05529e444bf212d Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 17 Aug 2026 11:48:41 -0700 Subject: [PATCH 2/5] fix: diff-scope safety on raw scans, redact after subtraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cursor review catch: redacting the head scan BEFORE the base subtraction rewrote `multi_statement` matched_patterns to "", so base keys (raw) never matched and pre-existing threats resurfaced as newly introduced. Redaction now runs after diff-scoping — subtraction compares raw engine patterns on both sides. Regression test: identical base/head passwd-like input yields zero threats and safe:true. Co-Authored-By: Claude Fable 5 --- .../opencode/src/altimate/native/altimate-core.ts | 6 +++++- .../opencode/test/altimate/threat-redaction.test.ts | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/altimate/native/altimate-core.ts b/packages/opencode/src/altimate/native/altimate-core.ts index 20675dbac..c89421492 100644 --- a/packages/opencode/src/altimate/native/altimate-core.ts +++ b/packages/opencode/src/altimate/native/altimate-core.ts @@ -207,7 +207,7 @@ export function registerAll(): void { // occurrence, so a PR that ADDS a second identical injection still // reports it. Recompute safe/risk_score from the surviving threats so a // fully pre-existing threat set doesn't leave a stale unsafe verdict. - let safety: Record = redactScan(toData(core.scanSql(params.sql))) + let safety: Record = toData(core.scanSql(params.sql)) if (params.base_sql) { try { const baseCounts = new Map() @@ -243,6 +243,10 @@ export function registerAll(): void { // Unscannable base — keep the full head scan (fail open to MORE findings). } } + // Redact AFTER diff-scoping: subtraction must compare raw engine + // matched_patterns on both sides (redacting first would rewrite head + // multi_statement keys to "" and never match the base). + safety = redactScan(safety) // PII exposure for the composite check — the tool has always rendered a // PII section; previously nothing populated it. Additive: a PII failure // must not fail the whole composite. diff --git a/packages/opencode/test/altimate/threat-redaction.test.ts b/packages/opencode/test/altimate/threat-redaction.test.ts index 0c37c93b8..9f5711aa7 100644 --- a/packages/opencode/test/altimate/threat-redaction.test.ts +++ b/packages/opencode/test/altimate/threat-redaction.test.ts @@ -58,6 +58,17 @@ describeIf("safety handlers redact raw-input echoes (real engine)", () => { expect(threats.toLowerCase()).not.toContain("root:x:0") }) + test("diff-scoping still filters pre-existing multi_statement threats despite redaction", async () => { + // Regression (cursor review on #1111): redacting BEFORE base subtraction + // rewrote head matched_patterns to "" so base keys never matched + // and pre-existing threats resurfaced as introduced. Redaction now runs + // after subtraction: identical base/head must yield zero threats. + const { Dispatcher } = await import("../../src/altimate/native") + const r = await Dispatcher.call("altimate_core.check", { sql: PASSWD, base_sql: PASSWD }) + expect((((r.data as any).safety?.threats ?? []) as any[]).length).toBe(0) + expect((r.data as any).safety.safe).toBe(true) + }) + test("real SQL statement types are preserved in messages", async () => { const { Dispatcher } = await import("../../src/altimate/native") const r = await Dispatcher.call("altimate_core.safety", { sql: "SELECT 1; DROP TABLE users;" }) From 7b3920491b47ea838af694b77267b3ba6cd47568 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 17 Aug 2026 12:00:55 -0700 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20harden=20threat=20redaction=20?= =?UTF-8?q?=E2=80=94=20keyword=20allowlist,=20greedy=20quotes,=20all=20con?= =?UTF-8?q?sumers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-2 review hardening on the redaction: - Keyword ALLOWLIST, not shape: the token is kept only when its first word is a known SQL statement keyword — shape alone passed content like "TOP SECRET PASSWORD" or "AKIAIOSFODNN7EXAMPLE". - Greedy quoted match: an embedded apostrophe in the raw token ("Statement type 'DROP'ROOT:X:0' is not…") previously closed the match early and leaked the remainder; greedy capture spans to the last quote and embedded quotes fail the check, redacting everything inside. - All scanSql consumers covered: `redactScan` moved to `EngineCoerce` (pure, no NAPI import) and applied in `sql.analyze` (copied raw message/detail into issues) and the `altimate_core.grade` handler (EvalResult embeds a full safety scan that the CLI grade check renders). - Tests: allowlist/apostrophe cases; composite + safety tests assert success AND non-empty threats AND the redaction marker (previously vacuous if threats were missing); sql.analyze + grade echo tests; Dispatcher.reset() and telemetry-env restore in afterAll; shared dispatcher import hoisted into beforeAll. Co-Authored-By: Claude Fable 5 --- .../src/altimate/native/altimate-core.ts | 26 ++---- .../src/altimate/native/engine-coerce.ts | 57 ++++++++++--- .../src/altimate/native/sql/register.ts | 3 +- .../test/altimate/threat-redaction.test.ts | 82 +++++++++++++++---- 4 files changed, 126 insertions(+), 42 deletions(-) diff --git a/packages/opencode/src/altimate/native/altimate-core.ts b/packages/opencode/src/altimate/native/altimate-core.ts index c89421492..84217553b 100644 --- a/packages/opencode/src/altimate/native/altimate-core.ts +++ b/packages/opencode/src/altimate/native/altimate-core.ts @@ -42,20 +42,6 @@ function fail(error: unknown): AltimateCoreResult { return { success: false, data: {}, error: String(error) } } -/** Redact raw-input echoes from a scan result's threats (see EngineCoerce.redactThreatText). */ -function redactScan(scan: Record): Record { - const threats = (scan.threats as any[] | undefined)?.map((t: any) => ({ - ...t, - message: typeof t.message === "string" ? EngineCoerce.redactThreatText(t.message) : t.message, - detail: typeof t.detail === "string" ? EngineCoerce.redactThreatText(t.detail) : t.detail, - // For multi_statement the matched pattern IS the raw input line — an - // arbitrary-file content echo. Injection rules keep their SQL-shaped - // patterns (useful, and inherently query-derived). - matched_pattern: t.rule === "multi_statement" ? "" : t.matched_pattern, - })) - return threats ? { ...scan, threats } : scan -} - // --------------------------------------------------------------------------- // IFF / QUALIFY transpile transforms (ported from Python guard.py) // --------------------------------------------------------------------------- @@ -128,7 +114,7 @@ export function registerAll(): void { register("altimate_core.safety", async (params) => { try { const raw = core.scanSql(params.sql) - const data = redactScan(toData(raw)) + const data = EngineCoerce.redactScan(toData(raw)) return ok(true, data) } catch (e) { return fail(e) @@ -246,7 +232,7 @@ export function registerAll(): void { // Redact AFTER diff-scoping: subtraction must compare raw engine // matched_patterns on both sides (redacting first would rewrite head // multi_statement keys to "" and never match the base). - safety = redactScan(safety) + safety = EngineCoerce.redactScan(safety) // PII exposure for the composite check — the tool has always rendered a // PII section; previously nothing populated it. Additive: a PII failure // must not fail the whole composite. @@ -488,7 +474,13 @@ export function registerAll(): void { try { const schema = schemaOrEmpty(params.schema_path, params.schema_context) const raw = await core.evaluate(params.sql, schema) - return ok(true, toData(raw)) + const data = toData(raw) + // EvalResult embeds a full safety scan — redact its threat echoes too + // (the CLI grade check renders nested threat messages). + if (data.safety && typeof data.safety === "object") { + data.safety = EngineCoerce.redactScan(data.safety as Record) + } + return ok(true, data) } catch (e) { return fail(e) } diff --git a/packages/opencode/src/altimate/native/engine-coerce.ts b/packages/opencode/src/altimate/native/engine-coerce.ts index 552f92e66..4df5399b3 100644 --- a/packages/opencode/src/altimate/native/engine-coerce.ts +++ b/packages/opencode/src/altimate/native/engine-coerce.ts @@ -64,18 +64,55 @@ export function piiColumnsFromReport(piiData: unknown): Array - SQL_KEYWORD_LIKE.test(tok.trim()) ? `${prefix}${tok}` : `${prefix}`, - ) - .replace(/(Statement type ')([^']*)(')/i, (_m, a: string, tok: string, c: string) => - SQL_KEYWORD_LIKE.test(tok) ? `${a}${tok}${c}` : `${a}${c}`, - ) + return ( + text + .replace(/(Disallowed statement type: )(.+)$/i, (_m, prefix: string, tok: string) => + isSqlStatementType(tok) ? `${prefix}${tok}` : `${prefix}${REDACTED}`, + ) + // GREEDY quoted match — an attacker apostrophe inside the token would + // otherwise close the match early and leak the remainder + // (e.g. "Statement type 'DROP'ROOT:X:0' is not…"). Greedy capture spans + // to the LAST quote; embedded quotes fail the shape check and redact. + .replace(/(Statement type ')(.*)(')/i, (_m, a: string, tok: string, c: string) => + isSqlStatementType(tok) ? `${a}${tok}${c}` : `${a}${REDACTED}${c}`, + ) + ) +} + +/** + * Redact raw-input echoes from a scan result's threats. For `multi_statement` + * the matched pattern IS the raw input line, so it is always redacted; + * injection rules keep their SQL-shaped patterns. Pure record manipulation — + * safe to use from any consumer without loading the NAPI binding. + */ +export function redactScan(scan: Record): Record { + const threats = (scan.threats as any[] | undefined)?.map((t: any) => ({ + ...t, + message: typeof t.message === "string" ? redactThreatText(t.message) : t.message, + detail: typeof t.detail === "string" ? redactThreatText(t.detail) : t.detail, + matched_pattern: t.rule === "multi_statement" ? "" : t.matched_pattern, + })) + return threats ? { ...scan, threats } : scan } export * as EngineCoerce from "./engine-coerce" diff --git a/packages/opencode/src/altimate/native/sql/register.ts b/packages/opencode/src/altimate/native/sql/register.ts index 443d54ddf..0d3a76c24 100644 --- a/packages/opencode/src/altimate/native/sql/register.ts +++ b/packages/opencode/src/altimate/native/sql/register.ts @@ -38,7 +38,8 @@ export function registerAllSql(): void { const lint = JSON.parse(JSON.stringify(lintRaw)) const semantics = JSON.parse(JSON.stringify(semanticsRaw)) - const safety = JSON.parse(JSON.stringify(safetyRaw)) + // Redact raw-input echoes (multi_statement quotes the input verbatim). + const safety = EngineCoerce.redactScan(JSON.parse(JSON.stringify(safetyRaw))) as any const issues: SqlAnalyzeIssue[] = [] diff --git a/packages/opencode/test/altimate/threat-redaction.test.ts b/packages/opencode/test/altimate/threat-redaction.test.ts index 9f5711aa7..7d65c975d 100644 --- a/packages/opencode/test/altimate/threat-redaction.test.ts +++ b/packages/opencode/test/altimate/threat-redaction.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test, beforeAll } from "bun:test" +import { describe, expect, test, beforeAll, afterAll } from "bun:test" import { EngineCoerce } from "../../src/altimate/native/engine-coerce" let coreAvailable = false @@ -9,7 +9,7 @@ try { const describeIf = coreAvailable ? describe : describe.skip describe("redactThreatText", () => { - test("keeps SQL-keyword-like statement types", () => { + test("keeps SQL-statement-keyword types", () => { expect(EngineCoerce.redactThreatText("Disallowed statement type: DROP")).toBe("Disallowed statement type: DROP") expect(EngineCoerce.redactThreatText("Statement type 'ALTER TABLE' is not in the allowed list")).toBe( "Statement type 'ALTER TABLE' is not in the allowed list", @@ -24,6 +24,28 @@ describe("redactThreatText", () => { expect(EngineCoerce.redactThreatText(detail)).not.toContain("ROOT:X:0") }) + test("redacts keyword-shaped but non-SQL content (allowlist, not shape)", () => { + // Shape alone would pass these — the first word must be a SQL statement keyword. + expect(EngineCoerce.redactThreatText("Disallowed statement type: TOP SECRET PASSWORD")).toBe( + "Disallowed statement type: ", + ) + expect(EngineCoerce.redactThreatText("Disallowed statement type: AKIAIOSFODNN7EXAMPLE")).toBe( + "Disallowed statement type: ", + ) + expect(EngineCoerce.redactThreatText("Statement type 'PRIVATE NOTE' is not in the allowed list")).toBe( + "Statement type '' is not in the allowed list", + ) + }) + + test("embedded apostrophes cannot close the quoted match early", () => { + // Greedy capture spans to the LAST quote; the embedded quote fails the + // shape check and everything inside is redacted. + const attack = "Statement type 'DROP'ROOT:X:0:0' is not in the allowed list" + const out = EngineCoerce.redactThreatText(attack) + expect(out).not.toContain("ROOT:X:0") + expect(out).toContain("") + }) + test("leaves unrelated messages untouched", () => { expect(EngineCoerce.redactThreatText("Tautology attack detected (numeric constant comparison)")).toBe( "Tautology attack detected (numeric constant comparison)", @@ -34,28 +56,62 @@ describe("redactThreatText", () => { // Sanity security test [6/15] regression: `altimate check /etc/passwd` must not // reflect file contents in threat messages (found by the main-only Verdaccio // sanity job after PR #1090 surfaced real threat messages). -describeIf("safety handlers redact raw-input echoes (real engine)", () => { +describeIf("safety consumers redact raw-input echoes (real engine)", () => { + let D: typeof import("../../src/altimate/native/dispatcher") + let priorTelemetry: string | undefined + beforeAll(async () => { + priorTelemetry = process.env.ALTIMATE_TELEMETRY_DISABLED process.env.ALTIMATE_TELEMETRY_DISABLED = "true" const { registerAll } = await import("../../src/altimate/native/altimate-core") + const { registerAllSql } = await import("../../src/altimate/native/sql/register") registerAll() + registerAllSql() + D = await import("../../src/altimate/native/dispatcher") + }) + + afterAll(async () => { + if (priorTelemetry === undefined) delete process.env.ALTIMATE_TELEMETRY_DISABLED + else process.env.ALTIMATE_TELEMETRY_DISABLED = priorTelemetry + // registerAll mutates shared Dispatcher state — reset for test isolation. + ;(await import("../../src/altimate/native/dispatcher")).reset() }) const PASSWD = "root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n" test("altimate_core.safety does not echo non-SQL file content", async () => { - const { Dispatcher } = await import("../../src/altimate/native") - const r = await Dispatcher.call("altimate_core.safety", { sql: PASSWD }) - const text = JSON.stringify((r.data as any).threats ?? []) + const r = await D.call("altimate_core.safety", { sql: PASSWD }) + expect(r.success).toBe(true) + const threats = ((r.data as any).threats ?? []) as any[] + expect(threats.length).toBeGreaterThan(0) + const text = JSON.stringify(threats) expect(text.toLowerCase()).not.toContain("root:x:0") expect(text).toContain("redacted") }) test("composite check does not echo non-SQL file content in threats", async () => { - const { Dispatcher } = await import("../../src/altimate/native") - const r = await Dispatcher.call("altimate_core.check", { sql: PASSWD }) - const threats = JSON.stringify(((r.data as any).safety?.threats ?? [])) - expect(threats.toLowerCase()).not.toContain("root:x:0") + const r = await D.call("altimate_core.check", { sql: PASSWD }) + expect(r.success).toBe(true) + // The sanitizer must actually be exercised — an empty threat list would + // make the not-contains assertion vacuous. + const threats = (((r.data as any).safety?.threats ?? []) as any[]) + expect(threats.length).toBeGreaterThan(0) + const text = JSON.stringify(threats) + expect(text.toLowerCase()).not.toContain("root:x:0") + expect(text).toContain("redacted") + }) + + test("sql.analyze does not echo non-SQL file content in issues", async () => { + const r = await D.call("sql.analyze", { sql: PASSWD }) + const text = JSON.stringify((r as any).issues ?? []) + expect(text.toLowerCase()).not.toContain("root:x:0") + }) + + test("altimate_core.grade nested safety threats are redacted", async () => { + const r = await D.call("altimate_core.grade", { sql: PASSWD }) + expect(r.success).toBe(true) + const text = JSON.stringify(((r.data as any).safety?.threats ?? [])) + expect(text.toLowerCase()).not.toContain("root:x:0") }) test("diff-scoping still filters pre-existing multi_statement threats despite redaction", async () => { @@ -63,15 +119,13 @@ describeIf("safety handlers redact raw-input echoes (real engine)", () => { // rewrote head matched_patterns to "" so base keys never matched // and pre-existing threats resurfaced as introduced. Redaction now runs // after subtraction: identical base/head must yield zero threats. - const { Dispatcher } = await import("../../src/altimate/native") - const r = await Dispatcher.call("altimate_core.check", { sql: PASSWD, base_sql: PASSWD }) + const r = await D.call("altimate_core.check", { sql: PASSWD, base_sql: PASSWD }) expect((((r.data as any).safety?.threats ?? []) as any[]).length).toBe(0) expect((r.data as any).safety.safe).toBe(true) }) test("real SQL statement types are preserved in messages", async () => { - const { Dispatcher } = await import("../../src/altimate/native") - const r = await Dispatcher.call("altimate_core.safety", { sql: "SELECT 1; DROP TABLE users;" }) + const r = await D.call("altimate_core.safety", { sql: "SELECT 1; DROP TABLE users;" }) const threats = ((r.data as any).threats ?? []) as any[] const messages = threats.map((t) => `${t.message} ${t.detail}`).join(" ") // SQL-keyword statement types stay useful in messages/details From 5b9b6ae1ebbdd3dbdfb5ec81f71d9df27e8e6e3f Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 17 Aug 2026 12:21:15 -0700 Subject: [PATCH 4/5] fix: whole-phrase keyword allowlist + non-vacuous consumer tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `isSqlStatementType` now requires EVERY word of the preserved phrase to come from the SQL keyword vocabulary (statement keywords first, auxiliary words like TABLE/INDEX/IF/EXISTS after) — checking only the first word preserved attacker tails such as "SET SECRET PASSWORD" verbatim. Since preserved text is a subset of the fixed vocabulary, no attacker-specific content can survive. Tests cover keyword-prefixed tails and compound types ("DROP INDEX IF EXISTS" stays useful). - `sql.analyze` test asserts non-empty safety issues + the redaction marker (was vacuous if the scan produced nothing). - Grade test corrected to the REAL EvalResult shape: its `safety` section is `{ method, score }` — there are no nested threats to echo (the handler redaction stays as defense-in-depth). The test now asserts the whole result is echo-free minus the by-contract input-echo fields (`sql`, `lint.sql`), which the CLI never renders. Co-Authored-By: Claude Fable 5 --- .../src/altimate/native/engine-coerce.ts | 14 ++++++++- .../test/altimate/threat-redaction.test.ts | 31 ++++++++++++++++--- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/altimate/native/engine-coerce.ts b/packages/opencode/src/altimate/native/engine-coerce.ts index 4df5399b3..0f49ab03a 100644 --- a/packages/opencode/src/altimate/native/engine-coerce.ts +++ b/packages/opencode/src/altimate/native/engine-coerce.ts @@ -78,10 +78,22 @@ const SQL_STATEMENT_KEYWORDS = new Set([ "DESC", "EXECUTE", "EXEC", "REPLACE", "VALUES", "LOCK", "UNLOCK", "COMMENT", "RENAME", "START", "SAVEPOINT", "RELEASE", "PREPARE", "DEALLOCATE", ]) +// Auxiliary words allowed AFTER a statement keyword ("ALTER TABLE", +// "DROP INDEX IF EXISTS"). Every word of a preserved phrase must come from +// the keyword vocabulary — checking only the first word would preserve +// attacker tails like "SET SECRET PASSWORD" verbatim. +const SQL_AUX_KEYWORDS = new Set([ + "TABLE", "INDEX", "VIEW", "DATABASE", "SCHEMA", "COLUMN", "TRIGGER", + "FUNCTION", "PROCEDURE", "SEQUENCE", "MATERIALIZED", "TEMPORARY", "TEMP", + "IF", "NOT", "EXISTS", "OR", "INTO", "TRANSACTION", "WORK", "CASCADE", + "UNIQUE", "ROLE", "USER", "EXTENSION", "TYPE", +]) function isSqlStatementType(token: string): boolean { const t = token.trim() if (!SQL_TOKEN_SHAPE.test(t)) return false - return SQL_STATEMENT_KEYWORDS.has(t.split(/\s+/)[0].toUpperCase()) + const words = t.split(/\s+/).map((w) => w.toUpperCase()) + if (!SQL_STATEMENT_KEYWORDS.has(words[0])) return false + return words.slice(1).every((w) => SQL_STATEMENT_KEYWORDS.has(w) || SQL_AUX_KEYWORDS.has(w)) } export function redactThreatText(text: string): string { return ( diff --git a/packages/opencode/test/altimate/threat-redaction.test.ts b/packages/opencode/test/altimate/threat-redaction.test.ts index 7d65c975d..21eeec441 100644 --- a/packages/opencode/test/altimate/threat-redaction.test.ts +++ b/packages/opencode/test/altimate/threat-redaction.test.ts @@ -35,6 +35,17 @@ describe("redactThreatText", () => { expect(EngineCoerce.redactThreatText("Statement type 'PRIVATE NOTE' is not in the allowed list")).toBe( "Statement type '' is not in the allowed list", ) + // A keyword PREFIX must not preserve an arbitrary tail. + expect(EngineCoerce.redactThreatText("Disallowed statement type: SET SECRET PASSWORD")).toBe( + "Disallowed statement type: ", + ) + expect(EngineCoerce.redactThreatText("Disallowed statement type: DROP DEAD FRED")).toBe( + "Disallowed statement type: ", + ) + // Compound statement types made purely of keywords stay useful. + expect(EngineCoerce.redactThreatText("Disallowed statement type: DROP INDEX IF EXISTS")).toBe( + "Disallowed statement type: DROP INDEX IF EXISTS", + ) }) test("embedded apostrophes cannot close the quoted match early", () => { @@ -103,15 +114,27 @@ describeIf("safety consumers redact raw-input echoes (real engine)", () => { test("sql.analyze does not echo non-SQL file content in issues", async () => { const r = await D.call("sql.analyze", { sql: PASSWD }) - const text = JSON.stringify((r as any).issues ?? []) + const safetyIssues = (((r as any).issues ?? []) as any[]).filter((i) => i.type === "safety") + // The sanitizer must actually be exercised — no safety issues would make + // the not-contains assertion vacuous. + expect(safetyIssues.length).toBeGreaterThan(0) + const text = JSON.stringify(safetyIssues) expect(text.toLowerCase()).not.toContain("root:x:0") + expect(text).toContain("redacted") }) - test("altimate_core.grade nested safety threats are redacted", async () => { + test("altimate_core.grade result does not echo non-SQL file content", async () => { + // EvalResult's safety section is { method, score } — no threat scan to + // echo (the handler's redactScan is defense-in-depth if that changes). + // Assert the whole result is echo-free, minus the `sql` field, which by + // contract carries back the caller's own input. const r = await D.call("altimate_core.grade", { sql: PASSWD }) expect(r.success).toBe(true) - const text = JSON.stringify(((r.data as any).safety?.threats ?? [])) - expect(text.toLowerCase()).not.toContain("root:x:0") + // `sql` (EvalResult) and `lint.sql` (LintResult) carry back the caller's + // own input by contract and are never rendered by the CLI grade check. + const { sql: _echoedInput, ...rest } = r.data as any + if (rest.lint) rest.lint = { ...rest.lint, sql: undefined } + expect(JSON.stringify(rest).toLowerCase()).not.toContain("root:x:0") }) test("diff-scoping still filters pre-existing multi_statement threats despite redaction", async () => { From 389ff1aff37e645f9416cf7aefd5c59bfc6bdccc Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 17 Aug 2026 12:35:43 -0700 Subject: [PATCH 5/5] fix: exact single-keyword statement-type allowlist (engine-faithful) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two review comments pulled opposite directions — cubic wanted compound types like "DELETE FROM" preserved; Codex wanted exact statement types instead of any keyword-vocabulary ordering ("SELECT DELETE UPDATE" leaked verbatim). Live-probing the 0.7.0 engine across DDL/DML/DCL/TCL settles both: the engine emits SINGLE-WORD statement types only ({ALTER, BEGIN, CALL, CREATE, DELETE, DROP, EXPLAIN, GRANT, INSERT, SET, TRUNCATE, UPDATE}). The allowlist is now an exact single-keyword match — the strictest rule that never redacts anything the engine actually produces: multi-word input can only be non-engine content, so keyword-prefixed tails and keyword permutations both redact. Vocabulary-ordering and multi-word tests updated with the probe rationale inline. Co-Authored-By: Claude Fable 5 --- .../src/altimate/native/engine-coerce.ts | 21 +++++++------------ .../test/altimate/threat-redaction.test.ts | 15 ++++++------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/packages/opencode/src/altimate/native/engine-coerce.ts b/packages/opencode/src/altimate/native/engine-coerce.ts index 0f49ab03a..62f300ff3 100644 --- a/packages/opencode/src/altimate/native/engine-coerce.ts +++ b/packages/opencode/src/altimate/native/engine-coerce.ts @@ -78,22 +78,17 @@ const SQL_STATEMENT_KEYWORDS = new Set([ "DESC", "EXECUTE", "EXEC", "REPLACE", "VALUES", "LOCK", "UNLOCK", "COMMENT", "RENAME", "START", "SAVEPOINT", "RELEASE", "PREPARE", "DEALLOCATE", ]) -// Auxiliary words allowed AFTER a statement keyword ("ALTER TABLE", -// "DROP INDEX IF EXISTS"). Every word of a preserved phrase must come from -// the keyword vocabulary — checking only the first word would preserve -// attacker tails like "SET SECRET PASSWORD" verbatim. -const SQL_AUX_KEYWORDS = new Set([ - "TABLE", "INDEX", "VIEW", "DATABASE", "SCHEMA", "COLUMN", "TRIGGER", - "FUNCTION", "PROCEDURE", "SEQUENCE", "MATERIALIZED", "TEMPORARY", "TEMP", - "IF", "NOT", "EXISTS", "OR", "INTO", "TRANSACTION", "WORK", "CASCADE", - "UNIQUE", "ROLE", "USER", "EXTENSION", "TYPE", -]) +// The engine emits SINGLE-WORD statement types only — probed against the +// live 0.7.0 binary across DDL/DML/DCL/TCL second statements, the full +// observed set is {ALTER, BEGIN, CALL, CREATE, DELETE, DROP, EXPLAIN, GRANT, +// INSERT, SET, TRUNCATE, UPDATE}. Exact single-keyword matching is therefore +// both engine-faithful and the strictest rule: multi-word input can only be +// non-engine content (kills keyword-permutation leaks like +// "SELECT DELETE UPDATE" without redacting anything the engine produces). function isSqlStatementType(token: string): boolean { const t = token.trim() if (!SQL_TOKEN_SHAPE.test(t)) return false - const words = t.split(/\s+/).map((w) => w.toUpperCase()) - if (!SQL_STATEMENT_KEYWORDS.has(words[0])) return false - return words.slice(1).every((w) => SQL_STATEMENT_KEYWORDS.has(w) || SQL_AUX_KEYWORDS.has(w)) + return SQL_STATEMENT_KEYWORDS.has(t.toUpperCase()) } export function redactThreatText(text: string): string { return ( diff --git a/packages/opencode/test/altimate/threat-redaction.test.ts b/packages/opencode/test/altimate/threat-redaction.test.ts index 21eeec441..cc977a295 100644 --- a/packages/opencode/test/altimate/threat-redaction.test.ts +++ b/packages/opencode/test/altimate/threat-redaction.test.ts @@ -9,10 +9,10 @@ try { const describeIf = coreAvailable ? describe : describe.skip describe("redactThreatText", () => { - test("keeps SQL-statement-keyword types", () => { + test("keeps single SQL-statement-keyword types (the only shape the engine emits)", () => { expect(EngineCoerce.redactThreatText("Disallowed statement type: DROP")).toBe("Disallowed statement type: DROP") - expect(EngineCoerce.redactThreatText("Statement type 'ALTER TABLE' is not in the allowed list")).toBe( - "Statement type 'ALTER TABLE' is not in the allowed list", + expect(EngineCoerce.redactThreatText("Statement type 'ALTER' is not in the allowed list")).toBe( + "Statement type 'ALTER' is not in the allowed list", ) }) @@ -35,16 +35,17 @@ describe("redactThreatText", () => { expect(EngineCoerce.redactThreatText("Statement type 'PRIVATE NOTE' is not in the allowed list")).toBe( "Statement type '' is not in the allowed list", ) - // A keyword PREFIX must not preserve an arbitrary tail. + // ANY multi-word phrase redacts — the engine only emits single-word + // statement types (live-probed), so multi-word input is never engine + // vocabulary: keyword-prefixed tails and keyword permutations both die. expect(EngineCoerce.redactThreatText("Disallowed statement type: SET SECRET PASSWORD")).toBe( "Disallowed statement type: ", ) expect(EngineCoerce.redactThreatText("Disallowed statement type: DROP DEAD FRED")).toBe( "Disallowed statement type: ", ) - // Compound statement types made purely of keywords stay useful. - expect(EngineCoerce.redactThreatText("Disallowed statement type: DROP INDEX IF EXISTS")).toBe( - "Disallowed statement type: DROP INDEX IF EXISTS", + expect(EngineCoerce.redactThreatText("Disallowed statement type: SELECT DELETE UPDATE")).toBe( + "Disallowed statement type: ", ) })