Graphql Introspection in Django with Mutual Tls
Graphql Introspection in Django with Mutual Tls — how this specific combination creates or exposes the vulnerability
GraphQL introspection in Django, when combined with mutual TLS (mTLS), can expose information that an authenticated endpoint might otherwise protect. Introspection allows clients to query the GraphQL schema, including types, queries, and mutations, which is useful during development but can aid an attacker in mapping the API surface. In a Django GraphQL setup using packages such as graphene-django or Strawberry, introspection is often enabled by default in non-production settings. Even when mTLS is enforced at the transport layer to authenticate clients, introspection can still reveal details about object models, field relationships, and resolver logic that are not directly exposed through schema design.
Mutual TLS ensures that both client and server present certificates, which strongly authenticates the client to the Django application. However, mTLS does not limit what the authenticated client can query once the TLS handshake completes. If introspection is allowed for mTLS-authenticated requests, an attacker who obtains a valid client certificate can still perform schema queries to explore endpoints, field arguments, and response shapes. This becomes a concern when introspection is unintentionally permitted in production or when certificate-based access control does not align with GraphQL authorization rules.
The combination can inadvertently widen the attack surface: mTLS secures the channel, but if introspection is not explicitly restricted, it provides a structured map of the API to any client that presents a valid certificate. This is particularly relevant in microservice environments where mTLS is common and GraphQL endpoints are exposed internally. Moreover, introspection responses may include details about input types and default values that can be leveraged in further attacks, such as crafting malicious queries or probing for business logic flaws.
To evaluate risk, you can scan the endpoint with middleBrick, which checks unauthenticated and authenticated attack surfaces, including GraphQL-specific exposures where applicable. Its LLM/AI Security checks can also detect whether introspection responses might leak information that could be used in prompt injection or data exfiltration scenarios.
Mutual Tls-Specific Remediation in Django — concrete code fixes
To secure GraphQL introspection in Django when using mutual TLS, you should disable introspection for production or limit it to specific internal clients, and enforce strict client certificate mapping to views. Below are concrete code examples for configuring mTLS in Django and controlling introspection behavior.
Mutual TLS setup in Django
Use django-sslserver or a reverse proxy (such as Nginx or Traefik) to handle client certificate verification, and pass the verified client certificate information to Django via headers. Ensure your Django settings validate the certificate chain and extract the client identity.
import ssl
from pathlib import Path
# settings.py
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Assume the reverse proxy sets these headers after verifying client certs
CLIENT_CERT_HEADER = 'SSL_CLIENT_S_DN_CN' # Example header from proxy
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'yourapp.middleware.ClientCertificateMiddleware', # Custom middleware
]
Custom middleware to enforce client certificate validation
This middleware checks that a required header (populated by the proxy after mTLS verification) is present and optionally maps the certificate to an allowed list of subjects.
import logging
from django.http import HttpResponseForbidden
logger = logging.getLogger(__name__)
class ClientCertificateMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
cert_subject = request.META.get('HTTP_X_SSL_CLIENT_DN')
if not cert_subject:
logger.warning('Missing client certificate')
return HttpResponseForbidden('Client certificate required')
# Optionally validate against an allowlist
allowed_subjects = {'CN=internal-client-1', 'CN=internal-client-2'}
if cert_subject not in allowed_subjects:
logger.warning(f'Unauthorized client certificate: {cert_subject}')
return HttpResponseForbidden('Client certificate not authorized')
request.client_subject = cert_subject
return self.get_response(request)
Disabling or restricting introspection in GraphQL views
In your GraphQL view, conditionally disable introspection based on the request or client identity. With graphene-django, you can pass graphiql=False and control the schema introspection option. With Strawberry, use environment-based flags or request checks.
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView
class SecureGraphQLView(GraphQLView):
def get_response(self, request, data, show_graphiql=False):
# Disable introspection for non-internal clients
if not getattr(request, 'client_subject', '').startswith('CN=internal'):
# Force schema to exclude introspection fields if supported
# This pattern depends on your GraphQL library's capabilities
data['query'] = self.mask_introspection_query(data.get('query', ''))
return super().get_response(request, data, show_graphiql=False)
def mask_introspection_query(self, query):
# Simple safeguard: remove introspection fields for non-allowed clients
import re
if query and re.search(r'\b(introspection|__schema|__type)\b', query):
return '{ __typename }' # Return a minimal safe response
return query
urlpatterns = [
path('graphql/', csrf_exempt(SecureGraphQLView.as_view(graphiql=False))),
]
For Strawberry, you can gate introspection behind a runtime check:
import strawberry
from strawberry.http import GraphQLRequest
from django.conf import settings
@strawberry.type
class Query:
@strawberry.field
def hello(self) -> str:
return "world"
schema = strawberry.Schema(query=Query)
@csrf_exempt
def graphql_view(request):
if request.method == 'OPTIONS':
return HttpResponse('OK')
body = request.body if hasattr(request, 'body') else b''
# Only allow introspection for trusted client certificates
if 'introspection' in body.decode('utf-8', errors='ignore') and not request.META.get('HTTP_X_SSL_CLIENT_DN', '').startswith('CN=internal'):
return HttpResponse('Introspection not allowed', status=403)
return schema.execute_sync(
request.body.decode('utf-8'),
context_value={'request': request},
)
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 |