Skip to content

fix(security): YAML criterion path containment + SQL read-only gate [2 HIGH]#68

Merged
tzone85 merged 1 commit into
mainfrom
fix/yaml-criteria-containment-and-sql-readonly
Jun 11, 2026
Merged

fix(security): YAML criterion path containment + SQL read-only gate [2 HIGH]#68
tzone85 merged 1 commit into
mainfrom
fix/yaml-criteria-containment-and-sql-readonly

Conversation

@tzone85

@tzone85 tzone85 commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Summary

Closes both HIGH findings from the post-PR-67 certification audit. Both are exploitable through a malicious or copy-pasted `vxd.yaml` with zero privileges beyond the normal submission path.

# Severity What
1 HIGH `resolvePath` in `internal/engine/criteria.go` accepted absolute YAML paths — file_contains could read `/etc/shadow` and exfil via WebSocket command_result.Detail
2 HIGH `evaluateSQLQueryReturns` in `internal/engine/criteria_db.go` ran raw YAML SQL with no classifier or read-only transaction — DROP TABLE / DELETE committed

YAML criterion path containment

  • Absolute paths rejected outright.
  • Joined+cleaned form must remain a descendant of cleaned workDir.
  • `..` traversals rejected even when they would collapse back inside workDir.
  • Empty paths rejected.
  • `resolvePath` now returns `(string, error)` so the criterion handlers surface rejection in the standard `CriterionResult.Detail` channel.

Four new tests: `TestResolvePath_RejectsAbsolute`, `_RejectsTraversal`, `_AcceptsContainedRelative`, `_RejectsEmpty`.

SQL read-only gate

  • Moved `internal/cli/sql_safety.go` + tests to new `internal/sqlsafety` package so both engine and cli call the exact same classifier.
  • `evaluateSQLQueryReturns` now: (a) calls `sqlsafety.ValidateSQLForReadOnly` with no `--write` override (QA criteria are never expected to mutate); (b) wraps execution in `pgx.BeginTx{AccessMode: pgx.ReadOnly}` so Postgres rejects any DDL/DML that slipped past the classifier.

Eight new tests cover plain DROP, INSERT, multi-statement with SELECT prefix, CTE-DELETE-RETURNING, EXPLAIN ANALYZE INSERT, empty-after-stripping, plus four positive cases (SELECT/SHOW/VALUES/TABLE).

Test plan

  • `go test ./... -count=1` — all 29 packages green (new `sqlsafety` package)
  • `go vet ./...` clean
  • `go build ./...` clean
  • `vxd db sql` behaviour unchanged (only the package path moved)

Closes both HIGH findings from the post-PR-67 certification audit.

1. resolvePath escape in internal/engine/criteria.go (HIGH).
   resolvePath returned c.Path verbatim when filepath.IsAbs was true,
   letting a YAML criterion read arbitrary host files. A poisoned
   repo's vxd.yaml could ship
     - kind: file_contains
       path: /etc/shadow
       value: root
   The CriterionResult.Detail then flowed back through the WebSocket
   command_result response, exfiltrating the file contents to anyone
   with dashboard read access. The same path also accepted `..`
   traversals that collapsed back inside workDir via the OS at open
   time. Now: absolute paths rejected outright, joined+cleaned form
   must remain a descendant of cleaned workDir, empty paths rejected.
   resolvePath now returns (string, error) so the criterion handlers
   surface the rejection in the standard CriterionResult.Detail
   channel. Four new tests pin the boundary (absolute, traversal,
   contained relative, empty).

2. evaluateSQLQueryReturns SQL injection (HIGH).
   The YAML-supplied SQL for the sql_query_returns criterion was
   passed straight to conn.Query with no classifier and no read-only
   transaction. vxd db sql already had a three-layer defence
   (ValidateSQLForReadOnly + BEGIN READ ONLY + --write gating); the
   same logic now protects the QA path.

   Moved internal/cli/sql_safety.go + its tests to a new
   internal/sqlsafety package so both engine and cli can call the
   exact same classifier without cli↔engine import inversion. cli/db.go
   updated to import sqlsafety; behaviour unchanged for vxd db sql.
   engine/criteria_db.go now calls sqlsafety.ValidateSQLForReadOnly
   (no --write override — QA criteria are never expected to mutate)
   and wraps execution in pgx.BeginTx with AccessMode: pgx.ReadOnly.
   Postgres itself rejects any DDL/DML that slipped past the classifier.
   Eight new tests in criteria_db_safety_test.go cover plain DROP,
   INSERT, multi-statement with SELECT prefix, CTE-DELETE-RETURNING,
   EXPLAIN ANALYZE INSERT, empty-after-stripping, plus four positive
   cases (SELECT/SHOW/VALUES/TABLE) confirming the gate passes legitimate
   SQL.

All 28 packages still pass with -count=1.
@tzone85 tzone85 merged commit 69ed55d into main Jun 11, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant