HIGH null pointer dereferenceaxumapi keys

Null Pointer Dereference in Axum with Api Keys

Null Pointer Dereference in Axum with Api Keys — how this specific combination creates or exposes the vulnerability

A null pointer dereference in an Axum application that uses API keys typically occurs when a developer attempts to access fields or methods on an option or result that is None. This is common when API keys are parsed from headers or query parameters and the framework’s extractor returns an Option or a Result that is not explicitly handled for the null (or absent) case.

In Axum, extractor combinators such as Extension, Query, or custom extractors can return Option types. If an API key is passed as a header and the extraction logic assumes presence, a missing key can yield None. Calling methods like .unwrap(), .expect(), or pattern-matching with Some without a fallback can trigger a null pointer dereference at runtime, resulting in a 500 error or, in unsafe code blocks, undefined behavior.

Consider an endpoint where the API key is used for authorization but not validated for existence before being forwarded to an authorization module:

use axum::{routing::get, Router, extract::Extension};
use std::net::SocketAddr;

struct AuthService;

impl AuthService {
    fn verify(key: &str) -> bool {
        // pretend verification
        key == "secret"
    }
}

async fn handler(auth: Extension<AuthService>, api_key: Option<String>) -> String {
    // Potential null dereference: api_key may be None
    let key = api_key.unwrap(); // <- unsafe if key is absent
    if auth.verify(&key) {
        "OK".into()
    } else {
        "Forbidden".into()
    }
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/secure", get(handler))
        .layer(Extension(AuthService));
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

In this example, if the client request omits the API key header, api_key becomes None, and .unwrap() causes a panic — effectively a null pointer dereference in Rust’s safe code (panicking on None). In unsafe blocks, directly dereferencing a null pointer can corrupt memory or crash the process.

An attacker can exploit this to cause denial of service by sending requests without the API key, or, in more complex scenarios involving unsafe interop, potentially achieve arbitrary code execution. This pattern also surfaces when integrating with libraries that return nullable pointers via FFI, where missing validation leads to immediate crashes.

To avoid this, always handle the Option or Result explicitly, use combinators like .ok_or_else() to convert to HTTP errors, or employ typed extractors that reject requests with appropriate status codes before reaching business logic.

Api Keys-Specific Remediation in Axum — concrete code fixes

Remediation focuses on eliminating assumptions about presence and validating API keys safely before use. Replace .unwrap() and .expect() with explicit error handling that returns a proper HTTP response (e.g., 401 Unauthorized or 400 Bad Request).

Prefer typed extractors that enforce key presence at the routing layer, or use Option combinators to provide early, graceful rejection.

Here is a safe pattern using Extension and manual header extraction with .ok_or_else() to avoid null dereferences:

use axum::{{
    async_trait,
    extract::{FromRequest, Request},
    response::IntoResponse,
    Extension, Json,
}};
use http::{Request, StatusCode};
use std::convert::Infallible;
use std::net::SocketAddr;
use tower_http::Extension as AxumExtension;

struct ApiKey(String);

#[async_trait]
impl FromRequest<S> for ApiKey 
where
    S: Send + Sync,
{
    type Rejection = (StatusCode, String);

    async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
        match req.headers().get("x-api-key") {
            Some(hv) => hv.to_str().map(ApiKey).map_err(|_| {
                (StatusCode::BAD_REQUEST, "Invalid API key header".into())
            }),
            None => Err((StatusCode::UNAUTHORIZED, "Missing API key".into())),
        }
    }
}

struct AuthService;

impl AuthService {
    fn verify(key: &str) -> bool {
        key == "secret"
    }
}

async fn handler(
    Extension(auth): Extension<AuthService>,
    ApiKey(key): ApiKey,
) -> impl IntoResponse {
    if auth.verify(&key) {
        Json(serde_json::json!({ "status": "authorized" }))
    } else {
        (StatusCode::FORBIDDEN, "Invalid API key")
    }
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/secure", get(handler))
        .layer(Extension(AuthService));
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

This extractor returns a 401 status if the header is missing, eliminating the possibility of a null dereference. For simpler cases, you can also use .map_or_else() or .and_then() on Option values to provide defaults or errors inline.

When using dependency injection via Extension, ensure the injected service does not itself rely on optional key fields without checks. Combine extractor validation with middleware that verifies keys before requests reach handlers, ensuring null safety and meaningful error responses.

Frequently Asked Questions

What extractor pattern prevents null pointer dereference for API keys in Axum?
Use a custom extractor that returns Result<ApiKey, (StatusCode, String)> and validates presence of the header, rejecting missing keys with 401 instead of relying on Option defaults or .unwrap().
Why should I avoid <code>.unwrap()</code> when handling API key headers in Axum?
Calling .unwrap() on an Option that is None causes a panic (null pointer dereference in Rust terms), leading to 500 errors and potential denial of service. Explicit error handling returns proper HTTP status codes safely.