Graphql Introspection in Express with Dynamodb
Graphql Introspection in Express with Dynamodb — how this specific combination creates or exposes the vulnerability
GraphQL introspection in an Express service that uses DynamoDB as the persistence layer can expose both data structure and operational details that increase the attack surface. Introspection is a core GraphQL feature that returns the full schema, including types, queries, mutations, and directives. When enabled in production, it allows an unauthenticated attacker to map your API surface, discover query names such as getUser or listOrders, and infer naming conventions that map to DynamoDB table operations or Lambda-backed resolvers.
In this stack, introspection becomes a reconnaissance aid for further abuse. For example, an attacker can use introspection to identify fields that accept filter objects, which may translate into DynamoDB query parameters like KeyConditionExpression or FilterExpression. If those resolvers do not enforce strict authorization and input validation, the exposed schema can guide an Insecure Direct Object References (IDOR) or Broken Function Level Authorization (BFLA) attack. The DynamoDB layer itself does not leak via introspection, but the schema reveals how data is organized, which tables or indexes are likely referenced, and whether operations are paginated or filtered in ways that can be abused.
Additionally, introspection responses can disclose default values and resolver signatures, which may hint at unauthenticated or weakly authenticated endpoints. When combined with other checks such as BOLA/IDOR or Property Authorization, an attacker can correlate schema information with runtime behavior to construct more precise attacks. This is especially risky if the GraphQL server is also exposing an unauthenticated LLM endpoint, as schema details could be used in prompt injection or data exfiltration probes. The combination of introspection, DynamoDB data modeling patterns, and Express routing creates a scenario where metadata becomes actionable intelligence for an attacker.
Dynamodb-Specific Remediation in Express — concrete code fixes
To secure GraphQL introspection in Express with DynamoDB, disable introspection in production and enforce strict input validation and authorization at the resolver level. Below are concrete, realistic examples that demonstrate how to implement these controls while interacting with DynamoDB using the AWS SDK for JavaScript.
1. Disable introspection in production
Use graphql-builders or graphql utilities to conditionally enable introspection. In production, set introspection: false.
const { buildSchema } = require('graphql');
const isProduction = process.env.NODE_ENV === 'production';
const schema = buildSchema(`
type Query {
getUser(id: ID!): User
}
type User {
id: ID!
email: String
}
`);
const graphqlHTTP = require('express-graphql');
app.use('/graphql', graphqlHTTP((req, res, graphQLParams) => ({
schema: schema,
graphiql: false,
introspection: !isProduction,
customFormatErrorFn: (err) => ({ message: err.message })
})));
2. Validate and sanitize DynamoDB inputs in resolvers
Ensure that inputs like id or filter are validated before being passed to DynamoDB. Use allowlists and avoid directly passing user input into KeyConditionExpression.
const { DynamoDBClient, GetItemCommand } = require('@aws-sdk/client-dynamodb');
const client = new DynamoDBClient({ region: 'us-east-1' });
async function getUserById(userId) {
if (!/^[a-f0-9-]+$/.test(userId)) {
throw new Error('Invalid user ID');
}
const command = new GetItemCommand({
TableName: process.env.USERS_TABLE,
Key: {
id: { S: userId }
}
});
const response = await client.send(command);
return response.Item ? { id: response.Item.id.S, email: response.Item.email.S } : null;
}
3. Enforce authorization in resolvers, not at the DynamoDB layer
DynamoDB does not enforce row-level permissions. Implement authorization in your GraphQL resolvers and pass only permitted parameters to DynamoDB.
async function getUserResolver(root, args, context) {
const requestingUserId = context.user?.sub;
if (!requestingUserId || requestingUserId !== args.id) {
throw new Error('Unauthorized');
}
return getUserById(args.id);
}
4. Use environment-based configuration for endpoint exposure
Ensure that the GraphQL endpoint is not publicly exposed in environments where it is not needed. In Express, conditionally mount the middleware.
if (process.env.NODE_ENV !== 'production') {
app.use('/graphql', graphqlHTTP({ /* config */ }));
}
5. Monitor and log suspicious patterns without exposing schema
Log introspection requests and high-risk patterns (e.g., attempts to query internal types) without returning schema details to the client.
app.use('/graphql', (req, res, next) => {
if (req.body && req.body.query && req.body.query.includes('__schema')) {
// Log for audit without exposing details
console.warn('Introspection query detected', { userId: req.user?.sub });
}
next();
}, graphqlHTTP({ /* config with introspection disabled in prod */ }));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 |