fix(security): reject subqueries in count_rows where clause - #201
fix(security): reject subqueries in count_rows where clause#201moizpgedge wants to merge 1 commit into
Conversation
count_rows's where parameter was interpolated directly into the generated SELECT COUNT(*) statement with no check beyond validateReadOnlyQuery, which only recognises constructs that escape read-only mode. A subquery in where is ordinary legal SQL, so it was invisible to that guard, and let a caller run a boolean- or error-based blind injection oracle against any table the connected role could read, not only the table named in the call. Add validateCountRowsWhereClause, which rejects a bare SELECT or TABLE keyword in the clause's comment-stripped, literal-blanked residue (reusing stripSQLNoise from readonly_guard.go). TABLE is included because "TABLE tablename" is PostgreSQL shorthand for "SELECT * FROM tablename" and reads identical data without the word SELECT ever appearing; VALUES, the grammar's third row-returning form, is deliberately left unblocked, since it admits no FROM clause and so can never read a table, and unlike SELECT/TABLE it is not fully reserved and can legitimately be an unquoted column name. query_database and execute_explain are unaffected, since running arbitrary SQL, including subqueries, is their entire purpose. Verified directly against a live server: every payload from the report is now rejected (boolean-blind and error-based cross-table oracles, EXISTS, the TABLE-shorthand variant), a legitimate simple predicate still succeeds, VALUES is correctly left unblocked, and the pre-existing stacked-query rejection is untouched. Fixes #200
Walkthrough
Changescount_rows security validation
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Sequence Diagram(s)sequenceDiagram
participant Client
participant MCPServer
participant CountRowsHandler
participant PostgreSQL
Client->>MCPServer: tools/call count_rows
MCPServer->>CountRowsHandler: dispatch request
CountRowsHandler->>CountRowsHandler: reject SELECT/TABLE subqueries
CountRowsHandler->>PostgreSQL: execute accepted predicate
PostgreSQL-->>CountRowsHandler: count result
CountRowsHandler-->>Client: JSON-RPC response
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 36 |
| Duplication | 2 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
internal/tools/count_rows_test.go (1)
54-87: 🔒 Security & Privacy | 🔵 TrivialSolid subquery-rejection coverage; consider adding nested-comment and dollar-quote evasion cases.
Given the guard's correctness rests on
stripSQLNoise's noise-removal fidelity (see the request_verification comment oninternal/tools/count_rows.go), it would strengthen this suite to add cases like"1=1 AND (/* /* */ SELECT 1 FROM secret_table */) IS NOT NULL"(nested comments) and a dollar-quoted-string variant, mirroring the existingSEL/**/ECTtest's intent.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/tools/count_rows_test.go` around lines 54 - 87, The subquery rejection test lacks coverage for nested-comment and dollar-quoted-string evasion paths in stripSQLNoise. Extend TestValidateCountRowsWhereClause_RejectsSubqueries with representative cases that hide or surround SELECT using nested comments and dollar-quoted strings, mirroring the existing comment-splitting evasion coverage, and assert each is rejected.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@internal/tools/count_rows_test.go`:
- Around line 54-87: The subquery rejection test lacks coverage for
nested-comment and dollar-quoted-string evasion paths in stripSQLNoise. Extend
TestValidateCountRowsWhereClause_RejectsSubqueries with representative cases
that hide or surround SELECT using nested comments and dollar-quoted strings,
mirroring the existing comment-splitting evasion coverage, and assert each is
rejected.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 83e5d014-a517-4873-bcfc-87918bd92c1a
📒 Files selected for processing (4)
docs/changelog.mdinternal/tools/count_rows.gointernal/tools/count_rows_test.gotest/count_rows_injection_test.go
count_rows's where parameter was interpolated directly into the generated SELECT COUNT(*) statement with no check beyond validateReadOnlyQuery, which only recognises constructs that escape read-only mode. A subquery in where is ordinary legal SQL, so it was invisible to that guard, and let a caller run a boolean- or error-based blind injection oracle against any table the connected role could read, not only the table named in the call.
Add validateCountRowsWhereClause, which rejects a bare SELECT or TABLE keyword in the clause's comment-stripped, literal-blanked residue (reusing stripSQLNoise from readonly_guard.go). TABLE is included because "TABLE tablename" is PostgreSQL shorthand for "SELECT * FROM tablename" and reads identical data without the word SELECT ever appearing; VALUES, the grammar's third row-returning form, is deliberately left unblocked, since it admits no FROM clause and so can never read a table, and unlike SELECT/TABLE it is not fully reserved and can legitimately be an unquoted column name. query_database and execute_explain are unaffected, since running arbitrary SQL, including subqueries, is their entire purpose.
Verified directly against a live server: every payload from the report is now rejected (boolean-blind and error-based cross-table oracles, EXISTS, the TABLE-shorthand variant), a legitimate simple predicate still succeeds, VALUES is correctly left unblocked, and the pre-existing stacked-query rejection is untouched.
Fixes #200
Summary by CodeRabbit
Security
count_rowsvalidation to reject subqueries inwherefilters, includingSELECTandTABLEpatterns.VALUESexpressions and blocking stacked queries.Documentation