Spaces:
Paused
Paused
Upload core/validation.py with huggingface_hub
Browse files- core/validation.py +49 -42
core/validation.py
CHANGED
|
@@ -25,16 +25,16 @@ class InputValidationMiddleware(BaseHTTPMiddleware):
|
|
| 25 |
# Maximum request size (10MB)
|
| 26 |
MAX_REQUEST_SIZE = 10 * 1024 * 1024
|
| 27 |
|
| 28 |
-
#
|
| 29 |
-
|
| 30 |
-
r"(
|
| 31 |
re.IGNORECASE,
|
| 32 |
)
|
| 33 |
|
| 34 |
-
XSS_PATTERN = re.compile(r"(<script|javascript:|onerror=|onload=|<iframe|<embed|<object)", re.IGNORECASE)
|
| 35 |
-
|
| 36 |
# Path traversal detection
|
| 37 |
-
PATH_TRAVERSAL_PATTERN = re.compile(
|
|
|
|
|
|
|
| 38 |
|
| 39 |
# Command injection detection
|
| 40 |
COMMAND_INJECTION_PATTERN = re.compile(
|
|
@@ -79,7 +79,10 @@ class InputValidationMiddleware(BaseHTTPMiddleware):
|
|
| 79 |
if "multipart/form-data" in content_type:
|
| 80 |
# Allow multipart for file uploads, will validate individual parts
|
| 81 |
pass
|
| 82 |
-
elif
|
|
|
|
|
|
|
|
|
|
| 83 |
logger.warning(
|
| 84 |
"Invalid content type",
|
| 85 |
extra={
|
|
@@ -88,7 +91,9 @@ class InputValidationMiddleware(BaseHTTPMiddleware):
|
|
| 88 |
"client_ip": request.client.host if request.client else None,
|
| 89 |
},
|
| 90 |
)
|
| 91 |
-
raise HTTPException(
|
|
|
|
|
|
|
| 92 |
|
| 93 |
# Get request body for POST/PUT/PATCH
|
| 94 |
if request.method in ["POST", "PUT", "PATCH"]:
|
|
@@ -96,21 +101,6 @@ class InputValidationMiddleware(BaseHTTPMiddleware):
|
|
| 96 |
body = await request.body()
|
| 97 |
body_str = body.decode("utf-8")
|
| 98 |
|
| 99 |
-
# Check for SQL injection patterns
|
| 100 |
-
if self.SQL_INJECTION_PATTERN.search(body_str):
|
| 101 |
-
logger.error(
|
| 102 |
-
"SQL injection attempt detected",
|
| 103 |
-
extra={
|
| 104 |
-
"path": str(request.url.path),
|
| 105 |
-
"method": request.method,
|
| 106 |
-
"client_ip": (request.client.host if request.client else None),
|
| 107 |
-
},
|
| 108 |
-
)
|
| 109 |
-
raise HTTPException(
|
| 110 |
-
status_code=400,
|
| 111 |
-
detail="Invalid input: Potential SQL injection detected",
|
| 112 |
-
)
|
| 113 |
-
|
| 114 |
# Check for XSS patterns
|
| 115 |
if self.XSS_PATTERN.search(body_str):
|
| 116 |
logger.error(
|
|
@@ -118,10 +108,14 @@ class InputValidationMiddleware(BaseHTTPMiddleware):
|
|
| 118 |
extra={
|
| 119 |
"path": str(request.url.path),
|
| 120 |
"method": request.method,
|
| 121 |
-
"client_ip": (
|
|
|
|
|
|
|
| 122 |
},
|
| 123 |
)
|
| 124 |
-
raise HTTPException(
|
|
|
|
|
|
|
| 125 |
|
| 126 |
# Check for path traversal
|
| 127 |
if self.PATH_TRAVERSAL_PATTERN.search(body_str):
|
|
@@ -130,10 +124,14 @@ class InputValidationMiddleware(BaseHTTPMiddleware):
|
|
| 130 |
extra={
|
| 131 |
"path": str(request.url.path),
|
| 132 |
"method": request.method,
|
| 133 |
-
"client_ip": (
|
|
|
|
|
|
|
| 134 |
},
|
| 135 |
)
|
| 136 |
-
raise HTTPException(
|
|
|
|
|
|
|
| 137 |
|
| 138 |
# Check for command injection
|
| 139 |
if self.COMMAND_INJECTION_PATTERN.search(body_str):
|
|
@@ -142,7 +140,9 @@ class InputValidationMiddleware(BaseHTTPMiddleware):
|
|
| 142 |
extra={
|
| 143 |
"path": str(request.url.path),
|
| 144 |
"method": request.method,
|
| 145 |
-
"client_ip": (
|
|
|
|
|
|
|
| 146 |
},
|
| 147 |
)
|
| 148 |
raise HTTPException(
|
|
@@ -156,13 +156,6 @@ class InputValidationMiddleware(BaseHTTPMiddleware):
|
|
| 156 |
# Check query parameters for attacks
|
| 157 |
query_string = str(request.url.query)
|
| 158 |
if query_string:
|
| 159 |
-
if self.SQL_INJECTION_PATTERN.search(query_string):
|
| 160 |
-
logger.error(
|
| 161 |
-
"SQL injection in query parameters",
|
| 162 |
-
extra={"path": str(request.url.path), "query": query_string},
|
| 163 |
-
)
|
| 164 |
-
raise HTTPException(status_code=400, detail="Invalid query parameters")
|
| 165 |
-
|
| 166 |
if self.PATH_TRAVERSAL_PATTERN.search(query_string):
|
| 167 |
logger.error(
|
| 168 |
"Path traversal in query parameters",
|
|
@@ -230,7 +223,9 @@ class CaseStatus(str, Enum):
|
|
| 230 |
# Common validation patterns
|
| 231 |
SAFE_STRING_PATTERN = re.compile(r"^[a-zA-Z0-9\s\-_\.,!?()]+$")
|
| 232 |
EMAIL_PATTERN = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
|
| 233 |
-
UUID_PATTERN = re.compile(
|
|
|
|
|
|
|
| 234 |
|
| 235 |
|
| 236 |
def validate_safe_string(v: str) -> str:
|
|
@@ -277,7 +272,9 @@ class UserCreateRequest(BaseModel):
|
|
| 277 |
@classmethod
|
| 278 |
def username_alphanumeric(cls, v):
|
| 279 |
if not re.match(r"^[a-zA-Z0-9_-]+$", v):
|
| 280 |
-
raise ValueError(
|
|
|
|
|
|
|
| 281 |
return v
|
| 282 |
|
| 283 |
|
|
@@ -288,22 +285,32 @@ class UserLoginRequest(BaseModel):
|
|
| 288 |
|
| 289 |
class CaseCreateRequest(BaseModel):
|
| 290 |
title: constr(min_length=1, max_length=200) = Field(description="Case title")
|
| 291 |
-
description: constr(max_length=2000) | None = Field(
|
|
|
|
|
|
|
| 292 |
assigned_to: UUIDStr | None = Field(None, description="Assigned user ID")
|
| 293 |
|
| 294 |
|
| 295 |
class CaseUpdateRequest(BaseModel):
|
| 296 |
-
title: constr(min_length=1, max_length=200) | None = Field(
|
| 297 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
status: CaseStatus | None = Field(None, description="Case status")
|
| 299 |
assigned_to: UUIDStr | None = Field(None, description="Assigned user ID")
|
| 300 |
|
| 301 |
|
| 302 |
class EvidenceUploadRequest(BaseModel):
|
| 303 |
case_id: UUIDStr = Field(description="Case ID")
|
| 304 |
-
filename: constr(min_length=1, max_length=255) = Field(
|
|
|
|
|
|
|
| 305 |
file_type: SafeString = Field(description="MIME type")
|
| 306 |
-
size_bytes: int = Field(
|
|
|
|
|
|
|
| 307 |
|
| 308 |
@field_validator("file_type")
|
| 309 |
@classmethod
|
|
|
|
| 25 |
# Maximum request size (10MB)
|
| 26 |
MAX_REQUEST_SIZE = 10 * 1024 * 1024
|
| 27 |
|
| 28 |
+
# XSS pattern detection - targets actual XSS attack vectors
|
| 29 |
+
XSS_PATTERN = re.compile(
|
| 30 |
+
r"(<script|javascript:|data:text/html|<iframe|<object|<embed|on\w+=)",
|
| 31 |
re.IGNORECASE,
|
| 32 |
)
|
| 33 |
|
|
|
|
|
|
|
| 34 |
# Path traversal detection
|
| 35 |
+
PATH_TRAVERSAL_PATTERN = re.compile(
|
| 36 |
+
r"(\.\./|\.\.\\|%2e%2e%2f|%2e%2e/|\.\.%2f|%2e%2e%5c)", re.IGNORECASE
|
| 37 |
+
)
|
| 38 |
|
| 39 |
# Command injection detection
|
| 40 |
COMMAND_INJECTION_PATTERN = re.compile(
|
|
|
|
| 79 |
if "multipart/form-data" in content_type:
|
| 80 |
# Allow multipart for file uploads, will validate individual parts
|
| 81 |
pass
|
| 82 |
+
elif (
|
| 83 |
+
content_type not in self.ALLOWED_CONTENT_TYPES
|
| 84 |
+
and content_type != "application/x-www-form-urlencoded"
|
| 85 |
+
):
|
| 86 |
logger.warning(
|
| 87 |
"Invalid content type",
|
| 88 |
extra={
|
|
|
|
| 91 |
"client_ip": request.client.host if request.client else None,
|
| 92 |
},
|
| 93 |
)
|
| 94 |
+
raise HTTPException(
|
| 95 |
+
status_code=415, detail=f"Unsupported content type: {content_type}"
|
| 96 |
+
)
|
| 97 |
|
| 98 |
# Get request body for POST/PUT/PATCH
|
| 99 |
if request.method in ["POST", "PUT", "PATCH"]:
|
|
|
|
| 101 |
body = await request.body()
|
| 102 |
body_str = body.decode("utf-8")
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
# Check for XSS patterns
|
| 105 |
if self.XSS_PATTERN.search(body_str):
|
| 106 |
logger.error(
|
|
|
|
| 108 |
extra={
|
| 109 |
"path": str(request.url.path),
|
| 110 |
"method": request.method,
|
| 111 |
+
"client_ip": (
|
| 112 |
+
request.client.host if request.client else None
|
| 113 |
+
),
|
| 114 |
},
|
| 115 |
)
|
| 116 |
+
raise HTTPException(
|
| 117 |
+
status_code=400, detail="Invalid input: Potential XSS detected"
|
| 118 |
+
)
|
| 119 |
|
| 120 |
# Check for path traversal
|
| 121 |
if self.PATH_TRAVERSAL_PATTERN.search(body_str):
|
|
|
|
| 124 |
extra={
|
| 125 |
"path": str(request.url.path),
|
| 126 |
"method": request.method,
|
| 127 |
+
"client_ip": (
|
| 128 |
+
request.client.host if request.client else None
|
| 129 |
+
),
|
| 130 |
},
|
| 131 |
)
|
| 132 |
+
raise HTTPException(
|
| 133 |
+
status_code=400, detail="Invalid input: Path traversal detected"
|
| 134 |
+
)
|
| 135 |
|
| 136 |
# Check for command injection
|
| 137 |
if self.COMMAND_INJECTION_PATTERN.search(body_str):
|
|
|
|
| 140 |
extra={
|
| 141 |
"path": str(request.url.path),
|
| 142 |
"method": request.method,
|
| 143 |
+
"client_ip": (
|
| 144 |
+
request.client.host if request.client else None
|
| 145 |
+
),
|
| 146 |
},
|
| 147 |
)
|
| 148 |
raise HTTPException(
|
|
|
|
| 156 |
# Check query parameters for attacks
|
| 157 |
query_string = str(request.url.query)
|
| 158 |
if query_string:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
if self.PATH_TRAVERSAL_PATTERN.search(query_string):
|
| 160 |
logger.error(
|
| 161 |
"Path traversal in query parameters",
|
|
|
|
| 223 |
# Common validation patterns
|
| 224 |
SAFE_STRING_PATTERN = re.compile(r"^[a-zA-Z0-9\s\-_\.,!?()]+$")
|
| 225 |
EMAIL_PATTERN = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
|
| 226 |
+
UUID_PATTERN = re.compile(
|
| 227 |
+
r"^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$"
|
| 228 |
+
)
|
| 229 |
|
| 230 |
|
| 231 |
def validate_safe_string(v: str) -> str:
|
|
|
|
| 272 |
@classmethod
|
| 273 |
def username_alphanumeric(cls, v):
|
| 274 |
if not re.match(r"^[a-zA-Z0-9_-]+$", v):
|
| 275 |
+
raise ValueError(
|
| 276 |
+
"Username must be alphanumeric with underscores and hyphens only"
|
| 277 |
+
)
|
| 278 |
return v
|
| 279 |
|
| 280 |
|
|
|
|
| 285 |
|
| 286 |
class CaseCreateRequest(BaseModel):
|
| 287 |
title: constr(min_length=1, max_length=200) = Field(description="Case title")
|
| 288 |
+
description: constr(max_length=2000) | None = Field(
|
| 289 |
+
None, description="Case description"
|
| 290 |
+
)
|
| 291 |
assigned_to: UUIDStr | None = Field(None, description="Assigned user ID")
|
| 292 |
|
| 293 |
|
| 294 |
class CaseUpdateRequest(BaseModel):
|
| 295 |
+
title: constr(min_length=1, max_length=200) | None = Field(
|
| 296 |
+
None, description="Case title"
|
| 297 |
+
)
|
| 298 |
+
description: constr(max_length=2000) | None = Field(
|
| 299 |
+
None, description="Case description"
|
| 300 |
+
)
|
| 301 |
status: CaseStatus | None = Field(None, description="Case status")
|
| 302 |
assigned_to: UUIDStr | None = Field(None, description="Assigned user ID")
|
| 303 |
|
| 304 |
|
| 305 |
class EvidenceUploadRequest(BaseModel):
|
| 306 |
case_id: UUIDStr = Field(description="Case ID")
|
| 307 |
+
filename: constr(min_length=1, max_length=255) = Field(
|
| 308 |
+
description="Original filename"
|
| 309 |
+
)
|
| 310 |
file_type: SafeString = Field(description="MIME type")
|
| 311 |
+
size_bytes: int = Field(
|
| 312 |
+
ge=0, le=100 * 1024 * 1024, description="File size in bytes"
|
| 313 |
+
) # Max 100MB
|
| 314 |
|
| 315 |
@field_validator("file_type")
|
| 316 |
@classmethod
|