Skip to content

Commit 86a2bf4

Browse files
committed
fix: add loop boundary injection protection to forecasting endpoints
Security fixes: - Add validation for horizon_days parameter (max 365 days) to prevent DoS attacks - Add validation for batch SKU list size (max 100 SKUs) to prevent DoS attacks - Implement defense in depth: validation at Pydantic model, service method, and endpoint levels - Add Field constraints and field_validator decorators for input validation - Add logging when limits are exceeded This prevents attackers from causing denial of service by: - Setting extremely large horizon_days values in forecast requests - Sending batch requests with thousands of SKUs - Exploiting loop boundaries to exhaust server resources
1 parent e4772a6 commit 86a2bf4

1 file changed

Lines changed: 57 additions & 4 deletions

File tree

src/api/routers/advanced_forecasting.py

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from dataclasses import dataclass
1818
import os
1919
from fastapi import APIRouter, HTTPException, BackgroundTasks
20-
from pydantic import BaseModel
20+
from pydantic import BaseModel, Field, field_validator
2121
# from src.api.services.forecasting_config import get_config, load_config_from_db
2222
import redis
2323
import asyncio
@@ -29,13 +29,52 @@
2929
# Pydantic models for API
3030
class ForecastRequest(BaseModel):
3131
sku: str
32-
horizon_days: int = 30
32+
horizon_days: int = Field(default=30, ge=1, le=365, description="Forecast horizon in days (1-365)")
3333
include_confidence_intervals: bool = True
3434
include_feature_importance: bool = True
35+
36+
@field_validator('horizon_days')
37+
@classmethod
38+
def validate_horizon_days(cls, v: int) -> int:
39+
"""Validate and restrict horizon_days to prevent loop boundary injection attacks."""
40+
# Enforce maximum limit to prevent DoS attacks
41+
MAX_HORIZON_DAYS = 365
42+
if v > MAX_HORIZON_DAYS:
43+
logger.warning(f"horizon_days {v} exceeds maximum {MAX_HORIZON_DAYS}, restricting to {MAX_HORIZON_DAYS}")
44+
return MAX_HORIZON_DAYS
45+
if v < 1:
46+
raise ValueError("horizon_days must be at least 1")
47+
return v
3548

3649
class BatchForecastRequest(BaseModel):
37-
skus: List[str]
38-
horizon_days: int = 30
50+
skus: List[str] = Field(..., min_length=1, max_length=100, description="List of SKUs to forecast (max 100)")
51+
horizon_days: int = Field(default=30, ge=1, le=365, description="Forecast horizon in days (1-365)")
52+
53+
@field_validator('horizon_days')
54+
@classmethod
55+
def validate_horizon_days(cls, v: int) -> int:
56+
"""Validate and restrict horizon_days to prevent loop boundary injection attacks."""
57+
# Enforce maximum limit to prevent DoS attacks
58+
MAX_HORIZON_DAYS = 365
59+
if v > MAX_HORIZON_DAYS:
60+
logger.warning(f"horizon_days {v} exceeds maximum {MAX_HORIZON_DAYS}, restricting to {MAX_HORIZON_DAYS}")
61+
return MAX_HORIZON_DAYS
62+
if v < 1:
63+
raise ValueError("horizon_days must be at least 1")
64+
return v
65+
66+
@field_validator('skus')
67+
@classmethod
68+
def validate_skus(cls, v: List[str]) -> List[str]:
69+
"""Validate and restrict SKU list size to prevent DoS attacks."""
70+
# Enforce maximum limit to prevent DoS attacks from large batch requests
71+
MAX_SKUS = 100
72+
if len(v) > MAX_SKUS:
73+
logger.warning(f"SKU list size {len(v)} exceeds maximum {MAX_SKUS}, restricting to first {MAX_SKUS} SKUs")
74+
return v[:MAX_SKUS]
75+
if len(v) == 0:
76+
raise ValueError("SKU list cannot be empty")
77+
return v
3978

4079
class ReorderRecommendation(BaseModel):
4180
sku: str
@@ -102,6 +141,14 @@ async def initialize(self):
102141

103142
async def get_real_time_forecast(self, sku: str, horizon_days: int = 30) -> Dict[str, Any]:
104143
"""Get real-time forecast with caching"""
144+
# Security: Validate and restrict horizon_days to prevent loop boundary injection attacks
145+
MAX_HORIZON_DAYS = 365
146+
if horizon_days > MAX_HORIZON_DAYS:
147+
logger.warning(f"horizon_days {horizon_days} exceeds maximum {MAX_HORIZON_DAYS}, restricting to {MAX_HORIZON_DAYS}")
148+
horizon_days = MAX_HORIZON_DAYS
149+
if horizon_days < 1:
150+
raise ValueError("horizon_days must be at least 1")
151+
105152
cache_key = f"forecast:{sku}:{horizon_days}"
106153

107154
# Check cache first
@@ -1141,6 +1188,12 @@ async def batch_forecast(request: BatchForecastRequest):
11411188
if not request.skus or len(request.skus) == 0:
11421189
raise HTTPException(status_code=400, detail="SKU list cannot be empty")
11431190

1191+
# Security: Additional validation to prevent DoS attacks from large batch requests
1192+
MAX_SKUS = 100
1193+
if len(request.skus) > MAX_SKUS:
1194+
logger.warning(f"Batch request contains {len(request.skus)} SKUs, restricting to first {MAX_SKUS}")
1195+
request.skus = request.skus[:MAX_SKUS]
1196+
11441197
await forecasting_service.initialize()
11451198

11461199
forecasts = {}

0 commit comments

Comments
 (0)