Lukynnnn's picture
Upload mcp_database_universal/safety.py with huggingface_hub
40a5645 verified
Raw
History Blame Contribute Delete
4.79 kB
"""Safety layer for SQL query validation and enforcement."""
import re
from enum import Enum
from dataclasses import dataclass
class QueryType(Enum):
SAFE_READ = "safe_read"
UNSAFE_READ = "unsafe_read"
WRITE = "write"
DANGEROUS = "dangerous"
UNKNOWN = "unknown"
@dataclass
class SafetyResult:
approved: bool
query_type: QueryType
reason: str
SAFE_READ_PATTERNS = [
re.compile(r"^\s*SELECT\b", re.IGNORECASE),
re.compile(r"^\s*EXPLAIN\b", re.IGNORECASE),
re.compile(r"^\s*PRAGMA\b", re.IGNORECASE),
re.compile(r"^\s*SHOW\b", re.IGNORECASE),
re.compile(r"^\s*DESCRIBE\b", re.IGNORECASE),
re.compile(r"^\s*WITH\b.*\bSELECT\b", re.IGNORECASE | re.DOTALL),
]
WRITE_PATTERNS = [
re.compile(r"^\s*INSERT\b", re.IGNORECASE),
re.compile(r"^\s*UPDATE\b", re.IGNORECASE),
re.compile(r"^\s*DELETE\b", re.IGNORECASE),
re.compile(r"^\s*REPLACE\b", re.IGNORECASE),
re.compile(r"^\s*MERGE\b", re.IGNORECASE),
]
DANGEROUS_PATTERNS = [
re.compile(r"^\s*DROP\b", re.IGNORECASE),
re.compile(r"^\s*ALTER\b", re.IGNORECASE),
re.compile(r"^\s*CREATE\b", re.IGNORECASE),
re.compile(r"^\s*TRUNCATE\b", re.IGNORECASE),
re.compile(r"^\s*GRANT\b", re.IGNORECASE),
re.compile(r"^\s*REVOKE\b", re.IGNORECASE),
]
SQL_INJECTION_PATTERNS = [
re.compile(r"(?:--|#|/\*)", re.IGNORECASE), # comments
re.compile(r";\s*\w", re.IGNORECASE), # multiple statements
re.compile(r"'\s*OR\s+'", re.IGNORECASE), # ' OR '1'='1
re.compile(r"'\s*OR\s+\"", re.IGNORECASE), # ' OR "1"="1
re.compile(r"UNION\s+ALL\s+SELECT", re.IGNORECASE),
re.compile(r"INTO\s+OUTFILE", re.IGNORECASE),
re.compile(r"LOAD_FILE\s*\(", re.IGNORECASE),
re.compile(r"BENCHMARK\s*\(", re.IGNORECASE),
re.compile(r"SLEEP\s*\(", re.IGNORECASE),
re.compile(r"WAITFOR\s+DELAY", re.IGNORECASE),
]
class SafetyValidator:
def __init__(self, read_only: bool = True, max_rows: int = 1000, max_query_time: int = 30):
self.read_only = read_only
self.max_rows = max_rows
self.max_query_time = max_query_time
def classify(self, sql: str) -> QueryType:
stripped = sql.strip()
if not stripped:
return QueryType.UNKNOWN
for pattern in DANGEROUS_PATTERNS:
if pattern.search(stripped):
return QueryType.DANGEROUS
for pattern in WRITE_PATTERNS:
if pattern.search(stripped):
return QueryType.WRITE
for pattern in SAFE_READ_PATTERNS:
if pattern.search(stripped):
return QueryType.SAFE_READ
return QueryType.UNKNOWN
def check_injection(self, sql: str) -> str | None:
for pattern in SQL_INJECTION_PATTERNS:
if pattern.search(sql):
return f"Suspected SQL injection pattern detected"
return None
def validate(self, sql: str) -> SafetyResult:
if not sql or not sql.strip():
return SafetyResult(
approved=False,
query_type=QueryType.UNKNOWN,
reason="Empty query",
)
injection = self.check_injection(sql)
if injection:
return SafetyResult(
approved=False,
query_type=QueryType.UNKNOWN,
reason=injection,
)
qtype = self.classify(sql)
if self.read_only and qtype in (QueryType.WRITE, QueryType.DANGEROUS):
return SafetyResult(
approved=False,
query_type=qtype,
reason=f"Query type '{qtype.value}' is not allowed in read-only mode. "
f"Set DATABASE_WRITE_ENABLED=true to allow write operations.",
)
if qtype == QueryType.UNKNOWN:
return SafetyResult(
approved=False,
query_type=qtype,
reason=f"Unrecognized query type. Only SELECT, EXPLAIN, PRAGMA, SHOW, DESCRIBE are allowed.",
)
if qtype == QueryType.UNSAFE_READ:
return SafetyResult(
approved=True,
query_type=qtype,
reason="Query approved with warning: contains potentially unsafe read operations",
)
return SafetyResult(
approved=True,
query_type=qtype,
reason="Query approved",
)
def ensure_limit(self, sql: str) -> str:
if self.max_rows <= 0:
return sql
stripped = sql.strip().rstrip(";")
lower = stripped.lower()
if "limit " in lower or "top " in lower:
return sql
if lower.startswith("select"):
return f"{stripped} LIMIT {self.max_rows}"
return sql