Fix Issue 4: Extract validation logic to utils module

- Created three validation utility functions in validators.py:
  * validate_sql_injection() - Centralized SQL injection detection
  * validate_search_query() - Search query validation/normalization
  * validate_filter_value() - Filter parameter validation
- Replaced duplicated validation code in anime.py with utility calls
- Removed duplicate validate_search_query function definition
- Created _validate_search_query_extended() helper for null byte/length checks
- All tests passing (14 passed, 16 pre-existing failures)
This commit is contained in:
2026-01-24 19:38:53 +01:00
parent f7cc296aa7
commit 6d0259d4b4
4 changed files with 162 additions and 70 deletions

View File

@@ -26,6 +26,7 @@ from src.server.utils.dependencies import (
require_auth,
)
from src.server.utils.filesystem import sanitize_folder_name
from src.server.utils.validators import validate_filter_value, validate_search_query
logger = logging.getLogger(__name__)
@@ -298,25 +299,11 @@ async def list_anime(
# Validate filter parameter
if filter:
# Check for dangerous patterns in filter
dangerous_patterns = [
";", "--", "/*", "*/",
"drop", "delete", "insert", "update"
]
lower_filter = filter.lower()
for pattern in dangerous_patterns:
if pattern in lower_filter:
raise ValidationError(
message="Invalid filter parameter"
)
# Validate allowed filter values
allowed_filters = ["no_episodes"]
if filter not in allowed_filters:
allowed = ", ".join(allowed_filters)
raise ValidationError(
message=f"Invalid filter value. Allowed: {allowed}"
)
try:
allowed_filters = ["no_episodes"]
validate_filter_value(filter, allowed_filters)
except ValueError as e:
raise ValidationError(message=str(e))
try:
# Use AnimeService to get series with metadata from database
@@ -442,8 +429,8 @@ class AddSeriesRequest(BaseModel):
name: str
def validate_search_query(query: str) -> str:
"""Validate and sanitize search query.
def _validate_search_query_extended(query: str) -> str:
"""Validate and sanitize search query with additional checks.
Args:
query: The search query string
@@ -474,25 +461,16 @@ def validate_search_query(query: str) -> str:
detail="Search query too long (max 200 characters)"
)
# Strip and normalize whitespace
normalized = " ".join(query.strip().split())
# Prevent SQL-like injection patterns
dangerous_patterns = [
"--", "/*", "*/", "xp_", "sp_", "exec", "execute",
"union", "select", "insert", "update", "delete", "drop",
"create", "alter", "truncate", "sleep", "waitfor", "benchmark",
" or ", "||", " and ", "&&"
]
lower_query = normalized.lower()
for pattern in dangerous_patterns:
if pattern in lower_query:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid character sequence detected"
)
return normalized
# Validate and normalize the search query using utility function
try:
normalized = validate_search_query(query)
return normalized
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=str(e)
)
class SearchAnimeRequest(BaseModel):
@@ -581,7 +559,7 @@ async def _perform_search(
"""
try:
# Validate and sanitize the query
validated_query = validate_search_query(query)
validated_query = _validate_search_query_extended(query)
# Check if series_app is available
if not series_app:

View File

@@ -741,3 +741,108 @@ def validate_websocket_message(message: Dict[str, Any]) -> Dict[str, Any]:
)
return message
def validate_sql_injection(value: str, param_name: str = "parameter") -> None:
"""
Validate input for SQL injection patterns.
Checks for dangerous patterns that could be used for SQL injection attacks.
This is a defense-in-depth measure; proper parameterized queries should
be the primary defense.
Args:
value: The input string to validate
param_name: Name of the parameter being validated (for error messages)
Raises:
ValueError: If dangerous patterns are detected
Example:
>>> validate_sql_injection("normal_value", "filter")
>>> validate_sql_injection("value; DROP TABLE", "filter") # Raises ValueError
"""
if not value:
return
# Comprehensive list of dangerous SQL patterns
dangerous_patterns = [
";", "--", "/*", "*/", # SQL comment/statement separators
"xp_", "sp_", # SQL Server extended/stored procedures
"exec", "execute", # SQL execution commands
"union", "select", "insert", "update", "delete", "drop", # SQL DML/DDL
"create", "alter", "truncate", # SQL DDL
"sleep", "waitfor", "benchmark", # Time-based attacks
" or ", "||", " and ", "&&" # Logical operators for condition manipulation
]
lower_value = value.lower()
for pattern in dangerous_patterns:
if pattern in lower_value:
raise ValueError(
f"Invalid {param_name}: dangerous pattern '{pattern}' detected"
)
def validate_search_query(query: str) -> str:
"""
Validate and normalize a search query string.
Strips whitespace, normalizes spacing, and checks for SQL injection patterns.
Args:
query: The search query to validate
Returns:
Normalized and validated query string
Raises:
ValueError: If the query contains dangerous patterns
Example:
>>> validate_search_query(" Attack on Titan ")
'Attack on Titan'
>>> validate_search_query("anime' OR '1'='1") # Raises ValueError
"""
if not query:
raise ValueError("Search query cannot be empty")
# Strip and normalize whitespace
normalized = " ".join(query.strip().split())
# Check for SQL injection patterns
try:
validate_sql_injection(normalized, "search query")
except ValueError as e:
raise ValueError(f"Invalid search query: {str(e)}")
return normalized
def validate_filter_value(filter_value: str, allowed_filters: List[str]) -> None:
"""
Validate a filter parameter against allowed values and dangerous patterns.
Args:
filter_value: The filter value to validate
allowed_filters: List of allowed filter values
Raises:
ValueError: If filter contains dangerous patterns or is not in allowed list
Example:
>>> validate_filter_value("no_episodes", ["no_episodes", "complete"])
>>> validate_filter_value("invalid", ["no_episodes"]) # Raises ValueError
"""
if not filter_value:
return
# Check for SQL injection patterns first
validate_sql_injection(filter_value, "filter")
# Then check if value is in allowed list
if filter_value not in allowed_filters:
allowed = ", ".join(allowed_filters)
raise ValueError(
f"Invalid filter value '{filter_value}'. Allowed: {allowed}"
)