|
17 | 17 | from dataclasses import dataclass |
18 | 18 | import os |
19 | 19 | from fastapi import APIRouter, HTTPException, BackgroundTasks |
20 | | -from pydantic import BaseModel |
| 20 | +from pydantic import BaseModel, Field, field_validator |
21 | 21 | # from src.api.services.forecasting_config import get_config, load_config_from_db |
22 | 22 | import redis |
23 | 23 | import asyncio |
|
29 | 29 | # Pydantic models for API |
30 | 30 | class ForecastRequest(BaseModel): |
31 | 31 | 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)") |
33 | 33 | include_confidence_intervals: bool = True |
34 | 34 | 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 |
35 | 48 |
|
36 | 49 | 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 |
39 | 78 |
|
40 | 79 | class ReorderRecommendation(BaseModel): |
41 | 80 | sku: str |
@@ -102,6 +141,14 @@ async def initialize(self): |
102 | 141 |
|
103 | 142 | async def get_real_time_forecast(self, sku: str, horizon_days: int = 30) -> Dict[str, Any]: |
104 | 143 | """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 | + |
105 | 152 | cache_key = f"forecast:{sku}:{horizon_days}" |
106 | 153 |
|
107 | 154 | # Check cache first |
@@ -1141,6 +1188,12 @@ async def batch_forecast(request: BatchForecastRequest): |
1141 | 1188 | if not request.skus or len(request.skus) == 0: |
1142 | 1189 | raise HTTPException(status_code=400, detail="SKU list cannot be empty") |
1143 | 1190 |
|
| 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 | + |
1144 | 1197 | await forecasting_service.initialize() |
1145 | 1198 |
|
1146 | 1199 | forecasts = {} |
|
0 commit comments