-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathvalidator.py
More file actions
69 lines (53 loc) · 1.96 KB
/
Copy pathvalidator.py
File metadata and controls
69 lines (53 loc) · 1.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
"""SQL validator.
Guarantees (defense-in-depth on top of the read-only DB driver):
- Exactly one statement.
- Statement is a SELECT (no INSERT / UPDATE / DELETE / DDL / ATTACH / PRAGMA).
- No multi-statement injection via ';'.
- Adds a LIMIT clause if missing (DoS protection).
Returns the normalized SQL on success, raises `InvalidSQL` on failure.
"""
from __future__ import annotations
import sqlglot
from sqlglot import exp
class InvalidSQL(Exception):
pass
_FORBIDDEN_NODES = (
exp.Insert,
exp.Update,
exp.Delete,
exp.Create,
exp.Drop,
exp.Alter,
exp.AlterColumn,
exp.TruncateTable,
exp.Pragma,
exp.Attach,
exp.Command, # generic catch-all for VACUUM, REINDEX, ...
)
def validate_and_normalize(sql: str, *, max_limit: int) -> str:
sql = (sql or "").strip().rstrip(";").strip()
if not sql:
raise InvalidSQL("Empty SQL.")
try:
statements = sqlglot.parse(sql, read="sqlite")
except sqlglot.errors.ParseError as e:
raise InvalidSQL(f"Parse error: {e}") from e
statements = [s for s in statements if s is not None]
if len(statements) != 1:
raise InvalidSQL(
f"Expected exactly one statement, got {len(statements)}. "
"Do not chain statements with ';'."
)
stmt = statements[0]
if not isinstance(stmt, exp.Select) and stmt.find(exp.Select) is None:
raise InvalidSQL("Only SELECT statements are allowed.")
for node in stmt.walk():
if isinstance(node, _FORBIDDEN_NODES):
raise InvalidSQL(
f"Disallowed statement type: {type(node).__name__}. Only SELECT is permitted."
)
# Inject LIMIT if missing on the outermost SELECT.
outer = stmt if isinstance(stmt, exp.Select) else stmt.find(exp.Select)
if outer is not None and outer.args.get("limit") is None:
outer.set("limit", exp.Limit(expression=exp.Literal.number(max_limit)))
return stmt.sql(dialect="sqlite")