HIGH mass assignmentaxummutual tls

Mass Assignment in Axum with Mutual Tls

Mass Assignment in Axum with Mutual Tls — how this specific combination creates or exposes the vulnerability

Mass Assignment occurs when an API binds incoming request data directly to data models or structs without explicit field filtering. In Axum, this typically happens when developers use serde::Deserialize to map JSON payloads into structs and then persist those structs without removing sensitive or unowned fields. When Mutual TLS is used for transport-layer authentication, the server may place strong trust in the client certificate and assume the request is both authenticated and safe. This trust can cause developers to skip additional authorization checks, inadvertently allowing a mass assignment path to remain open. For example, a client presenting a valid certificate might send an extended JSON payload that adds or modifies fields such as role, is_admin, or permissions. If the server’s Axum handler deserializes into a struct that includes those fields and then directly saves the struct, privilege escalation or unintended data modification can occur.

Mutual TLS binds identity to transport, but it does not enforce application-level permissions. The combination creates a false sense of security: the server validates the client certificate and may skip secondary authorization, while the handler relies on implicit binding between the certificate and the user context. If the route does not explicitly whitelist fields during deserialization, an attacker who compromises a valid certificate (or uses a low-privilege certificate with an overlooked handler) can exploit mass assignment to modify sensitive attributes. This maps to the OWASP API Top 10 1: Broken Object Level Authorization and can intersect with BOLA/IDOR when object ownership is not validated independently of certificate identity.

In Axum, a vulnerable pattern looks like accepting a full struct without filtering:

#[derive(serde::Deserialize)]
struct UserUpdate {
    username: String,
    email: String,
    role: String, // sensitive, should not be assignable by client
    is_admin: bool,
}

async fn update_user(
    user_id: Path,
    update: Json<UserUpdate>,
) -> impl IntoResponse {
    // Missing: ensure current user can only modify allowed fields
    // Missing: role/is_admin checks against certificate or claims
    // Risk: mass assignment if certificate is trusted implicitly
    HttpResponse::Ok().finish()
}

Even with mTLS enforced at the router or TLS layer, the handler must explicitly filter fields and enforce per-request authorization rather than relying on transport identity alone.

Mutual Tls-Specific Remediation in Axum — concrete code fixes

To remediate mass assignment in Axum while using Mutual TLS, apply explicit field filtering and strict authorization at the handler level. Do not rely on the presence of a client certificate to enforce write constraints. Use selective deserialization and validate ownership or permissions based on runtime identity derived from the certificate, not the certificate alone.

1) Use a dedicated DTO with only safe fields

Define input structs that include only the fields the client is allowed to set. Keep sensitive fields such as role or is_admin out of deserialization paths, and instead apply server-side defaults or updates based on admin checks.

#[derive(serde::Deserialize)]
struct UserUpdateSafe {
    username: String,
    email: String,
    // role and is_admin omitted; set via admin checks or defaults
}

async fn update_user_safe(
    user_id: Path,
    update: Json<UserUpdateSafe>,
    // Example: extract certificate subject or identity from request extensions
    Extension(state): Extension<Arc<AppState>>,
    OriginalUri(uri): OriginalUri,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    let user_id = user_id.into_inner();
    // Fetch existing user from data store
    let mut user = state.repo.get_user(&user_id).await.map_err(|_| (StatusCode::NOT_FOUND, "not found"))?;
    // Apply only safe fields
    user.username = update.username;
    user.email = update.email;
    // Authorization: ensure the certificate identity matches the user or has admin rights
    let requester_identity = extract_identity_from_cert(&state, &uri).map_err(|_| (StatusCode::FORBIDDEN, "invalid identity"))?;
    if !state.authz.can_update_user(&requester_identity, &user_id) {
        return Err((StatusCode::FORBIDDEN, "not authorized").into());
    }
    state.repo.save_user(user).await.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "save failed"))?;
    Ok(Json(user))
}

2) Enforce mTLS and map certificate to identity, then apply checks

Configure Axum to require client certificates and extract identity (e.g., Common Name or SAN) to perform per-request authorization. Below is an example using tower-https with Rustls and extracting peer certificate data into request extensions for further checks.

use axum::extract::Extension;
use rustls::Certificate;
use std::sync::Arc;

// Helper to extract identity from peer certs
fn extract_identity_from_cert(state: &AppState, uri: &hyper::Uri) -> Result<String, &'static str> {
    // In practice, retrieve peer certs from request extensions provided by your TLS acceptor
    // This is a simplified placeholder: real integration depends on your server setup
    state.cert_resolver.resolve(uri).map_err(|_| "no cert")
}

// AppState holds authorization service and data access
struct AppState {
    repo: Arc<dyn UserRepo + Send + Sync>,
    authz: Arc<dyn Authz + Send + Sync>,
    cert_resolver: Arc<dyn CertResolver + Send + Sync>,
}

async fn update_user_mtls(
    user_id: Path,
    update: Json<UserUpdateSafe>,
    Extension(state): Extension<Arc<AppState>>
) -> Result<impl IntoResponse, (StatusCode, String)> {
    let user_id = user_id.into_inner();
    let requester_identity = extract_identity_from_cert(&state, &hyper::Uri::from_maybe_shared("http://example.com").map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "uri error"))?).map_err(|_| (StatusCode::FORBIDDEN, "cert identity"))?;
    if !state.authz.can_update_user(&requester_identity, &user_id) {
        return Err((StatusCode::FORBIDDEN, "not authorized").into());
    }
    let mut user = state.repo.get_user(&user_id).await.map_err(|_| (StatusCode::NOT_FOUND, "not found"))?;
    user.username = update.username.clone();
    user.email = update.email.clone();
    state.repo.save_user(user).await.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "save failed"))?;
    Ok(Json(user))
}

By combining explicit DTOs, certificate-derived identity, and server-side authorization, you mitigate mass assignment while preserving the benefits of Mutual TLS for peer authentication.

Related CWEs: propertyAuthorization

CWE IDNameSeverity
CWE-915Mass Assignment HIGH

Frequently Asked Questions

Does Mutual TLS alone prevent mass assignment in Axum?
No. Mutual TLS authenticates the client at the transport layer but does not enforce application-level field-level authorization. You must still filter input structs and validate permissions in handlers to prevent mass assignment.
Can I rely on serde's default behavior to ignore unknown fields and stay safe?
No. By default, serde ignores unknown fields during deserialization, but it will still populate all fields present in the struct. If sensitive fields are present in the JSON, they will be set. Use explicit DTOs without sensitive fields and apply server-side mapping to stay safe.