Graphql Introspection in Fastapi with Jwt Tokens
Graphql Introspection in Fastapi with Jwt Tokens — how this specific combination creates or exposes the vulnerability
GraphQL introspection in a FastAPI application exposes the schema, queries, and types of your API through the standard __schema and __type operations. When combined with JWT-based authentication, a misconfiguration can unintentionally expose introspection to authenticated contexts where it should be limited or disabled. Even when endpoints expect a valid JWT, developers sometimes allow introspection at the same route without additional constraints, relying solely on authentication to limit access.
This combination creates risk because authenticated requests with valid JWTs can still trigger introspection queries, giving attackers detailed insight into the API structure, including queries, mutations, subscriptions, and field relationships. Attackers can use this information to discover sensitive operations, locate hidden fields, and plan further attacks such as injection or IDOR. If introspection is enabled broadly and JWT validation does not explicitly restrict it, the authentication mechanism fails to reduce the attack surface effectively.
In FastAPI, GraphQL servers are often implemented using libraries like Strawberry or GraphQL-core. If the GraphQL handler is mounted under a protected route that validates JWTs but introspection is left enabled without scope or role checks, any client that possesses a valid token can still retrieve the full schema. This is especially problematic when the JWT does not enforce fine-grained permissions for introspection, or when the validation middleware applies authentication but not authorization for the introspection operation itself.
For example, a FastAPI route that decodes a JWT and passes user context to GraphQL resolvers may still allow introspection queries to proceed if the GraphQL schema exposes __schema without additional guards. The presence of a valid JWT does not inherently prevent introspection; without explicit schema-level or route-level controls, attackers can leverage authenticated introspection to map the API.
To mitigate this specific combination, you must ensure that introspection is either disabled in production or tightly controlled based on JWT claims, roles, or scopes. This requires explicit schema configuration and route-level checks rather than relying on authentication alone.
Jwt Tokens-Specific Remediation in Fastapi — concrete code fixes
Remediation focuses on ensuring JWT validation explicitly governs introspection access. In FastAPI, you can conditionally disable introspection or restrict it based on token claims. Below are concrete code examples that show how to implement this safely.
Example 1: Disable introspection by default in Strawberry
import strawberry
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
security = HTTPBearer()
def verify_jwt(credentials: HTTPAuthorizationCredentials = Depends(security)):
# Replace with actual JWT validation logic
if credentials.credentials != "valid-token":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token",
headers={"WWW-Authenticate": "Bearer"},
)
return {"sub": "user", "scopes": ["read"]}
@strawberry.type
class Query:
@strawberry.field
def public_field(self) -> str:
return "ok"
# Disable introspection explicitly
schema = strawberry.Schema(query=Query, enable_introspection=False)
app = FastAPI()
@app.post("/graphql")
async def graphql_endpoint(
data: str,
token_info: dict = Depends(verify_jwt),
):
# You can further inspect token_info["scopes"] to allow introspection selectively
return {"data": data}
Example 2: Conditional introspection with GraphQL-core and FastAPI
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from graphql import GraphQLSchema, build_schema, graphql_sync
from graphql.execution.base import ExecutionResult
security = HTTPBearer()
def verify_jwt(credentials: HTTPAuthorizationCredentials = Depends(security)):
if credentials.credentials != "valid-token":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token",
headers={"WWW-Authenticate": "Bearer"},
)
return {"sub": "user", "scopes": ["read", "introspect"]}
schema_str = """
type Query {
public: String
secret: String @auth(requires: ADMIN)
}
"""
schema: GraphQLSchema = build_schema(schema_str)
app = FastAPI()
def get_introspection_data(allowed: bool) -> dict:
if not allowed:
return {}
from graphql import graphql_sync
from graphql.type import build_schema, GraphQLSchema
result = graphql_sync(schema, "__schema { queryType { name } }")
return result.data or {}
@app.post("/graphql")
async def graphql_endpoint(
query: str,
token_info: dict = Depends(verify_jwt),
):
can_introspect = "introspect" in token_info.get("scopes", [])
if query.strip().startswith("__") and not can_introspect:
raise HTTPException(status_code=400, detail="Introspection not allowed")
# Execute query with schema, passing context with token_info if needed
result: ExecutionResult = graphql_sync(schema, query)
return {"data": result.data, "errors": [str(e) for e in result.errors]}
Example 3: Middleware to enforce introspection rules per request
from fastapi import FastAPI, Request, HTTPException, status
from fastapi.middleware import Middleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
import jwt
app = FastAPI()
JWT_PUBLIC_KEY = "your-public-key"
def decode_jwt(token: str) -> dict:
try:
return jwt.decode(token, JWT_PUBLIC_KEY, algorithms=["RS256"], audience="api", issuer="auth.example.com")
except jwt.PyJWTError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
@app.middleware("http")
async def enforce_introspection_policy(request: Request, call_next):
if request.url.path == "/graphql" and request.method == "POST":
auth_header = request.headers.get("authorization")
if auth_header:
token = auth_header.split(" ")[1]
claims = decode_jwt(token)
body = await request.body()
# parse GraphQL query from body (simplified)
import json
data = json.loads(body)
query = data.get("query", "")
if query.strip().startswith("__") and not claims.get("can_introspect"):
raise HTTPException(status_code=403, detail="Introspection forbidden by policy")
else:
raise HTTPException(status_code=401, detail="Missing authorization")
response = await call_next(request)
return response
Remediation summary
- Disable introspection globally in production unless explicitly required.
- Use JWT scopes or roles to gate introspection and avoid allowing it for general authenticated users.
- Validate and filter introspection queries at the route or middleware layer, not only at the schema level.
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 |