SQL Injection in LLM Systems
Critical security vulnerability in LLM applications where user-controlled metadata filters or search parameters are directly interpolated into SQL queries without proper parameterization, enabling attackers to execute arbitrary database operations.
Vulnerability Patterns in RAG Systems
Direct String Interpolation
# VULNERABLE: Direct value interpolation
def _build_filter_clause(self, filters):
clause = f"metadata = '{filters['value']}'" # SQL injection risk
return clause
Unparameterized IN Clauses
# VULNERABLE: List values directly embedded
def build_filter(self, values):
value_str = "', '".join(values)
return f"column IN ('{value_str}')" # Injection through crafted list items
JSON Metadata Filtering
# VULNERABLE: User-controlled JSON metadata
def filter_by_metadata(self, key, value):
query = f"SELECT * FROM docs WHERE metadata->>{key} = '{value}'"
# Attacker controls both key and value
Attack Vectors
Quote Escape Attacks
# Attacker input: "'; DROP TABLE documents; --"
# Resulting query: SELECT * FROM docs WHERE field = ''; DROP TABLE documents; --'
Boolean Logic Manipulation
# Attacker input: "' OR 1=1 --"
# Resulting query: SELECT * FROM docs WHERE field = '' OR 1=1 --'
# Returns all documents regardless of intended filtering
Nested JSON Exploitation
# Attacker input for JSON key: "'; DELETE FROM embeddings; SELECT 'x"
# Malicious query execution through JSON operator injection
LLM System Specific Risks
Metadata-Driven Retrieval
RAG systems often use metadata filtering to scope retrieval results. User queries may indirectly control metadata values, creating injection opportunities:
# User asks: "Show me documents from ministry '; DROP TABLE docs; --"
# System generates metadata filter with user input
ministry_filter = user_input # Dangerous!
Multi-Source Federation
Systems querying multiple databases with different schemas increase attack surface:
# Different databases, same vulnerability pattern
postgres_query = f"WHERE ministry = '{user_ministry}'"
sqlite_query = f"WHERE dept = '{user_ministry}'" # Same injection risk
Dynamic Query Construction
LLM systems often build queries dynamically based on user intent:
# AI agent constructs query based on user request
if user_wants_recent:
date_filter = f"date > '{user_date}'" # User controls date format
Case Study: Assistant-RH Vulnerabilities
The assistant-rh system exhibited multiple SQL injection vulnerabilities:
Metadata Filter Injection
# retriever.py:69 - Direct string interpolation
def _build_filter_clause(self, metadata_filters):
for key, value in metadata_filters.items():
clauses.append(f"metadata->>'{key}' = '{value}'") # VULNERABLE
List Value Injection
# IN clause construction with user-controlled values
values = "', '".join(filter_values) # No escaping
query = f"field IN ('{values}')" # Injection through list items
Compound Attack Surface
The system's multi-source architecture multiplied the attack surface, with similar vulnerabilities across PostgreSQL and SQLite implementations.
Mitigation Strategies
Parameterized Queries
# SECURE: Use parameterized queries
def build_filter_secure(self, key, value):
query = "SELECT * FROM docs WHERE metadata->>%s = %s"
return self.execute(query, (key, value))
Input Validation
# SECURE: Validate metadata keys against whitelist
ALLOWED_METADATA_KEYS = {'ministry', 'department', 'date', 'category'}
def validate_metadata_key(self, key):
if key not in ALLOWED_METADATA_KEYS:
raise ValueError(f"Invalid metadata key: {key}")
JSON-Specific Protections
# SECURE: Proper JSON parameter binding
def filter_json_metadata(self, key, value):
# Validate key is safe for JSON operator
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', key):
raise ValueError("Invalid JSON key format")
# Use proper parameterization
query = "SELECT * FROM docs WHERE metadata->>%s = %s"
return self.execute(query, (key, value))
Query Builder Abstractions
# SECURE: Use ORM or query builder
from sqlalchemy import and_, or_
def build_secure_filter(self, filters):
conditions = []
for key, value in filters.items():
# ORM handles parameterization automatically
conditions.append(self.model.metadata[key] == value)
return and_(*conditions)
Detection and Prevention
Code Review Patterns
Look for these patterns in code reviews:
- String formatting in SQL queries (
f"...",.format(),%formatting) - Direct concatenation of user input into queries
- Raw SQL construction without parameterization
Static Analysis
# Custom linter rules
def check_sql_injection(node):
if isinstance(node, ast.JoinedStr): # f-string
if any('SELECT' in part or 'WHERE' in part for part in extract_strings(node)):
return "Potential SQL injection in f-string"
Testing Strategies
def test_sql_injection_protection():
malicious_inputs = [
"'; DROP TABLE docs; --",
"' OR 1=1 --",
"' UNION SELECT password FROM users --"
]
for malicious_input in malicious_inputs:
# Should not modify database or return unexpected results
result = search_with_filter('ministry', malicious_input)
assert_no_injection_occurred(result)
Production Hardening
Database Permissions
- Use dedicated database users with minimal required permissions
- Avoid connections with DDL (DROP, CREATE) privileges for application queries
- Implement row-level security where applicable
Query Monitoring
# Log and monitor for injection patterns
def log_suspicious_queries(query, params):
suspicious_patterns = ['DROP', 'DELETE', 'UPDATE', 'UNION', '--', ';']
if any(pattern in query.upper() for pattern in suspicious_patterns):
logger.warning(f"Suspicious query detected: {query[:100]}")
Defense in Depth
- Input validation at multiple layers
- Database-level query restrictions
- Application-level parameterization
- Regular security audits
See also
- assistant-rh - Case study system with SQL injection vulnerabilities
- undefined-references - Related production code quality issues
- module-loading-failures - Other critical system failures
- database-security - Broader database security practices
- input-validation - General input validation strategies