How the Dify DB-Query Plugin Enforces SELECT-Only Queries for Security
The Dify db-query plugin guarantees read-only database access by parsing incoming SQL with sqlparse and rejecting any statement that is not a single SELECT query before establishing a database connection.
The junjiem/dify-plugin-tools-dbquery repository provides LLM tools for querying databases within the Dify ecosystem. To prevent destructive SQL injection attacks such as table drops or data exfiltration, the plugin implements a strict SELECT-only security gate that analyzes every query at the application layer.
The Three-Step Security Validation in _invoke
The enforcement logic resides in the _invoke method of both plugin variants. When a user submits a query, the plugin executes three validation checks using the sqlparse library.
First, the plugin parses the raw SQL into an abstract syntax tree:
statements = sqlparse.parse(query_sql)
Second, it verifies that exactly one statement exists. If len(statements) != 1, the plugin raises a ValueError with the message "Only a single query SQL can be filled".
Third, the plugin inspects the statement type via statement.get_type(). If the result is anything other than 'SELECT', execution halts with the error "Query SQL can only be a single SELECT statement".
statements = sqlparse.parse(query_sql)
if len(statements) != 1:
raise ValueError("Only a single query SQL can be filled")
statement = statements[0]
if statement.get_type() != 'SELECT':
raise ValueError("Query SQL can only be a single SELECT statement")
This logic appears at lines 44-45 in db_query/tools/sql_query.py for the standard plugin and at identical lines in db_query_pre_auth/tools/sql_query.py for the pre-authenticated variant.
Pre-Connection Enforcement Architecture
The validation occurs before the plugin opens any database connection. This design ensures that malicious payloads—such as DROP TABLE, INSERT, or multi-statement attacks—never reach the database engine.
By blocking non-SELECT queries at the application layer, the plugin eliminates the risk of accidental or intentional data modification, even if connection credentials are compromised. The credential validation happens separately, but the SELECT-only gate serves as the primary security boundary.
Practical Examples: Allowed vs. Blocked Queries
✅ Valid Single SELECT Statement
When submitting a pure read query, the plugin connects and returns results:
{
"tool_name": "sql_query",
"tool_parameters": {
"db_type": "mysql",
"db_host": "127.0.0.1",
"db_port": "3306",
"db_username": "user",
"db_password": "pwd",
"db_name": "mydb",
"query_sql": "SELECT id, name FROM users LIMIT 10",
"output_format": "markdown"
}
}
❌ Blocked Non-SELECT Statements
Attempting data modification triggers immediate rejection:
{
"tool_name": "sql_query",
"tool_parameters": {
"db_type": "mysql",
"db_host": "127.0.0.1",
"db_username": "user",
"db_password": "pwd",
"db_name": "mydb",
"query_sql": "DELETE FROM users WHERE id = 1"
}
}
Result:
ValueError: Query SQL can only be a single SELECT statement
❌ Blocked Multiple Statements
The parser also rejects queries containing semicolon-separated statements:
{
"tool_name": "sql_query",
"tool_parameters": {
"query_sql": "SELECT * FROM users; SELECT * FROM orders"
}
}
Result:
ValueError: Only a single query SQL can be filled
Key Source Files and Implementation Locations
The SELECT-only enforcement is implemented consistently across both plugin variants:
db_query/tools/sql_query.py(lines 44-45): Contains the_invokemethod with validation logic for the standard plugin.db_query_pre_auth/tools/sql_query.py(lines 44-45): Identical validation for the pre-auth variant, which sources credentials from runtime configuration rather than user input.tools/db_util.py: Utility module that handles the actual database execution only after validation passes.
Summary
- The plugin uses sqlparse to build an AST from user-supplied SQL.
- It requires exactly one statement per invocation, rejecting multiple statements with a
ValueError. - It verifies
statement.get_type() == 'SELECT'and raisesValueErrorfor any other command type. - Validation occurs before database connection, ensuring destructive commands never execute.
- Both the standard and pre-auth variants implement identical security logic at lines 44-45 of their respective
sql_query.pyfiles.
Frequently Asked Questions
What happens if I try to run an INSERT or UPDATE statement?
The plugin raises a ValueError with the message "Query SQL can only be a single SELECT statement" and terminates execution before connecting to the database. This prevents any data modification regardless of user permissions or database privileges.
Does the pre-auth version have the same security checks?
Yes. The pre-auth variant in db_query_pre_auth/tools/sql_query.py contains identical validation logic at lines 44-45, ensuring consistent SELECT-only enforcement across both plugin modes.
Can I bypass the SELECT-only restriction using SQL comments or subqueries?
No. The sqlparse library analyzes the actual statement type, not just the raw text. While subqueries within a SELECT are permitted, the outermost statement must still be a SELECT. Comments do not alter the parsed statement type returned by get_type().
Which parsing library does the plugin use to analyze SQL statements?
The plugin uses sqlparse, a Python library for parsing and splitting SQL statements. It calls sqlparse.parse() to generate an AST and then inspects the statement type to enforce the security policy.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →