Graphql Introspection in Actix with Mutual Tls
Graphql Introspection in Actix with Mutual Tls — how this specific combination creates or exposes the vulnerability
GraphQL introspection in an Actix web service allows clients to query the schema, types, and operations dynamically. When mutual TLS (mTLS) is used for transport-layer authentication, the assumption is often that only authorized clients can reach the endpoint. However, mTLS ensures client identity but does not restrict what an authenticated client can query. If introspection is enabled in production, any mTLS-authenticated client can retrieve the full schema, including queries, mutations, subscriptions, and input types. This can expose field names, relationships, and business logic that you might intend to keep private.
In an Actix-based GraphQL server (for example, using async-graphql or juniper with Actix-web), enabling introspection is common during development. When combined with mTLS, an authenticated but potentially untrusted client can probe for sensitive operations such as exportData or internalHealth. Attackers with valid client certificates can automate introspection queries to map the API surface, which can aid in crafting further attacks like BOLA (Broken Object Level Authorization) by identifying object IDs and nested relations. The risk is compounded if the schema includes overly permissive queries or if field-level authorization is implemented only at the resolver level without schema-level restrictions.
Consider a scenario where your Actix GraphQL endpoint is reachable at https://api.example.com/graphql and mTLS is enforced by the Actix-web middleware. An authenticated attacker can send an introspection query over the mutually authenticated TLS channel to learn the schema. For example, the following query is what an attacker would submit to enumerate operations and types:
{ __schema { queryType { name } mutationType { name } types { name kind possibleTypes { name } } } }
If the server responds with a full schema, the attacker gains knowledge of mutation names like deleteUser or rotateApiKey, which can then be targeted for IDOR or BOLA if authorization checks are weak. Since mTLS does not limit introspection by itself, the combination creates a pathway for information disclosure that should be explicitly controlled.
Mutual Tls-Specific Remediation in Actix — concrete code fixes
To secure an Actix GraphQL service using mutual TLS while mitigating introspection risks, you must enforce mTLS at the web layer and explicitly disable introspection in production. Below are concrete patterns and configuration examples.
1. Enforce mTLS in Actix-web
Use Rust TLS acceptor with client certificate verification. The following example configures an Actix-web server that requires client certificates and validates them against a trusted CA:
use actix_web::{web, App, HttpServer};
use actix_web_httpauth::extractors::AuthenticationError;
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod, SslVerifyMode};
fn create_ssl_acceptor() -> std::io::Result<SslAcceptor> {
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls_server())?;
builder.set_private_key_file("keys/server.key", SslFiletype::PEM)?;
builder.set_certificate_chain_file("certs/server.crt")?;
builder.set_client_ca_file(&["certs/ca.crt"])?;
builder.set_verify(SslVerifyMode::PEER | SslVerifyMode::FAIL_IF_NO_PEER_CERT,
|_ssl, cert| cert.subject_name().find_entry_by_nid(nid::COMMONNAME).is_some());
Ok(builder.build())
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let ssl = create_ssl_acceptor().expect("SSL acceptor");
HttpServer::new(|| {
App::new()
.wrap(actix_web_httpauth::HttpAuthentication::basic("auth", |req, credentials| {
// optional additional app-level auth
future::ok(())
}))
.service(web::resource("/graphql").to(graphql_handler))
})
.bind_openssl("127.0.0.1:8443", ssl)?
.run()
.await
}
This ensures that only clients presenting a certificate signed by the trusted CA can establish a TLS connection. However, mTLS alone does not prevent introspection, so you must also disable it explicitly.
2. Disable introspection in production
If you use async-graphql, you can disable introspection by configuring the schema:
use async_graphql::*;
use async_graphql_actix_web::*;
struct Query;
#[Object]
impl Query {
async fn hello(&self) -> &str { "world" }
}
#[actix_web::main]
async fn main() {
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
.disable_introspection() // disables introspection queries
.finish();
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(schema.clone()))
.service(graphql::post("/graphql"))
})
.bind("127.0.0.1:8080")?
.run()
.await
.unwrap();
}
For juniper-based services, disable introspection via GraphQLWalk::introspection_method or by not including the introspection fields in your root object. In production, ensure introspection is disabled while keeping mTLS for client authentication. This prevents authenticated clients from enumerating operations and types, reducing the attack surface exposed through the GraphQL layer.
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 |