HIGH sql injectionaxumjwt tokens

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 IDNameSeverity
CWE-20Improper Input Validation HIGH
CWE-22Path Traversal HIGH
CWE-74Injection CRITICAL
CWE-77Command Injection CRITICAL
CWE-78OS Command Injection CRITICAL
CWE-79Cross-site Scripting (XSS) HIGH
CWE-89SQL Injection CRITICAL
CWE-90LDAP Injection HIGH
CWE-91XML Injection HIGH
CWE-94Code Injection CRITICAL

Frequently Asked Questions

Can a JWT token's payload alone cause SQL injection if the server does not use it in queries?
No. SQL injection requires the application to incorporate data into SQL strings or unsafe query fragments. If a server decodes a JWT but never uses its claims in database queries, the token’s payload cannot directly cause SQL injection.
Is using an ORM enough to prevent SQL injection when passing JWT claims into queries?
Not by itself. ORMs can still produce unsafe SQL if you use dynamic query building or raw queries with interpolated values. Always use parameterized queries or the ORM’s bound-parameter APIs, and treat JWT claims as untrusted input.