Injection Flaws in Actix with Jwt Tokens
Injection Flaws in Actix with Jwt Tokens
Injection flaws in Actix applications that rely on JWT tokens can occur when untrusted input is used to construct queries, commands, or dynamic behavior, and the JWT payload is not strictly validated. For example, if a developer deserializes the JWT claims into a loosely typed structure and then interpolates values into a database query or shell command, an attacker who can influence the token (via weak signing, algorithm confusion, or a stolen token) may inject malicious content.
Consider an Actix handler that reads a user role from a JWT and uses it to build a dynamic query without validation:
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation, TokenData};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
role: String,
}
async fn handler(req: web::HttpRequest) -> impl Responder {
let auth = req.headers().get("Authorization")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let token = auth.strip_prefix("Bearer ").unwrap_or(auth);
let decoding_key = DecodingKey::from_secret("secret".as_ref());
let validation = Validation::new(Algorithm::HS256);
match decode::(token, &decoding_key, &validation) {
Ok(token_data) => {
// Injection risk: role used to build a dynamic query
let query = format!("SELECT * FROM users WHERE role = '{}'", token_data.claims.role);
// execute_query(&pool, &query); // hypothetical
HttpResponse::Ok().body(query)
}
Err(_) => HttpResponse::Unauthorized().finish(),
}
}
If the JWT’s role claim is tampered with (e.g., changed to ' OR '1'='1), the resulting query becomes vulnerable to SQL injection. Even when using JWTs for authentication, the token’s contents must be treated as untrusted input. Attackers may exploit weak signing keys, missing audience/issuer validation, or algorithm confusion (e.g., expecting HS256 but server accepts RS256) to forge tokens that carry malicious injected values.
Another scenario involves command injection when JWT-derived values are passed to system utilities. For example, if an Actix service uses a JWT sub or custom field to construct a filename or shell argument without strict allow-listing, an attacker can inject shell metacharacters:
use actix_web::web;
use std::process::Command;
fn run_export(user_id: &str) -> std::io::Result<()> {
// Injection risk: user_id from JWT used directly in command
let output = Command::new("tar")
.arg("-czf")
.arg(format!("/exports/{}.tar.gz", user_id)) // unsafe if user_id contains "../" or shell chars
.output()?;
Ok(())
}
Here, a JWT-subject like ../../etc/passwd could lead to path traversal and unintended file inclusion. The core issue is treating the JWT as inherently trustworthy rather than validating and sanitizing individual claims before use in sensitive contexts.
SSRF and injection can also intersect if JWT claims influence URLs or hostnames used in outbound requests. For instance, if a claim specifies an endpoint or API key that is concatenated into a request without validation, an attacker may supply malicious hosts or paths, leading to internal service enumeration or data exfiltration.
Jwt Tokens-Specific Remediation in Actix
Remediation focuses on strict validation of JWT claims and avoiding direct use of token values in sensitive operations. Always validate issuer, audience, expiration, and signing algorithm. Use strongly typed, allow-listed claim values and avoid string interpolation for queries or commands.
1) Use a strongly typed, validated claim structure and parameterize queries instead of interpolating:
use actix_web::{web, HttpResponse};
use sqlx::PgPool;
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation, TokenData};
#[derive(Debug, sqlx::FromRow)]
struct User { /* fields */ }
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
role: String,
}
async fn safe_handler(
token: web::Query,
pool: web::Data,
) -> HttpResponse {
let decoding_key = DecodingKey::from_secret("secret".as_ref());
let mut validation = Validation::new(Algorithm::HS256);
validation.set_issuer(&["myapp"]);
validation.set_audience(&["myapi"]);
match decode::(token.token.as_str(), &decoding_key, &validation) {
Ok(token_data) => {
// Parameterized query avoids injection
let user: User = sqlx::query_as(
"SELECT * FROM users WHERE role = $1 AND sub = $2",
)
.bind(&token_data.claims.role)
.bind(&token_data.claims.sub)
.fetch_one(pool.get_ref())
.await
.unwrap_or_else(|_| /* handle */);
HttpResponse::Ok().json(user)
}
Err(_) => HttpResponse::Unauthorized().finish(),
}
}
2) If JWT-derived values must be used in dynamic contexts (e.g., filenames), enforce strict allow-listing and avoid shell usage:
use actix_web::web;
use std::path::{Path, PathBuf};
fn safe_path(user_id: &str) -> Option {
// Allow-list alphanumeric and underscore, reject path separators and shell chars
if user_id.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
Some(Path::new("/exports").join(format!("{}.tar.gz", user_id)))
} else {
None
}
}
fn export_user(user_id: &str) -> std::io::Result<()> {
match safe_path(user_id) {
Some(path) => {
// Use safe library functions, avoid shell
std::fs::write(&path, b"data")?;
Ok(())
}
None => Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid user_id")),
}
}
3) For middleware that inspects JWTs, validate algorithm and reject tokens with alg: none or mismatched keys. Enforce exp, nbf, iss, and aud. When in doubt, prefer opaque tokens validated via introspection rather than decoding and trusting payloads for security decisions.