chore(audit): weekly cloud audit — 4 SQL read-only bypasses + 4 scanner false-clean masquerades#123
Draft
tzone85 wants to merge 5 commits into
Draft
chore(audit): weekly cloud audit — 4 SQL read-only bypasses + 4 scanner false-clean masquerades#123tzone85 wants to merge 5 commits into
tzone85 wants to merge 5 commits into
Conversation
…and npm-audit false-clean
Two verified defects from the weekly cloud audit:
sqlsafety: the read-only SQL gate's side-effecting-function denylist
(ContainsDeniedFunction) matched `<fn>[\s]*\(`, but stripSQLCommentsAndStrings
never unwrapped double-quoted identifiers. Postgres folds an unquoted
identifier to lowercase, so `"pg_terminate_backend"` names the same built-in
as `pg_terminate_backend`, yet the interposed quote defeated the regex.
`vxd db sql <db> 'SELECT "pg_terminate_backend"(pid) FROM pg_stat_activity'`
(and `"pg_read_file"('/etc/passwd')`) classified as read-only and ran without
--write — exactly the cross-user disruption the denylist exists to block, and
READ ONLY transactions do not stop these calls. The classifier now unwraps
double-quoted identifiers to bare inner text so the true call shape is seen;
this also hardens ClassifyQuery/IsMultiStatement which share the strip pass.
security: parseNpmAudit unmarshalled only {vulnerabilities}, so npm's
valid-JSON error envelope ({"error":{"code":"ENOLOCK",...}}, emitted when the
audit cannot run — e.g. a package.json with no lockfile) parsed as a clean,
empty scan. RunScanners then counted it in `ran` (coverage OK) instead of
`failed` (coverage lost), so the dependency-CVE scan silently no-ops while the
security gate — and security.require_scanners strict mode — believe the tool
ran. parseNpmAudit now surfaces the error envelope as a failure.
Both restore already-documented behavior (no doc/config change). Regression
tests added: quoted-identifier denied/allowed cases + strip-pass unwrap;
npm-audit ENOLOCK/code-only error envelopes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2huFn4mXqnCNn1jZCZKWi
…false-clean (round 2 siblings)
Two verified siblings of the round-1 fixes, found in the same two files:
sqlsafety: stripSQLCommentsAndStrings modelled single-quoted strings with
only ''-doubling escape semantics. Postgres *escape strings* (E'...'/e'...')
let a backslash escape a quote, so `\'` does NOT close the string. The stripper
mis-tracked the boundary and over-consumed, swallowing a denied call that
followed a literal:
`vxd db sql <db> "SELECT E'\'' || pg_read_file('/etc/passwd')"` — a single
valid statement — classified read-only and ran without --write (confirmed:
ValidateSQL returned nil). The stripper now detects escape-string mode (E'/e'
at a token boundary) and consumes backslash escapes so the boundary is correct;
the trailing pg_read_file(/pg_terminate_backend( call is then caught.
security: parseSemgrep decoded only "results" and ignored the top-level
"errors" array. On a scan-level failure (e.g. `--config auto` cannot load rules
on an offline host) semgrep emits valid JSON with results:[] + an error-level
entry and exits non-zero, so parseSemgrep returned ([], nil) — counted in
RunScanners' `ran` (clean) list, not `failed`, hiding total coverage loss from
the gate and require_scanners strict mode (same masquerade class as the
round-1 npm-audit fix). It now returns an error when results are empty AND a
fatal error-level entry is present; a scan that produced findings is never
flipped to failed by a coexisting non-fatal warning.
Both restore already-documented behavior (no doc/config change). Regression
tests added: E-string denied/allowed + strip-pass cases; semgrep scan-error
envelope + results-plus-warning stays-clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2huFn4mXqnCNn1jZCZKWi
…k false-clean (round 3 siblings)
Third pair of verified siblings in the same two files:
sqlsafety: stripSQLCommentsAndStrings had no dollar-quote ($tag$...$tag$)
handling. A dollar-quoted literal containing an ODD number of apostrophes
desynced the single-quote tracker, swallowing a denied call that followed the
literal: `vxd db sql <db> "SELECT $$'$$, pg_read_file('/etc/passwd')"` — a
single valid statement whose pg_read_file call is real executable code outside
any string — classified read-only and ran without --write (confirmed:
ValidateSQL returned nil). The stripper now consumes dollar-quoted strings
before the single-quote branch (tag follows identifier rules, so positional
params like $1 are not treated as delimiters), fixing the desync.
security: parseGovulncheck runs in text mode, so — unlike the JSON scanners
whose failure output fails json.Unmarshal and routes to `failed` — a run that
could not execute (e.g. the vulnerability DB unreachable on an offline host)
produced zero findings and returned (nil, nil), counted in RunScanners' `ran`
(clean) list. Same masquerade class as the npm-audit/semgrep fixes. It now
detects govulncheck's "govulncheck: <msg>" fatal line and returns an error
when no findings were parsed; a clean no-vuln run is never flipped to failed.
The complete Postgres string/identifier lexical set is now handled by the
classifier: '...' (incl. E'...' escape), "..." identifiers, and $tag$...$tag$.
Both restore already-documented behavior (no doc/config change). Regression
tests added for both.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2huFn4mXqnCNn1jZCZKWi
…y mode
Fourth SQL-lexer sibling. A Postgres Unicode-escape identifier U&"\XXXX…" can
name a function to call, but its \XXXX escapes decode only server-side — the
denylist regex sees the raw escapes, so it never recognises the decoded name.
`vxd db sql <db> "SELECT U&\"\0070\0067_\0072\0065\0061\0064_\0066\0069\006C\0065\"('/etc/passwd')"`
(the escapes decode to pg_read_file) classified read-only and ran without
--write (confirmed: ValidateSQL returned nil), and READ ONLY does not block
pg_read_file.
Rather than attempt to decode arbitrary \XXXX / custom-UESCAPE sequences, the
read-only gate now rejects U&"…" identifiers outright (re-run with --write if
intentional). Only the identifier form is flagged: U&'…' is a string literal
whose content never executes and is already stripped correctly. The stripper
is refactored into lexSQL, which returns the stripped text plus a flag for a
U&"…" identifier seen outside comments/strings; stripSQLCommentsAndStrings and
the new hasUnicodeEscapeIdentifier are thin wrappers.
This completes the Postgres string/identifier lexical set the classifier
understands: '…' (incl. E'…'), "…" (incl. U&"…"), $tag$…$tag$, U&'…'.
Regression tests added (rejected identifier forms, allowed U&'…' string /
plain quoted identifier / u-prefixed name, --write opt-out).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2huFn4mXqnCNn1jZCZKWi
…h no issues Completes the fail-closed invariant across all five scanner parsers. gosec reports compile/package-load failures in its "Golang errors" map while still emitting valid JSON with an empty Issues list, so parseGosec (which decoded only Issues) returned a clean empty scan for a run that could not analyse the code — the same "failed scan masquerading as clean" the npm-audit/semgrep/ govulncheck fixes close. parseGosec now returns an error when Issues is empty AND Golang errors are present; a scan that produced issues is never flipped to failed by a coexisting per-file parse error. With this, parseGosec, parseGitleaks, parseNpmAudit, parseSemgrep and parseGovulncheck all surface a failed run as `failed` (coverage lost) rather than counting it in RunScanners' clean `ran` list. Regression test added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U2huFn4mXqnCNn1jZCZKWi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Weekly cloud audit — 2026-07-20
Fan-out audit across concurrency, silent failures, event-sourcing wiring, and security, then five loop-until-dry rounds with every candidate adversarially verified before fixing (rounds 4 and 5 both returned no findings → two consecutive dry passes → done; round 4 additionally verified the SQL findings against a live PostgreSQL 16 instance). Concurrency and event-sourcing wiring came back clean. Two real defect classes surfaced, each with several siblings — 8 verified fixes total, all in two files.
Class A —
internal/sqlsafety/sql_safety.go: read-only SQL gate bypassesvxd db sql(and thesql_query_returnsQA criterion) run operator SQL underBEGIN READ ONLY, which does not block side-effecting function calls (pg_terminate_backend,pg_read_file,lo_import, …) — so those are rejected statically byContainsDeniedFunction, which strips comments/strings then regex-matches<fn>(. The classifier's string/identifier lexer had gaps that let a denied call reach the DB without--write. Each was confirmed empirically (ValidateSQL(query, false, nil)returnednil).stripSQLCommentsAndStrings— double-quoted identifiers. The stripper left"..."verbatim. Postgres folds an unquoted identifier to lowercase, so"pg_terminate_backend"names the same built-in — but the quote between the name and(defeated the regex.SELECT "pg_terminate_backend"(pid)/SELECT "pg_read_file"('/etc/passwd')classified read-only. Fix: unwrap double-quoted identifiers to bare inner text.stripSQLCommentsAndStrings—E'…'escape strings. Single-quoted handling assumed only''escaping; Postgres escape strings let\'escape a quote, so the stripper mis-tracked the boundary and swallowed a call after the literal.SELECT E'\'' || pg_read_file('/etc/passwd')(a single valid statement) bypassed. Fix: detectE'/e'at a token boundary and consume backslash escapes.stripSQLCommentsAndStrings— dollar-quoted strings. No$tag$…$tag$handling; a dollar-quoted literal with an odd number of apostrophes desynced the single-quote tracker and hid the trailing call.SELECT $$'$$, pg_read_file('/etc/passwd')bypassed. Fix: consume dollar-quoted strings before the single-quote branch (tag follows identifier rules, so$1params are not delimiters).ValidateSQL/hasUnicodeEscapeIdentifier—U&"…"Unicode-escape identifiers. AU&"\XXXX…"identifier can name a function, but its escapes decode only server-side, so the regex (seeing raw\XXXX) never matches the decoded name.SELECT U&"\0070\0067_\0072\0065\0061\0064_\0066\0069\006C\0065"('/etc/passwd')(decodes topg_read_file) bypassed. Decoding arbitrary escapes is error-prone, so the gate now rejectsU&"…"identifiers in read-only mode (--writeopts out). Only the identifier form is flagged —U&'…'is a string literal (its content never executes) and is already stripped correctly.The classifier now understands the full Postgres string/identifier lexical set:
'…'(incl.E'…'),"…"(incl.U&"…"),$tag$…$tag$, andU&'…'.stripSQLCommentsAndStringswas refactored intolexSQL. Documented assumption (unchanged, in-code):standard_conforming_strings=on, the Postgres default and thepostgres:16devdb default; with it off the mismatch direction is over-rejection, never concealment.Class B —
internal/security/scanners.go: failed scans masquerading as cleanRunScannerssorts each tool intoran(clean),skipped(tool absent), orfailed(coverage lost). A parser returning(nil, nil)for a run that couldn't actually scan lands inran, so the security gate — andsecurity.require_scannersstrict mode — believe the tool provided coverage it never did. The JSON scanners whose failure output is non-conforming already fail closed viajson.Unmarshal; four parsers had a fail-open path:parseNpmAudit— npm's valid-JSON error envelope ({"error":{"code":"ENOLOCK",…}}, e.g. no lockfile) decoded to a nil vuln map → clean. Fix: surface theerrorfield as a failure.parseSemgrep— decoded onlyresults, ignoring the top-levelerrorsarray; a scan-level failure (e.g.--config autocan't load rules offline) emitsresults:[]+ an error-level entry → clean. Fix: fail whenresultsis empty and alevel:"error"entry is present (a scan that produced findings is never flipped).parseGovulncheck— text-mode parser with no failure detection; an unreachable vuln DB (offline host) produced zeroVulnerability #lines → clean. Fix: detect thegovulncheck:fatal line and fail when no findings were parsed.parseGosec— decoded onlyIssues, ignoring the"Golang errors"map; a compile/package-load failure emits valid JSON with emptyIssues→ clean. Fix: fail whenIssuesis empty andGolang errorsare present.All five parsers (
parseGosec,parseGitleaks,parseNpmAudit,parseSemgrep,parseGovulncheck) now surface a failed run asfailed, upholding the module's stated invariant that "a failed scan must never masquerade as a clean one."Every fix restores already-documented behavior (denylist doc comment;
RunScanners"must never masquerade" comment), so no CLI/config/doc-coverage changes are required. Regression tests were added for all eight.Verification
Run with the project's required toolchain (
GOTOOLCHAIN=go1.26.5);golangci-lintandgovulncheckwere re-installed against go1.26.5 (the pre-installed builds were compiled with go1.25 and refuse a go1.26 target).go build ./...— OKgo vet ./...— OKgolangci-lint run ./...(configdefault: standard) — 0 issuesgo test ./internal/sqlsafety/... ./internal/security/...— PASS (the two changed packages, incl. all new regression tests)TestDocCoverage_*) — PASSPre-existing, environment-only failures (present on unmodified
main, unrelated to this diff): the cloud runner executes the suite as root (uid 0) and without theghCLI, breaking tests that assert on filesystem-permission orgh-required failure paths —internal/cliTestApproveStory_*/TestApproveAll_*/TestRunApprove_*(nogh→ "gh CLI is required" short-circuit),internal/engineTestWriteLockFile_InvalidPath+TestWriteMetadata_InvalidDir(rootMkdirAllsucceeds under/),internal/figmaTestResolveToken_UnreadableFileIsDistinguished(root reads throughchmod 0o000),internal/repolearnTestCountFilesInDir_NonexistentDir(polluted by/nonexistent/paththe root engine test creates). Verified by stashing this diff and re-running on cleanmain— an identical 10-test set fails there. Not touched.govulncheck/make vuln: could not run — the environment's egress policy deniesvuln.go.dev(403 on CONNECT), so the vulnerability DB is unreachable. Not retried, per the proxy's report-don't-route-around rule.go build/vet/testserved as the fallback gate. (This same unreachable-DB condition is exactly the failure theparseGovulncheckfix above surfaces instead of hiding.)🤖 Generated with Claude Code
https://claude.ai/code/session_01U2huFn4mXqnCNn1jZCZKWi