Graphql Introspection in Adonisjs with Mutual Tls
Graphql Introspection in Adonisjs with Mutual Tls — how this specific combination creates or exposes the vulnerability
GraphQL introspection in an AdonisJS application reveals the full schema, queries, and mutations, which is valuable for developers but also for an attacker. When mutual TLS (mTLS) is used, the server validates the client certificate, but introspection is often left enabled in development or misconfigured in production. This combination creates a scenario where access is gated by mTLS, yet the exposed schema still allows information disclosure.
In AdonisJS, the GraphQL server is typically set up using packages such as @adonisjs/fold and a community GraphQL provider. Introspection is commonly enabled by default in development environments. If introspection is unintentionally exposed over an endpoint protected only by mTLS, an authenticated client with a valid certificate can run introspection queries to map the API surface. Attackers that compromise or steal a client certificate can leverage introspection to discover sensitive types, relationships, and query patterns, which can aid in crafting further attacks such as injection or IDOR.
mTLS ensures that only clients with a trusted certificate can reach the endpoint, but it does not reduce the attack surface of the GraphQL schema itself. If the endpoint lacks additional authorization checks around introspection, the combination of mTLS and open introspection can lead to unnecessary information exposure. This is particularly risky when the GraphQL endpoint also supports unauthenticated or weakly authenticated queries, as introspection may bypass intended access controls that rely solely on HTTP-level mTLS.
Real-world patterns show that introspection responses can include type definitions that reference internal models or business logic, effectively acting as an unauthentated enumeration mechanism for authenticated clients. For example, an introspection query can reveal queries like user(id: ID!): User or fields that should be restricted. Even with mTLS, an attacker with a valid certificate can automate introspection to map the API and identify endpoints for further exploitation, such as probing for BOLA or property authorization issues.
To assess this risk, scanners run parallel security checks including Authentication, Authorization, and Input Validation, while also testing for unauthenticated LLM endpoints and system prompt leakage. For GraphQL endpoints, introspection is treated as a potential information disclosure finding when it is accessible without proper authorization controls. The scanner evaluates whether the endpoint exposes schema details and maps findings to frameworks such as OWASP API Top 10 and PCI-DSS.
Mutual Tls-Specific Remediation in Adonisjs — concrete code fixes
To secure GraphQL introspection in AdonisJS with mutual TLS, you should disable introspection in production and enforce strict mTLS policies. Below are concrete code examples and configuration steps.
1. Configure the GraphQL server to disable introspection in production
When initializing the GraphQL provider, set introspection to false in production environments. In AdonisJS, this is typically done in the provider setup or configuration file.
// start/app.ts or a dedicated GraphQL provider file
import { Application } from '@adonisjs/core/types'
import { graphql } from '@adonisjs/graphql'
export default class AppProvider {
constructor(protected app: Application) {}
public register() {
this.app.container.singleton('graphql', () => {
return graphql({
introspection: this.app.environment === 'development', // disable in production
typeDefs: () => import('App/Graphql/types'),
resolvers: () => import('App/Graphql/resolvers'),
})
})
}
}
2. Enforce mutual TLS at the server or proxy layer
Mutual TLS is commonly enforced at the HTTP server or reverse proxy (e.g., Nginx, Caddy, or a cloud load balancer). Below is an example Nginx configuration that requires client certificates and passes only validated requests to the AdonisJS server.
# nginx.conf or site configuration
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/server.crt;
ssl_certificate_key /etc/ssl/private/server.key;
ssl_client_certificate /etc/ssl/certs/ca-bundle.crt;
ssl_verify_client on;
location /graphql {
proxy_pass http://127.0.0.1:3333;
proxy_set_header X-SSL-CERT $ssl_client_escaped_cert;
proxy_set_header X-SSL-VERIFY $ssl_client_verify;
}
}
In this setup, ssl_verify_client on ensures that only clients with a trusted certificate can reach the endpoint. The AdonisJS app can optionally read X-SSL-CERT or X-SSL-VERIFY headers for additional logging or authorization, but the gatekeeping is done at the proxy layer.
3. Add route-level authorization for introspection
Even with mTLS, you can add an explicit check in your GraphQL route or middleware to block introspection for non-trusted contexts. For example, using a custom middleware in AdonisJS:
// start/middleware/introspection_guard.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class IntrospectionGuard {
public async handle({ request, response, next }: HttpContextContract) {
const isGraphqlRequest = request.url().startsWith('/graphql')
const isIntrospection = request.input('query', '').includes('__schema')
if (isGraphqlRequest && isIntrospection) {
// Optionally allow only specific authenticated roles or IPs
const clientCert = request.header('x-ssl-cert')
if (!this.isAllowedCertificate(clientCert)) {
return response.forbidden({ error: 'Introspection not allowed' })
}
}
await next()
}
private isAllowedCertificate(cert?: string): boolean {
// Implement logic to validate certificate fingerprints or subjects
return !!cert && cert.includes('trusted-fingerprint')
}
}
Register this middleware in your route or global middleware stack to block unauthorized introspection attempts. This complements mTLS by adding an application-layer check.
4. Use environment-based feature flags
Control introspection via environment variables so that you can toggle it without redeploying. This is useful for temporary debugging in production while keeping the default secure.
// .env
GRAPHQL_INTROSPECTION=true # set to false in production
// config/graphql.ts
import { Env } from '@ioc:Adonis/Core/Env'
export default {
introspection: Env.get('GRAPHQL_INTROSPECTION', 'false') === 'true',
}
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 |