Graphql Introspection in Axum with Jwt Tokens
Graphql Introspection in Axum with Jwt Tokens — how this specific combination creates or exposes the vulnerability
GraphQL introspection is a query capability that returns the schema, types, and operations available on a GraphQL endpoint. When enabled in production, introspection can expose implementation details that assist attackers in crafting injection or privilege escalation attempts. In an Axum-based Rust service, developers often integrate GraphQL via crates such as juniper or async-graphql and may conditionally enable introspection based on build profiles or feature flags.
Combining introspection with JWT tokens in Axum introduces a nuanced risk pattern. JWT tokens are typically validated before request routing; however, if introspection is permitted for both authenticated and unauthenticated requests, an attacker can probe the schema without valid credentials. This becomes a security concern when introspection leaks type hierarchies, resolver behaviors, or field-level authorization logic that should remain hidden. Even when JWT validation is correctly implemented, misconfiguration can allow introspection queries to bypass authorization checks, effectively exposing a metadata leak channel.
Consider an Axum setup where routes are guarded by a JWT middleware that attaches claims to the request extensions. If the GraphQL handler does not explicitly reject introspection when authorization is absent or insufficient, an unauthenticated introspection query can still reach the resolver layer. Attackers can use such introspection responses to map query patterns, infer mutation capabilities, and identify fields that rely on JWT claims for filtering or scoping, which may lead to Insecure Direct Object References (IDOR) or BOLA (Broken Level Authorization).
In practice, a malicious actor can send an introspection query to a GraphQL endpoint protected by JWT tokens but lacking strict introspection controls. The response may reveal query names like userById, accountDetails, or sensitive metadata such as enum values and directive locations. This information complements other attack vectors such as BFLA (Business Logic Flaws Abuse) or Property Authorization weaknesses, where field-level permissions are not enforced consistently. The presence of JWT tokens does not inherently mitigate introspection exposure; it only shifts the problem to ensuring introspection is gated by the same authorization policies applied to data queries.
To align with secure design principles, introspection should be treated as a sensitive operation within Axum GraphQL handlers. When combined with JWT tokens, the key is to ensure introspection is either disabled entirely in production or explicitly authorized using the same context and policy checks applied to regular queries. This prevents attackers from harvesting schema knowledge that could facilitate further compromise, such as crafting targeted injections or exploiting rate-limiting gaps revealed through introspection.
Jwt Tokens-Specific Remediation in Axum — concrete code fixes
Securing GraphQL introspection in Axum when JWT tokens are used requires explicit gating of introspection queries behind authorization checks and disabling introspection in production builds. The following patterns demonstrate how to implement this using common Rust GraphQL crates while maintaining JWT-based context propagation.
1. Disable introspection in production builds
When using async-graphql, you can disable introspection entirely for release builds by configuring the schema conditionally:
// src/schema.rs
use async_graphql::{Schema, EmptySubscription};
use crate::models::Query;
pub type MySchema = Schema<Query, EmptySubscription<()>>;
pub fn build_schema(enable_introspection: bool) -> MySchema {
let schema = Schema::build(Query, EmptySubscription::default()).finish();
if enable_introspection {
schema
} else {
// In production, return a schema that does not expose introspection types
schema.disable_introspection()
}
}
Ensure enable_introspection is set based on your environment, typically debug_assertions for development and false for production.
2. Authorize introspection within the resolver context
With juniper in an Axum handler, validate JWT claims before allowing introspection by inspecting request extensions:
// src/graphql.rs
use juniper::{GraphQLObject, GraphQLInputObject, FieldResult};
use axum::extract::Extension;
use crate::jwt::Claims;
pub struct QueryRoot;
#[juniper::graphql_object(context = Context)]
impl QueryRoot {
async fn user_profile(&self, Extension(ctx): Extension<Context>) -> FieldResult<String> {
// JWT claims are already validated and present in Context
Ok(format!("Profile for {}", ctx.user_id))
}
// Disable introspection at the field level if needed
#[juniper::graphql(graphql_name = "__schema", skip)]
fn schema_introspection(&self) -> FieldResult<()> {
Err(juniper::FieldError::new(
"Introspection is disabled",
juniper::FieldError::new_code(juniper::ErrorCode::Forbidden),
))
}
}
The skip attribute on __schema prevents introspection resolver exposure. Ensure your Context type carries validated JWT claims and is injected via Axum extensions.
3. JWT validation middleware ensuring introspection is gated
An Axum middleware should validate JWT and attach claims only when valid; introspection queries then inherit the same authorization checks:
// src/middleware.rs
use axum::{Extension, async_trait};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use crate::jwt::Claims;
pub async fn validate_jwt_and_proceed(
Extension(cfg): Extension<AppConfig>,
headers: axum::http::HeaderMap,
) -> Extension<Claims> {
let token = headers.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "))
.expect("Missing or malformed Authorization header");
let validation = Validation::new(Algorithm::HS256);
let token_data = decode<Claims>(token, &DecodingKey::from_secret(cfg.jwt_secret.as_ref()), &validation)
.expect("Invalid token");
Extension(token_data.claims)
}
In your GraphQL route handler, require the same Extension(Claims) for all operations, including introspection, ensuring that unauthorized introspection attempts are rejected at the middleware or handler level.
4. Example integration in Axum routes
Wire everything together so that both queries and introspection respect JWT validation:
// src/main.rs
use axum::{routing::post, Router};
use crate::graphql::{build_schema, validate_jwt_and_proceed};
#[tokio::main]
async fn main() {
let schema = build_schema(!cfg!(debug_assertions)); // disable introspection in release
let app = Router::new()
.route("/graphql", post(|Extension(claims): Extension<Claims>, Extension(schema): Extension<MySchema>, body: String| async move {
let (variables, operation, ctx) = parse_request(&body, &claims).await;
let res = schema.execute(&body).variables(variables).context(ctx).await;
axum::response::Json(res)
}))
.layer(Extension(schema));
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
Related CWEs: dataExposure
| CWE ID | Name | Severity |
|---|---|---|
| CWE-200 | Exposure of Sensitive Information | HIGH |
| CWE-209 | Error Information Disclosure | MEDIUM |
| CWE-213 | Exposure of Sensitive Information Due to Incompatible Policies | HIGH |
| CWE-215 | Insertion of Sensitive Information Into Debugging Code | MEDIUM |
| CWE-312 | Cleartext Storage of Sensitive Information | HIGH |
| CWE-359 | Exposure of Private Personal Information (PII) | HIGH |
| CWE-522 | Insufficiently Protected Credentials | CRITICAL |
| CWE-532 | Insertion of Sensitive Information into Log File | MEDIUM |
| CWE-538 | Insertion of Sensitive Information into Externally-Accessible File | HIGH |
| CWE-540 | Inclusion of Sensitive Information in Source Code | HIGH |