Skip to content

fix(mcp): close approval/read-only bypass in run_query#261

Merged
debba merged 1 commit into
TabularisDB:mainfrom
ymadd:fix/mcp-approval-bypass
May 29, 2026
Merged

fix(mcp): close approval/read-only bypass in run_query#261
debba merged 1 commit into
TabularisDB:mainfrom
ymadd:fix/mcp-approval-bypass

Conversation

@ymadd
Copy link
Copy Markdown
Contributor

@ymadd ymadd commented May 27, 2026

Summary

The MCP run_query approval/read-only gates classify a query by its leading keyword, so a stacked payload such as SELECT 1; DROP TABLE users was tagged as a clean read (select) and slipped past both the read-only gate and the approval gate. Separately, when an approver edited an approved query before confirming, the edit was executed without being re-classified — so an approved SELECT could be flipped into a DELETE/DROP.

This PR makes the classifier fail closed on multi-statement payloads and re-enforces the gates on the approver-edited query.

The bypass

  1. Multi-statement payloadclassify_query_kind("SELECT 1; DROP TABLE users") returned "select" because only the first keyword was inspected. Both gates rely on kind != "select" to block, so the write reached the driver.
  2. Approver-edited query — on approval, edited_query replaced the original query but the original kind was kept, so read-only was never re-checked against what actually runs.

Changes

  • ai_activity.rsclassify_query_kind now returns "unknown" (fail closed) when a ; is followed by more SQL. String literals are stripped under both the SQL-standard ('') and MySQL backslash-escape (\') readings, and a trailing statement under either reading trips the gate — so a payload can't hide a ; separator by exploiting whichever dialect we don't assume (e.g. SELECT '\''; DROP TABLE users).
  • mcp/mod.rstool_run_query re-classifies the approver-edited query and re-applies the read-only gate before execution, updating the audit record with the effective query/kind.

Test plan

  • cargo test --lib ai_activity — 54 passing, incl. new cases:
    • SELECT 1; DROP … / …; DELETE … / …; UPDATE …unknown
    • MySQL '\''; DROP … cannot hide the separator → unknown
    • legitimate ; inside string literals / comments / '' escape boundary → stays select
    • single trailing ; keeps its kind
  • cargo check --lib clean

Notes

Rebased on current main (incl. #256). The execution layer's prepared-statement protocol already rejected most stacked queries, but the classifier is the documented fail-closed gate — this restores that contract.

The MCP run_query approval gate classified queries by their leading
keyword, so a stacked payload (`SELECT 1; DROP TABLE users`) was tagged
as a clean read and slipped past both the read-only and approval gates.
An approver could also edit an approved SELECT into a DELETE/DROP, which
was never re-validated.

- classify_query_kind now fails closed on multi-statement payloads,
  stripping string literals under both the SQL-standard and MySQL
  backslash-escape readings so neither dialect can hide a `;` separator
- tool_run_query re-classifies and re-enforces read-only on the
  approver-edited query before execution
@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented May 27, 2026

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (3 files)
  • src-tauri/src/ai_activity.rs — Multi-statement detection with dual-mode string stripping (standard + MySQL backslash escapes). has_trailing_statements correctly identifies real statement separators after stripping literals/comments. strip_impl cleanly separates dialect-aware escaping from the public strip_strings_and_comments API.
  • src-tauri/src/ai_activity_tests.rs — Comprehensive test coverage for multi-statement payloads, trailing semicolons, escaped quotes under both SQL-standard and MySQL interpretations, and comment/string embedding.
  • src-tauri/src/mcp/mod.rs — Re-classifies and re-enforces read-only mode on edited queries after approval, closing the approver-editing bypass.

Reviewed by kimi-k2.6 · 426,420 tokens

@debba debba merged commit 1b1bb03 into TabularisDB:main May 29, 2026
2 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.

2 participants