Mass Assignment in Actix with Jwt Tokens
Mass Assignment in Actix with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Mass Assignment occurs when an API binds incoming JSON directly to a data model without filtering which fields are accepted. In Actix, this commonly happens when developers use struct updates derived from request payloads such as web::Json<UserUpdate> and apply them to a database entity through an ORM like Diesel without an allowlist. When JWT tokens are involved, two risk dimensions increase: authentication state and authorization scope. A JWT often carries claims such as roles or scopes that the application uses to make authorization decisions, but the token itself is not an input filter for struct updates. If an endpoint accepts a broader struct than intended and merges it into a database record, an attacker can supply extra fields (for example, is_admin or role) that the JWT does not explicitly permit yet will still be applied because Actix blindly binds the JSON. This means the attacker can modify privileged attributes that the JWT context does not validate at the field level, bypassing intended privilege boundaries. In addition, JWTs may carry user identifiers used to locate a record (e.g., sub claim for user ID). If the request path includes an ID that differs from the JWT subject but the handler does not enforce ownership, mass assignment combined with ID or BOLA flaws can allow horizontal privilege escalation across user boundaries. The combination therefore exposes both vertical escalation (via role or permission fields) and horizontal access to other users’ data, even though the JWT is valid and properly verified.
Jwt Tokens-Specific Remediation in Actix — concrete code fixes
Remediation focuses on strict input filtering, explicit mapping, and ensuring JWT claims are used for authorization checks rather than assuming they constrain every field. Use dedicated update structs that only expose safe fields and map them to your ORM models before saving. Below are concrete Actix examples demonstrating secure handling with JWT validation and selective field updates.
Example 1: Whitelisted update with JWT subject verification
use actix_web::{web, HttpResponse, Result};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
struct Claims {
sub: String,
role: String,
}
#[derive(Debug, Deserialize)]
struct UpdateUserRequest {
email: Option,
display_name: Option,
// Never include is_admin or role from request
}
#[derive(Debug, Serialize, Deserialize)]
struct User {
id: i32,
email: String,
display_name: String,
is_admin: bool,
}
async fn update_user(
token: web::Header<actix_web::http::header::Authorization>,
path: web::Path<i32>,
body: web::Json<UpdateUserRequest>,
) -> Result<HttpResponse> {
// Verify JWT and extract claims
let token = token.into_inner().to_string().replace("Bearer ", "");
let decoding_key = DecodingKey::from_secret("secret".as_ref());
let validation = Validation::new(Algorithm::HS256);
let token_data = decode::<Claims>(&token, &decoding_key, &validation)
.map_err(|_| HttpResponse::Unauthorized().finish())?;
let user_id: i32 = path.into_inner();
// Ensure the subject matches the target ID or enforce admin scope
if token_data.claims.sub != user_id.to_string() && token_data.claims.role != "admin" {
return Ok(HttpResponse::Forbidden().finish());
}
// Whitelisted update: only email and display_name are accepted
let update = body.into_inner();
// In real code, use Diesel or SQLx with explicit columns
// diesel::update(users.find(user_id))
// .set((email.eq(update.email), display_name.eq(update.display_name)))
// .execute(&conn)?;
Ok(HttpResponse::Ok().finish())
}
Example 2: Explicit mapping to avoid mass assignment with role claims
use actix_web::web;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
struct PatchProfile {
email: Option,
display_name: Option,
}
#[derive(Debug, Serialize)]
struct SafeUserProfile {
email: String,
display_name: String,
}
// Handler that maps only safe fields, ignoring any injected role or permission fields
async fn patch_profile(
user_id: web::Path<i32>,
patch: web::Json<PatchProfile>,
// role_claims would be derived from JWT validation earlier
) -> SafeUserProfile {
let patch = patch.into_inner();
// Simulate fetching existing record
// let existing = users::table.find(*user_id).first<User>(&conn).unwrap();
let mut email = patch.email.unwrap_or_else(|| "[email protected]".to_string());
let mut display_name = patch.display_name.unwrap_or_else(|| "Anonymous".to_string());
// Enforce constraints from JWT claims here before saving
// if claims.role != "admin" { is_admin = existing.is_admin; }
// Apply only whitelisted fields
// diesel::update(users::table.find(*user_id))
// .set((users::email.eq(&email), users::display_name.eq(&display_name)))
// .execute(&conn)?;
SafeUserProfile { email, display_name }
}
Additional practices
- Always use a dedicated request struct that includes only the fields the endpoint should accept.
- Validate JWT claims for scope or role and enforce them at the handler or service layer, not via struct binding.
- Apply separate authorization logic that checks whether the JWT subject or role permits the mutation of specific attributes (e.g., is_admin).
- Consider using serialization filters or manual mapping rather than automatic struct-to-model assignment.
Related CWEs: propertyAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-915 | Mass Assignment | HIGH |