|
|
|
@ -193,27 +193,35 @@ class RequestValidationMiddleware(BaseHTTPMiddleware):
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# -------------------------
|
|
|
|
# -------------------------
|
|
|
|
# 4. Single source enforcement
|
|
|
|
# 4. JSON body inspection & Single source enforcement
|
|
|
|
# Ensuring data comes from ONLY one source (Query OR Body).
|
|
|
|
# Ensuring data comes from ONLY one source (Query OR Body).
|
|
|
|
# -------------------------
|
|
|
|
# -------------------------
|
|
|
|
content_type = request.headers.get("content-type", "")
|
|
|
|
content_type = request.headers.get("content-type", "")
|
|
|
|
has_json_body = content_type.startswith("application/json")
|
|
|
|
has_json_header = content_type.startswith("application/json")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Read body now so we can check if it's actually empty
|
|
|
|
|
|
|
|
body = b""
|
|
|
|
|
|
|
|
if has_json_header:
|
|
|
|
|
|
|
|
body = await request.body()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# We consider it a "JSON body" source ONLY if it's not empty and not just "{}"
|
|
|
|
|
|
|
|
has_actual_json_body = has_json_header and body and body.strip() != b"{}"
|
|
|
|
|
|
|
|
|
|
|
|
# Check for data parameters in query (anything whitelisted as 'data' but not 'session/auth')
|
|
|
|
# Check for data parameters in query (anything whitelisted as 'data' but not 'session/auth')
|
|
|
|
data_params_in_query = [
|
|
|
|
data_params_in_query = [
|
|
|
|
key for key, _ in params
|
|
|
|
key for key, _ in params
|
|
|
|
if key in ALLOWED_QUERY_PARAMS and key not in WRITE_METHOD_ALLOWED_PARAMS
|
|
|
|
if key in ALLOWED_QUERY_PARAMS and key not in WRITE_METHOD_ALLOWED_PARAMS
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
if has_json_body:
|
|
|
|
if has_actual_json_body:
|
|
|
|
# If sending JSON body, we forbid any data in query string (one source only)
|
|
|
|
# If sending actual JSON body, we forbid any data in query string (one source only)
|
|
|
|
if data_params_in_query:
|
|
|
|
if data_params_in_query:
|
|
|
|
raise HTTPException(
|
|
|
|
raise HTTPException(
|
|
|
|
status_code=422,
|
|
|
|
status_code=422,
|
|
|
|
detail=f"Single source enforcement: Data received from both JSON body and query string ({data_params_in_query}). Use only one source.",
|
|
|
|
detail=f"Single source enforcement: Data received from both JSON body and query string ({data_params_in_query}). Use only one source.",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Special case: GET with body is discouraged/forbidden in many strict security contexts
|
|
|
|
# Special case: GET with actual body is discouraged/forbidden
|
|
|
|
if request.method == "GET":
|
|
|
|
if request.method == "GET":
|
|
|
|
raise HTTPException(
|
|
|
|
raise HTTPException(
|
|
|
|
status_code=422,
|
|
|
|
status_code=422,
|
|
|
|
@ -265,17 +273,9 @@ class RequestValidationMiddleware(BaseHTTPMiddleware):
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# -------------------------
|
|
|
|
# -------------------------
|
|
|
|
# 7. JSON body inspection
|
|
|
|
# 7. JSON body inspection & Re-injection
|
|
|
|
# -------------------------
|
|
|
|
# -------------------------
|
|
|
|
if content_type.startswith("application/json"):
|
|
|
|
if has_json_header:
|
|
|
|
body = await request.body()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#if len(body) > MAX_JSON_BODY_SIZE:
|
|
|
|
|
|
|
|
# raise HTTPException(
|
|
|
|
|
|
|
|
# status_code=413,
|
|
|
|
|
|
|
|
# detail="JSON body too large",
|
|
|
|
|
|
|
|
# )
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if body:
|
|
|
|
if body:
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
payload = json.loads(body)
|
|
|
|
payload = json.loads(body)
|
|
|
|
@ -284,13 +284,11 @@ class RequestValidationMiddleware(BaseHTTPMiddleware):
|
|
|
|
status_code=422,
|
|
|
|
status_code=422,
|
|
|
|
detail="Invalid JSON body",
|
|
|
|
detail="Invalid JSON body",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
inspect_json(payload)
|
|
|
|
inspect_json(payload)
|
|
|
|
|
|
|
|
|
|
|
|
# Re-inject body for downstream handlers
|
|
|
|
# Re-inject body for downstream handlers
|
|
|
|
async def receive():
|
|
|
|
async def receive():
|
|
|
|
return {"type": "http.request", "body": body}
|
|
|
|
return {"type": "http.request", "body": body}
|
|
|
|
|
|
|
|
|
|
|
|
request._receive = receive # noqa: protected-access
|
|
|
|
request._receive = receive # noqa: protected-access
|
|
|
|
|
|
|
|
|
|
|
|
return await call_next(request)
|
|
|
|
return await call_next(request)
|
|
|
|
|