Graphql Introspection Abuse in Axum
How GraphQL Introspection Abuse Manifests in Axum
GraphQL introspection is a built-in feature that allows clients to query the schema's structure, including types, fields, mutations, and subscriptions. While essential for development tools like GraphiQL, leaving introspection enabled in production exposes your API's entire attack surface to unauthenticated actors. In Axum applications, this risk is particularly acute because the default configurations of popular GraphQL libraries (like async-graphql or juniper) enable introspection unless explicitly disabled.
An attacker can send a standard introspection query, such as:
{
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
name
fields { name }
inputFields { name }
}
}
}In an Axum service using async-graphql, a minimal setup might look like this:
use axum::{routing::post, Router};
use async_graphql::{Schema, EmptyMutation, EmptySubscription};
use std::net::SocketAddr;
#[derive(async_graphql::SimpleObject)]
struct Query {
#[graphql(name = "getUser")]
async fn get_user(&self, id: i32) -> Option<String> {
// Business logic
Some("user_data".to_string())
}
}
#[tokio::main]
async fn main() {
let schema = Schema::build(Query, EmptyMutation, EmptySubscription).finish();
let app = Router::new().route("/graphql", post(schema.graphql_handler()));
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}Here, the Schema::build(...).finish() call enables introspection by default. An attacker can immediately enumerate all available fields, including getUser. This reveals potential entry points for Broken Object Level Authorization (BOLA/IDOR) attacks (OWASP API3:2023). For instance, knowing the id argument type is i32 allows the attacker to craft sequential ID guesses. If the resolver lacks proper authorization checks (e.g., verifying the current user owns the resource), this leads to unauthorized data access.
More insidiously, introspection can expose hidden mutations. Consider an Axum app using juniper with an admin-only mutation:
#[derive(GraphQLObject)]
struct AdminAction {
success: bool,
}
#[derive(GraphQLInputObject)]
struct AdminInput {
secret_key: String,
}
struct QueryRoot;
#[juniper::graphql_object]
impl QueryRoot {
fn api_version() -> &str { "1.0" }
}
#[juniper::graphql_object]
impl MutationRoot {
fn perform_admin_action(input: AdminInput) -> AdminAction {
// Critical operation
AdminAction { success: true }
}
}
let schema = Schema::new(QueryRoot, MutationRoot);Even if the frontend hides this mutation, introspection lists it. An attacker could then attempt direct calls to performAdminAction, potentially triggering privilege escalation if the mutation lacks robust authorization (a BFLA violation, OWASP5). In Axum, such mutations are often mounted as POST /graphql endpoints, making them trivially discoverable via introspection.
Axum-Specific Detection
Detecting introspection abuse in Axum apps requires testing the live GraphQL endpoint. Manually, you can use curl or GraphQL clients to send the introspection query and inspect the response. A full schema response confirms the vulnerability. For example:
curl -X POST http://localhost:3000/graphql \
-H 'Content-Type: application/json' \
-d '{"query": "{ __schema { types { name } } }"}'If the response contains a data.__schema.types array with all type names, introspection is enabled. However, manual testing doesn't scale. This is where middleBrick's automated scanning excels. When you submit your Axum API's URL to middleBrick, it automatically sends a suite of introspection queries as part of its unauthenticated black-box scan. The scanner analyzes the response: if it receives a complete schema (dozens of types and fields), it flags this as a Data Exposure finding under its 12 parallel security checks.
middleBrick's report will show a specific finding like "GraphQL introspection enabled in production" with a severity rating (typically High). It includes the exact request/response snippets that proved the issue and maps the finding to OWASP API Top 10 categories (API3:2023 - Broken Object Property Level Authorization, and sometimes API4:2023 - Unrestricted Resource Consumption if the schema is large). The scan also cross-references any provided OpenAPI/Swagger spec; if your Axum app serves both REST and GraphQL, middleBrick notes the discrepancy where GraphQL exposes more surface than documented.
You can integrate this detection into your CI/CD pipeline using middleBrick's GitHub Action. For an Axum service, add a step to scan your staging endpoint before merge:
name: API Security Scan
on: [pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Run middleBrick scan
uses: middlebrick/action@v1
with:
api_url: ${{ secrets.STAGING_API_URL }}
fail_on_score_lt: 80
If introspection is enabled, the scan will fail the build (if your score threshold is breached), catching the misconfiguration before production deploy.
Axum-Specific Remediation
Remediation focuses on disabling introspection in production and ensuring strict authorization on all resolvers. In Axum, the approach depends on your GraphQL library.
1. Disable Introspection at the Schema Level (async-graphql)
The async-graphql crate provides a direct method. Modify your schema builder:
use async_graphql::{Schema, EmptyMutation, EmptySubscription};
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
.disable_introspection() // Disables __schema and __type queries
.finish();This causes introspection queries to return null for __schema, effectively hiding the structure. Deploy this change to your Axum server and re-run middleBrick to confirm the finding is resolved.
2. Block Introspection via Middleware (Juniper or Custom)
juniper does not have a built-in introspection toggle. Instead, use Axum's middleware to inspect incoming GraphQL requests and block queries containing __schema or __type. This is a defensive layer even if you switch libraries:
use axum::{
body::Body,
http::{Request, StatusCode},
middleware::Next,
response::Response,
};
use std::convert::Infallible;
async fn block_introspection(
mut req: Request<Body>,
next: Next<Body>,
) -> Result<Response<Body>, Infallible> {
// Extract the GraphQL query from the request body
if let Ok(body) = req.body().try_into_string() {
if body.contains("__schema") || body.contains("__type") {
return Ok(Response::builder()
.status(StatusCode::FORBIDDEN)
.body(Body::from("Introspection disabled")))
}
}
Ok(next.run(req).await)
}
// In your router setup
let app = Router::new()
.route("/graphql", post(schema.graphql_handler())
.layer(axum::middleware::from_fn(block_introspection));Note: This simple string check may have bypasses (e.g., whitespace variations). For robust protection, parse the GraphQL query AST. Libraries like graphql-parser can reliably detect introspection operations.
3. Enforce Field-Level Authorization
Disabling introspection is a first step, but each resolver must still enforce authorization. In Axum, use extractors to inject the authenticated user context. For async-graphql:
use async_graphql::{Context, FieldResult, Object};
use axum::extract::Extension;
#[derive(async_graphql::SimpleObject)]
struct User {
id: i32,
email: String,
}
struct Query;
#[async_graphql::Object]
impl Query {
async fn get_user(
&self,
ctx: &Context<'_>,
id: i32,
) -> FieldResult<User> {
let user = ctx.data::<AuthUser>()?; // Extracted from Axum middleware
// Authorization: user can only fetch their own data
if user.id != id {
return Err(async_graphql::Error::from("Forbidden"));
}
// Fetch from database...
Ok(User { id, email: "[email protected]".into() })
}
}In your Axum router, ensure an authentication middleware (e.g., JWT validation) populates the AuthUser in the GraphQL context:
use axum::{routing::post, Extension, Router};
async fn graphql_handler(
req: Request<Body>,
schema: Extension<Schema>,
auth_user: Option<AuthUser>, // From your auth middleware
) -> Response {
let context = Context::new().insert(auth_user);
schema.execute(req.into_body()).await.into_response()
}
let app = Router::new()
.route("/graphql", post(graphql_handler))
.layer(Extension(schema));This pattern ensures every resolver has access to the user's identity and can enforce fine-grained access control, mitigating BOLA risks even if introspection were accidentally left on in a non-production environment.