HIGH axumgraphql introspection abuse

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.

Frequently Asked Questions

Why is GraphQL introspection dangerous if my schema doesn't contain sensitive fields?
Introspection reveals the entire API structure, including operation names, argument types, and relationships. Attackers use this map to identify potential BOLA/IDOR vulnerabilities (e.g., guessing numeric IDs) or locate hidden admin mutations. Even without obvious sensitive fields, the schema blueprint accelerates automated attacks.
How does middleBrick detect GraphQL introspection abuse in an Axum application?
middleBrick sends standard GraphQL introspection queries (e.g., requesting __schema.types) to your endpoint during its 5-15 second black-box scan. If the response contains a full type listing, it flags this as a Data Exposure finding with High severity. The scan requires no credentials and works on any publicly accessible GraphQL endpoint, including Axum services.