Null Pointer Dereference in Fastapi
How Null Pointer Dereference Manifests in Fastapi
Null pointer dereference in Fastapi applications occurs when code attempts to access properties or methods on objects that are None, leading to runtime exceptions. In Fastapi's async context, these failures can propagate through middleware and exception handlers, creating unique attack surfaces.
Fastapi's dependency injection system creates specific patterns where null dereferences commonly occur. Consider a dependency that fetches a database record:
from fastapi import Depends, HTTPException
from sqlalchemy.orm import Session
from database import get_db
from models import User
def get_user(user_id: int, db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
return user
@app.get('/user/profile')
def read_user_profile(user: User = Depends(get_user)):
return {
'name': user.name, # Crash if user is None
'email': user.email # Crash if user is None
}This pattern fails catastrophically when the user doesn't exist. Fastapi's dependency system doesn't automatically handle None returns, causing the endpoint to throw an unhandled exception.
Path parameter coercion adds another attack vector. Fastapi automatically converts path parameters to specified types, but fails silently on invalid conversions:
@app.get('/items/{item_id}')
def get_item(item_id: int):
item = items_db.get(item_id) # item_id might be None after failed coercion
return {'id': item.id, 'name': item.name} # Crashes if item is NoneWhen clients provide non-integer values for integer parameters, Fastapi may return None, leading to dereference failures.
Query parameter handling creates similar issues. Optional parameters default to None, but code often assumes they're populated:
@app.get('/search')
def search_items(category: str = None, limit: int = 10):
if category.lower() == 'electronics': # Crash if category is None
return filter_items(category)
return filter_items('all')Request body parsing with Pydantic models introduces another failure mode. When clients send incomplete or malformed JSON, Fastapi creates models with None fields:
from pydantic import BaseModel
class OrderRequest(BaseModel):
product_id: int
quantity: int
discount_code: str = None
@app.post('/order')
def create_order(request: OrderRequest):
discount = discount_service.get_discount(request.discount_code)
# Crash if discount_code is None and service doesn't handle it
return {'total': calculate_total(request.quantity, discount)}Middleware and exception handlers aren't immune. A global exception handler that assumes certain attributes exist can itself crash:
@app.exception_handler(Exception)
def global_exception_handler(request: Request, exc: Exception):
logger.error(f"Request from {request.client.host} failed") # request.client might be None
return JSONResponse(status_code=500, content={'error': 'Internal Server Error'})Fastapi-Specific Detection
Detecting null pointer dereferences in Fastapi requires understanding its async execution model and dependency injection patterns. Static analysis tools often miss these issues because they depend on runtime data flow.
middleBrick's Fastapi-specific scanner identifies dereference vulnerabilities by analyzing the actual request/response cycle. It sends crafted requests that trigger edge cases:
# Example of what middleBrick tests for:
# 1. Non-existent database records
response = client.get('/user/profile', params={'user_id': 999999})
# 2. Invalid type coercion
response = client.get('/items/abc') # abc cannot convert to int
# 3. Missing request body fields
response = client.post('/order', json={'product_id': 123}) # quantity missing
# 4. Empty optional parameters
response = client.get('/search?category=') # empty string parameterThe scanner examines HTTP status codes and response bodies to detect unhandled exceptions. Fastapi's default behavior is to return 500 errors with stack traces, which middleBrick flags as potential dereference issues.
middleBrick's OpenAPI analysis component cross-references your API specification with runtime behavior. It identifies dependencies that might return None but are used without null checks:
# middleBrick detects this pattern:
@app.get('/user/{user_id}')
def get_user(user_id: int = Depends(get_user)):
# If get_user returns None, this crashes
return {'user': {'id': user.id, 'name': user.name}}The scanner also tests Fastapi's exception handling infrastructure. Custom exception handlers that assume certain attributes exist are flagged:
# middleBrick tests if exception handlers are null-safe:
@app.exception_handler(ValueError)
def value_error_handler(request: Request, exc: ValueError):
# request.client might be None for certain request types
client_ip = request.client.host if request.client else 'unknown'
return JSONResponse(status_code=400, content={'error': str(exc), 'client_ip': client_ip})middleBrick's LLM security module specifically tests for null pointer dereferences in AI-powered endpoints. When Fastapi applications use LLM APIs, null responses from AI services can cause crashes:
@app.post('/chat/completion')
async def chat_completion(prompt: str):
response = await llm_client.generate(prompt)
# If response is None, next line crashes
return {'content': response['choices'][0]['message']['content']}Fastapi-Specific Remediation
Fastapi provides several native patterns for preventing null pointer dereferences. The most robust approach uses Fastapi's exception handling system to convert None values into proper HTTP responses.
For database dependencies, implement null checks that raise HTTP exceptions:
from fastapi import HTTPException, status
def get_user(user_id: int, db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f'User {user_id} not found'
)
return user
@app.get('/user/profile')
def read_user_profile(user: User = Depends(get_user)):
# Guaranteed to have valid User object
return {'name': user.name, 'email': user.email}Path parameter validation can use Fastapi's custom validators to prevent invalid conversions:
from fastapi import Path, HTTPException
from pydantic import validator
from typing import Annotated
class ValidatedInt(int):
@classmethod
def __get_validators__(cls):
yield cls.validate
@classmethod
def validate(cls, v):
if isinstance(v, str) and not v.isdigit():
raise ValueError('must be a positive integer')
return int(v)
@app.get('/items/{item_id}')
def get_item(item_id: Annotated[ValidatedInt, Path()]):
item = items_db.get(item_id)
if item is None:
raise HTTPException(status_code=404, detail='Item not found')
return {'id': item.id, 'name': item.name}Query parameters should use Pydantic's strict typing with default values:
from pydantic import BaseModel
from typing import Optional
class SearchParams(BaseModel):
category: Optional[str] = None
limit: int = 10
@validator('category')
def category_not_empty(cls, v):
if v == '':
return None
return v
@app.get('/search')
def search_items(params: SearchParams = Depends()):
category = params.category or 'all'
return filter_items(category, limit=params.limit)Request body validation should use Pydantic's strict mode to prevent unexpected None values:
from pydantic import BaseModel, Field
class OrderRequest(BaseModel):
product_id: int = Field(..., ge=1)
quantity: int = Field(..., ge=1)
discount_code: Optional[str] = None
@app.post('/order')
def create_order(request: OrderRequest):
discount = discount_service.get_discount(request.discount_code) if request.discount_code else None
return {'total': calculate_total(request.quantity, discount)}Middleware and exception handlers should use defensive programming with attribute existence checks:
@app.middleware("http")
async def add_request_id_header(request: Request, call_next):
response = await call_next(request)
client_info = getattr(request, 'client', None)
client_ip = client_info.host if client_info else 'unknown'
response.headers['X-Request-ID'] = generate_request_id(client_ip)
return response