-
Notifications
You must be signed in to change notification settings - Fork 254
Fix: detect mutating keywords inside CTEs in read-only mode #292
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
a37ced9
04cfa48
4ce777a
e488858
88f9e42
20aef0e
439fca7
7f61160
b37af1b
7719d4e
f3b32da
87f75fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,35 +7,115 @@ import { stripCommentsAndStrings } from "./sql-parser.js"; | |||||||||||||||||||||||||
| * but also other queries that are not destructive | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| export const allowedKeywords: Record<ConnectorType, string[]> = { | ||||||||||||||||||||||||||
| postgres: ["select", "with", "explain", "analyze", "show"], | ||||||||||||||||||||||||||
| mysql: ["select", "with", "explain", "analyze", "show", "describe", "desc"], | ||||||||||||||||||||||||||
| mariadb: ["select", "with", "explain", "analyze", "show", "describe", "desc"], | ||||||||||||||||||||||||||
| sqlite: ["select", "with", "explain", "analyze", "pragma"], | ||||||||||||||||||||||||||
| postgres: ["select", "with", "explain", "show"], | ||||||||||||||||||||||||||
| mysql: ["select", "with", "explain", "show", "describe", "desc"], | ||||||||||||||||||||||||||
| mariadb: ["select", "with", "explain", "show", "describe", "desc"], | ||||||||||||||||||||||||||
| sqlite: ["select", "with", "explain", "pragma"], | ||||||||||||||||||||||||||
| sqlserver: ["select", "with", "explain", "showplan"], | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Check if a SQL query is read-only based on its first keyword. | ||||||||||||||||||||||||||
| * Strips comments and strings before analyzing to avoid false positives. | ||||||||||||||||||||||||||
| * @param sql The SQL query to check | ||||||||||||||||||||||||||
| * @param connectorType The database type to check against | ||||||||||||||||||||||||||
| * @returns True if the query is read-only (starts with allowed keywords) | ||||||||||||||||||||||||||
| * Keywords that indicate data-modifying operations. | ||||||||||||||||||||||||||
| * Used to detect DML/DDL hidden inside CTEs or other constructs. | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| const mutatingKeywords = [ | ||||||||||||||||||||||||||
| "insert", | ||||||||||||||||||||||||||
| "update", | ||||||||||||||||||||||||||
| "delete", | ||||||||||||||||||||||||||
| "drop", | ||||||||||||||||||||||||||
| "alter", | ||||||||||||||||||||||||||
| "create", | ||||||||||||||||||||||||||
| "truncate", | ||||||||||||||||||||||||||
| "merge", | ||||||||||||||||||||||||||
| "grant", | ||||||||||||||||||||||||||
| "revoke", | ||||||||||||||||||||||||||
| "rename", | ||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** Base pattern: matches any mutating keyword as a whole word */ | ||||||||||||||||||||||||||
| const mutatingPattern = new RegExp( | ||||||||||||||||||||||||||
| `\\b(?:${mutatingKeywords.join("|")})\\b`, | ||||||||||||||||||||||||||
| "i", | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Extended pattern for dialects that support REPLACE INTO (MySQL/MariaDB/SQLite). | ||||||||||||||||||||||||||
| * Only matches `REPLACE INTO` (with optional LOW_PRIORITY/DELAYED), not | ||||||||||||||||||||||||||
| * REPLACE() function calls or identifiers named `replace`. | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| const mutatingPatternWithReplace = new RegExp( | ||||||||||||||||||||||||||
| `\\b(?:${mutatingKeywords.join("|")}|replace\\s+(?:(?:low_priority|delayed)\\s+)?into)\\b`, | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| * Extended pattern for dialects that support REPLACE INTO (MySQL/MariaDB/SQLite). | |
| * Only matches `REPLACE INTO` (with optional LOW_PRIORITY/DELAYED), not | |
| * REPLACE() function calls or identifiers named `replace`. | |
| */ | |
| const mutatingPatternWithReplace = new RegExp( | |
| `\\b(?:${mutatingKeywords.join("|")}|replace\\s+(?:(?:low_priority|delayed)\\s+)?into)\\b`, | |
| * Extended pattern for dialects that support REPLACE (MySQL/MariaDB/SQLite). | |
| * Matches REPLACE statements (including those without INTO) but not REPLACE() | |
| * function calls, by rejecting `replace` when it is immediately followed by `(`. | |
| */ | |
| const mutatingPatternWithReplace = new RegExp( | |
| `\\b(?:${mutatingKeywords.join("|")}|replace(?!\\s*\\()))\\b`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 87f75fe. Changed pattern to replace(?!\s*\() to catch all REPLACE statement forms (including REPLACE tbl SET ... without INTO). Accepts the minor false positive on identifiers named replace in MySQL/MariaDB/SQLite as a security tradeoff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There’s test coverage for
REPLACE INTO ..., but no test that asserts readonly rejection for the valid MySQL/MariaDB formREPLACE tbl_name SET ...(whereINTOis optional). Adding such a test would prevent regressions and would have caught the current bypass in the pattern matcher.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added in 87f75fe. Added tests for
REPLACE tbl SET ...(both standalone and inside WITH).