Double Free in Actix with Jwt Tokens
Double Free in Actix with Jwt Tokens — how this specific combination creates or exposes the vulnerability
A Double Free in Actix when using JWT tokens typically arises when token parsing, validation, and storage are handled across multiple layers without consistent ownership semantics. In Rust, a Double Free occurs when the same heap-allocated memory is freed more than once, which can happen if a JWT claims structure is dropped implicitly at the end of a request and then explicitly deallocated later (or vice versa) due to mismatched lifetimes or reference counting.
When JWT tokens are decoded into strongly-typed claim structs, Actix application code often deserializes the payload into an owned struct and then stores references to parts of it (e.g., subject or roles) in request extensions or session-like containers. If these references outlive the original decoded token and the token is parsed again within the same or a subsequent request on the same worker, the intermediate representation may be reconstructed in a way that causes the runtime to free the same buffer twice—once when the temporary token object is dropped and again when the runtime or an integration layer cleans up request-local data.
This risk is heightened when using per-request token extraction and validation in Actix middleware. Consider a scenario where a middleware extracts a bearer token, decodes it into a Claims object, validates it, and then attaches a borrowed slice of the token payload to the request extensions. If the middleware or downstream handler later re-decodes the token (for example, to re-validate scopes or to refresh context) and both the original and new decoded objects exist simultaneously, the runtime may schedule deallocations for overlapping memory regions. In unsafe Actix code paths that interact directly with token bytes—such as when using custom extractors that hold raw buffers—failure to ensure single ownership or to use Arc for shared data can trigger the Double Free when destructors run.
Moreover, integration with authentication libraries that internally cache decoded tokens or reuse buffers can introduce subtle sharing issues. For instance, if an Actix service decodes a JWT with a library that returns a struct containing a String or Vec, and that struct is cloned into multiple request-scoped objects without ensuring deep copies or reference counting, the finalizer for each clone may attempt to release the same underlying memory. This is particularly relevant when token validation logic is split across multiple Actix handler wrappers or when developers inadvertently pass token data between synchronous and asynchronous contexts without proper ownership handling.
In practice, the Double Free may not always lead to an immediate crash; it can manifest as memory corruption that later causes undefined behavior, such as erratic service responses or crashes under load. Because Actix relies on Rust’s safety guarantees, such bugs usually indicate a breach of ownership rules—either by storing references with incorrect lifetimes or by duplicating deallocation logic across token processing stages. Security scanners that perform black-box testing, like those that analyze unauthenticated attack surfaces, may not directly observe a Double Free, but they can detect related symptoms such as inconsistent token validation outcomes or unexpected server behavior when malformed or repeated tokens are supplied.
Jwt Tokens-Specific Remediation in Actix — concrete code fixes
To prevent Double Free and similar memory-safety issues with JWT tokens in Actix, ensure that token payloads are owned by a single clear owner per request and avoid storing references to transient decoded data. Use Arc to share immutable token claims across parts of the request lifecycle, and prefer cloning small structures rather than sharing slices of raw token bytes.
Below are concrete, syntactically correct examples for Actix that demonstrate safe JWT handling.
1. Decode into an owned struct and wrap in Arc
Decode the JWT into an owned claims structure at the earliest point, then wrap it in an Arc if you need to share it across handlers, middleware, or request extensions.
use actix_web::{dev::ServiceRequest, Error, HttpRequest};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Claims {
sub: String,
roles: Vec,
exp: usize,
}
async fn validate_token(bearer: BearerAuth) -> Result, Error> {
let token = bearer.token();
let decoded = decode::(
token,
&DecodingKey::from_secret("secret".as_ref()),
&Validation::new(Algorithm::HS256),
)
.map_err(|_| actix_web::error::ErrorUnauthorized("invalid token"))?;
Ok(Arc::new(decoded.claims))
}
// In a handler:
async fn profile(req: HttpRequest) -> Result {
let claims = req.extensions()
.get::>()
.ok_or_else(|| actix_web::error::ErrorUnauthorized("missing claims"))?;
Ok(format!("user: {}, roles: {:?}", claims.sub, claims.roles))
}
2. Use a custom extractor that owns the claims
Create an extractor that decodes and owns the JWT payload, ensuring the token data is not borrowed beyond the extractor’s scope.
use actix_web::{FromRequest, HttpRequest, Payload, Error};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use std::future::{ready, Ready};
struct AuthenticatedClaims {
claims: Claims, // as defined above
}
impl FromRequest for AuthenticatedClaims {
type Error = Error;
type Future = Ready>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let bearer = BearerAuth::from_request(req, _).map_err(|_| actix_web::error::ErrorUnauthorized("no bearer"))?;
let decoded = decode::(
bearer.token(),
&DecodingKey::from_secret("secret".as_ref()),
&Validation::new(Algorithm::HS256),
).map_err(|_| actix_web::error::ErrorUnauthorized("invalid"))?;
ready(Ok(AuthenticatedClaims { claims: decoded.claims }))
}
}
// Usage in handler:
async fn submit(data: web::Json, claims: AuthenticatedClaims) -> Result {
Ok(format("processed for: {}", claims.claims.sub))
}
3. Avoid storing borrowed token slices in request extensions
Do not place references derived from a decoded token into request extensions if those references point into a temporary buffer. Instead, store owned values or Arc-wrapped data.
use actix_web::dev::ServiceRequest;
use std::sync::Arc;
// Safe: store owned data
req.extensions_mut().insert(Arc::new(claims));
// Unsafe pattern to avoid:
// req.extensions_mut().insert(token_string.as_str()); // token_string may be dropped
4. Middleware that re-validates safely
If middleware re-decodes tokens, ensure each decoding produces its own owned claims and does not reuse buffers from prior steps.
use actix_web::{Error, dev::{ServiceRequest, ServiceResponse}, Transform};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use futures_util::future::{ok, Either, FutureResult};
struct JwtValidationMiddleware;
impl Transform for JwtValidationMiddleware
where
S: actix_web::dev::Service, Error = Error>,
S::Future: 'static,
{
type Response = ServiceResponse;
type Error = Error;
type Transform = JwtValidationMiddlewareImpl;
type InitError = ();
type Future = FutureResult;
fn new_transform(&self, service: S) -> Self::Future {
ok(JwtValidationMiddlewareImpl { service })
}
}
struct JwtValidationMiddlewareImpl {
service: S,
}
impl actix_web::dev::Service for JwtValidationMiddlewareImpl
where
S: actix_web::dev::Service, Error = Error>,
S::Future: 'static,
{
type Response = ServiceResponse;
type Error = Error;
type Future = Either<Ready<Result<Self::Response, Self::Error>>, S::Future>;
fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&mut self, req: ServiceRequest) -> Self::Future {
// Decode and own the claims here; do not borrow from req extensions created elsewhere
let bearer = match BearerAuth::from_request(&req, &mut actix_web::dev::Payload::None) {
Ok(b) => b,
Err(_) => return Either::Left(ready(Err(actix_web::error::ErrorUnauthorized("no bearer")))),
};
let decoded = match decode::(
bearer.token(),
&DecodingKey::from_secret("secret".as_ref()),
&Validation::new(Algorithm::HS256),
) {
Ok(d) => d.claims,
Err(_) => return Either::Left(ready(Err(actix_web::error::ErrorUnauthorized("invalid")))),
};
// Store owned claims
req.extensions_mut().insert(Arc::new(decoded));
Either::Right(self.service.call(req))
}
}