Skip to content

fix(query-postgres): block function-call SQLi bypass in data-explorer WHERE clauses - #328

Merged
dviejokfs merged 9 commits into
mainfrom
fix/postgres-explorer-sqli
Jul 15, 2026
Merged

dviejokfs merged 9 commits into
mainfrom
fix/postgres-explorer-sqli

Conversation

@dviejokfs

Copy link
Copy Markdown
Contributor

Security review finding #4 — SQL injection via denylist bypass in the Postgres data-explorer (High)

Location: `crates/temps-query-postgres/src/lib.rs` (`validate_sql`)

User WHERE clauses are concatenated into `SELECT * FROM … WHERE `, guarded only by a keyword denylist applied after string literals are stripped. Data-returning functions like `query_to_xml`/`database_to_xml`/`table_to_xml` aren't on the denylist, and their SQL payload lives inside a quoted string that gets stripped before the checks run — so:

```sql
1=1 AND query_to_xml('select ... from users', true, false, '') IS NOT NULL
```

passes validation and exfiltrates other tables.

Fix

A keyword denylist can never be complete. Add a structural rule that rejects function-call syntax outright: any identifier immediately preceding `(` is treated as a function call and rejected. Only grouping parens and `IN`/`AND`/`OR`/`NOT (...)` are permitted.

With subqueries (already blocked), `UNION` (already blocked), and now function calls all rejected, a WHERE clause can reference only the current table's columns and constants — closing the cross-table exfiltration class rather than playing denylist whack-a-mole.

Note: the review also suggests enforcing a read-only DB role for browsing. That's a larger infra change (role provisioning) and is tracked separately; this PR closes the exploitable injection vector.

Verification (regression tests only)

`cargo test -p temps-query-postgres --lib` — 35 pass, including:

  • `test_sql_injection_xml_function_exfiltration` — the exact `query_to_xml`/`database_to_xml`/`table_to_xml` payloads, now rejected.
  • `test_sql_injection_arbitrary_function_call_blocked` — `length()`, `cast()`, `upper()`.
  • `test_function_call_block_allows_grouping_and_in_lists` — legitimate grouping and `IN (...)` still validate.

`cargo clippy -p temps-query-postgres --all-targets -- -D warnings` clean.

The data-browser WHERE-clause validator relied on a keyword denylist applied
after string literals are stripped (security review finding #4). Data-returning
functions such as query_to_xml/database_to_xml/table_to_xml take their SQL
payload as a string-literal argument, so the payload was stripped before the
denylist ran and the function names themselves were not denylisted. A clause
like:

    1=1 AND query_to_xml('select ... from users', true, false, '') IS NOT NULL

therefore passed validation and exfiltrated arbitrary tables.

Enumerating dangerous functions can never be complete, so add a structural rule
that rejects function-call syntax outright: any identifier immediately
preceding '(' is treated as a function call and rejected. Only grouping parens
and IN/AND/OR/NOT (...) are permitted. With subqueries, UNION, and function
calls all blocked, a WHERE clause can reference only the current table's columns
and constants, closing the cross-table exfiltration class.

Regression tests cover the XML-function bypass and arbitrary function calls, and
confirm legitimate grouping and IN-lists still validate.
@github-actions

github-actions Bot commented Jul 13, 2026 •

Copy link
Copy Markdown

📓 Changelog preview

This is what your commits will add to the generated CHANGELOG.md at release time (via git-cliff). Do not edit CHANGELOG.md by hand — it is generated from your Conventional Commit messages.

## [Unreleased]

### Fixed

- **query-postgres:** Block function calls in data-browser WHERE clauses
- **query-postgres:** Close quoted function call bypasses
- **query-postgres:** Reject alternate subquery forms
- **query-postgres:** Parse SQL literals safely
- **query-postgres:** Handle high-bit dollar tags

### Testing

- **query-postgres:** Execute attacks against seeded table

@dviejokfs
dviejokfs merged commit f3ea0cb into main Jul 15, 2026
18 checks passed
@dviejokfs
dviejokfs deleted the fix/postgres-explorer-sqli branch July 15, 2026 08:34
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