Skip to content

Conversation

@divyenduz
Copy link
Contributor

@divyenduz divyenduz commented Sep 25, 2025

See #225

@vercel
Copy link
Contributor

vercel bot commented Sep 25, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
agent Ready Ready Preview Sep 26, 2025 5:47pm

return 'The query is not a single safe statement. Only SELECT, INSERT, UPDATE, DELETE, and WITH statements are allowed.';
}

if (query.includes('$1') || query.includes('$2') || query.includes('$3') || query.includes('$4')) {
Copy link
Contributor

@vercel vercel bot Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete parameter placeholder detection only checks for $1-$4, but PostgreSQL queries can have many more parameters like $5, $6, etc.

View Details
📝 Patch Details
diff --git a/apps/dbagent/src/lib/targetdb/safe-explain.ts b/apps/dbagent/src/lib/targetdb/safe-explain.ts
index 000d59e..dca7d3a 100644
--- a/apps/dbagent/src/lib/targetdb/safe-explain.ts
+++ b/apps/dbagent/src/lib/targetdb/safe-explain.ts
@@ -19,7 +19,7 @@ export async function safeExplainQuery(client: ClientBase, schema: string, query
     return 'The query is not a single safe statement. Only SELECT, INSERT, UPDATE, DELETE, and WITH statements are allowed.';
   }
 
-  if (query.includes('$1') || query.includes('$2') || query.includes('$3') || query.includes('$4')) {
+  if (/\$\d+/.test(query)) {
     // TODO: we could use `GENERIC_PLAN` to still get the plan in this case.
     return 'The query seems to contain placeholders ($1, $2, etc). Replace them with actual values and try again.';
   }
diff --git a/apps/dbagent/src/lib/targetdb/unsafe-explain.ts b/apps/dbagent/src/lib/targetdb/unsafe-explain.ts
index 6a44941..ce9d34d 100644
--- a/apps/dbagent/src/lib/targetdb/unsafe-explain.ts
+++ b/apps/dbagent/src/lib/targetdb/unsafe-explain.ts
@@ -262,7 +262,7 @@ export async function unsafeExplainQuery(client: ClientBase, schema: string, que
     return 'The query is not a single safe statement. Only SELECT, INSERT, UPDATE, DELETE, and WITH statements are allowed.';
   }
 
-  if (query.includes('$1') || query.includes('$2') || query.includes('$3') || query.includes('$4')) {
+  if (/\$\d+/.test(query)) {
     // TODO: we could use `GENERIC_PLAN` to still get the plan in this case.
     return 'The query seems to contain placeholders ($1, $2, etc). Replace them with actual values and try again.';
   }

Analysis

Incomplete parameter placeholder detection allows PostgreSQL queries with $5+ parameters to bypass safety checks

What fails: safeExplainQuery() and unsafeExplainQuery() in safe-explain.ts and unsafe-explain.ts only check for $1, $2, $3, $4 parameters, missing queries with higher-numbered placeholders like $5, $6, etc.

How to reproduce:

// This query would incorrectly pass the safety check:
const query = "SELECT id FROM users WHERE status = $5";
const detected = query.includes('$1') || query.includes('$2') || query.includes('$3') || query.includes('$4');
console.log(detected); // false - query incorrectly passes safety check

Result: Queries with only $5, $6, $7, $8, $9 parameters bypass the placeholder detection and reach PostgreSQL EXPLAIN, which then fails with "bind message supplies 0 parameters, but prepared statement requires X"

Expected: All parameterized queries should be detected and blocked with the message "The query seems to contain placeholders ($1, $2, etc). Replace them with actual values and try again."

Evidence: The test file unsafe-explain.test.ts line 67 contains a query with $8 parameters, confirming that higher-numbered parameters exist in the codebase and should be handled.

Fix: Replace substring matching with regex pattern /\$\d+/.test(query) to detect any parameter placeholder.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good bot.

}

if (query.includes('$1') || query.includes('$2') || query.includes('$3') || query.includes('$4')) {
// TODO: we could use `GENERIC_PLAN` to still get the plan in this case.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've discuss to do this, I imagine it's in a different PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is in the 3rd one (https://github.com/xataio/agent/pull/227/files) but only for safeExplainQuery, I didn't do this yet for unsafeExplainQuery

@divyenduz divyenduz merged commit 3ce9bb1 into unsafe_explain Sep 26, 2025
5 of 6 checks passed
@divyenduz divyenduz deleted the safe_explain branch September 26, 2025 17:45
@xata-bot xata-bot restored the safe_explain branch September 26, 2025 17:46
github-merge-queue bot pushed a commit that referenced this pull request Sep 26, 2025
Related discussion #223

We already improved it in #224, this
PR makes it more secure by

1. Renaming `explainQuery` to `unsafeExplainQuery` while the single
query check and transaction wrapping is pretty secure already, there
might be edge cases where a query escapes the designated boundaries.
Therefore, we rename the tool to `unsafeExplainQuery`
2. Create a new tool called `safeExplainQuery`, this operates on
`queryId` and fetches the actual SQL from `pg_stat_statements` table
itself thereby eliminating the code path that can lead to any SQL
injection. This is done in the following stacked PR
#226
3. Use the new `safeExplainQuery` tool instead of `unsafeExplainQuery`,
to make it work, we had to additionally return `queryId` from
`getSlowQueries` tool in addition to the slow SQL query. This is done in
the following stacked PR #227
SolveEve pushed a commit to SolveEve/agent that referenced this pull request Nov 15, 2025
Related discussion xataio/agent#223

We already improved it in xataio/agent#224, this
PR makes it more secure by

1. Renaming `explainQuery` to `unsafeExplainQuery` while the single
query check and transaction wrapping is pretty secure already, there
might be edge cases where a query escapes the designated boundaries.
Therefore, we rename the tool to `unsafeExplainQuery`
2. Create a new tool called `safeExplainQuery`, this operates on
`queryId` and fetches the actual SQL from `pg_stat_statements` table
itself thereby eliminating the code path that can lead to any SQL
injection. This is done in the following stacked PR
xataio/agent#226
3. Use the new `safeExplainQuery` tool instead of `unsafeExplainQuery`,
to make it work, we had to additionally return `queryId` from
`getSlowQueries` tool in addition to the slow SQL query. This is done in
the following stacked PR xataio/agent#227
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.

3 participants