Sql Injection in Axum with Jwt Tokens
Sql Injection in Axum with Jwt Tokens — how this specific combination creates or exposes the vulnerability
SQL injection in an Axum application that uses JWT tokens often arises when a developer uses data from a JWT to construct SQL queries. Axum is a web framework for Rust, and while Rust’s type system and safe APIs reduce many classes of vulnerabilities, SQL injection remains possible when untrusted data is concatenated into SQL strings. A JWT token typically carries claims such as user ID, roles, or tenant identifiers. If these claims are extracted and directly interpolated into dynamic SQL—such as building a format!("SELECT * FROM users WHERE id = {user_id}") string or passing user-controlled claim values to a query builder without parameterization—an attacker who can influence the token’s payload (e.g., via a forged or manipulated token) can inject malicious SQL.
In a typical flow, an Axum handler decodes a JWT, extracts a claim like sub (subject), and uses it in a database call. If the handler does not treat the claim as untrusted input and instead directly embeds it in SQL, the attack surface mirrors classic SQL injection. For example, an attacker who can obtain or forge a token with sub: "1 OR 1=1" might cause the application to return all rows from a users table. Additionally, if the JWT is used to gate authorization (e.g., checking roles from the token), injection can escalate to privilege escalation or unauthorized data access. This risk is compounded if the token is not validated strictly (signature, issuer, audience), because a weaker validation step may allow tampered claims to reach the SQL layer.
The vulnerability is not inherent to using JWTs with Axum, but emerges from insecure coding patterns: concatenating claims into SQL strings, failing to use prepared statements, or trusting token data without server-side validation. Even when using an ORM, if dynamic SQL fragments or raw queries are built with interpolated claim values, injection remains possible. Therefore, any data derived from a JWT that reaches a database must be treated as untrusted input and handled with parameterized queries or safe query builders.
Jwt Tokens-Specific Remediation in Axum — concrete code fixes
Remediation focuses on strict validation of JWTs and using parameterized SQL queries. Never directly interpolate JWT claims into SQL strings. Instead, decode the token with strong validation, extract only the needed claim, and pass it to the database via bound parameters. Below are concrete, idiomatic Axum examples using jsonwebtoken for verification and sqlx for safe parameterized queries.
1. Validate JWT and use parameterized queries
Define your claims structure and decode the token with signature and standard validation. Then use the claim value in a parameterized query.
use axum::{routing::get, Router};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation, TokenData};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use std::net::SocketAddr;
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
role: String,
}
async fn get_user_handler(
axum::extract::Query(params): axum::extract::Query>,
pool: axum::extract::State<PgPool>,
) -> Result<impl warp::Reply, (axum::http::StatusCode, String)> {
// In practice, obtain the token from Authorization header
let token = params.get("token").ok_or((axum::http::StatusCode::UNAUTHORIZED, "missing token"))?;
let decoding_key = DecodingKey::from_secret("your-secret".as_ref());
let validation = Validation::new(Algorithm::HS256);
let token_data: TokenData<Claims> = decode(token, &decoding_key, &validation)
.map_err(|_| (axum::http::StatusCode::UNAUTHORIZED, "invalid token"))?;
let user_id = &token_data.claims.sub;
// Use parameterized query: do not format the SQL string with user input
let row: (String,) = sqlx::query_as("SELECT username FROM users WHERE id = $1")
.bind(user_id)
.fetch_one(pool.get_ref())
.await
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(axum::response::Json(serde_json::json!({ "username": row.0 })))
}
#[tokio::main]
async fn main() {
let pool = PgPool::connect("postgres://user:pass@localhost/dbname").await.unwrap();
let app = Router::new().route("/user", get(get_user_handler)).with_state(pool);
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr).serve(app.into_make_service()).await.unwrap();
}
2. Avoid dynamic SQL with claim-based filters
If you must build dynamic filters, use a safe query builder or strictly whitelist values. For example, if a role claim is used to filter data, compare it against an enum rather than interpolating it.
use axum::routing::get;
use jsonwebtoken::{decode, DecodingKey, Validation, TokenData};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
role: String,
}
async fn list_items_handler(
pool: axum::extract::State<PgPool>,
axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
) -> Result<impl warp::Reply, (axum::http::StatusCode, String)> {
let token = params.get("token").ok_or((axum::http::StatusCode::UNAUTHORIZED, "missing token"))?;
let token_data: TokenData<Claims> = decode(token, &DecodingKey::from_secret("your-secret".as_ref()), &Validation::new(Algorithm::HS256))
.map_err(|_| (axum::http::StatusCode::UNAUTHORIZED, "invalid token"))?;
// Whitelist roles instead of interpolating
let role = match token_data.claims.role.as_str() {
"admin" | "user" => &token_data.claims.role,
_ => return Err((axum::http::StatusCode::FORBIDDEN, "invalid role".into())),
};
// Safe: role is bound as a parameter, not interpolated
let rows: Vec<(String,)> = sqlx::query_as("SELECT item FROM items WHERE role = $1")
.bind(role)
.fetch_all(pool.get_ref())
.await
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(axum::response::Json(serde_json::json!({ "items": rows })))
}
Key takeaways: validate JWTs with strict algorithms and audience/issuer checks, and always use parameterized queries or a type-safe query builder. Treat JWT claims as untrusted input, and avoid any SQL construction that concatenates them. This approach mitigates SQL injection while preserving the utility of JWT-based authentication in Axum services.
Related CWEs: inputValidation
| CWE ID | Name | Severity |
|---|---|---|
| CWE-20 | Improper Input Validation | HIGH |
| CWE-22 | Path Traversal | HIGH |
| CWE-74 | Injection | CRITICAL |
| CWE-77 | Command Injection | CRITICAL |
| CWE-78 | OS Command Injection | CRITICAL |
| CWE-79 | Cross-site Scripting (XSS) | HIGH |
| CWE-89 | SQL Injection | CRITICAL |
| CWE-90 | LDAP Injection | HIGH |
| CWE-91 | XML Injection | HIGH |
| CWE-94 | Code Injection | CRITICAL |