Graphql Introspection in Express with Api Keys
Graphql Introspection in Express with Api Keys — how this specific combination creates or exposes the vulnerability
GraphQL introspection in an Express server exposes the full schema, including types, queries, and mutations. When API keys are used for authorization but introspection remains accessible without proper checks, an unauthenticated or low-privilege caller can enumerate the API surface. This combination creates a risk where an attacker can discover endpoints, query arguments, and response shapes, which can aid in crafting further attacks such as BOLA/IDOR or data exposure.
In Express, common patterns serve GraphQL via libraries like express-graphql or Apollo Server. If introspection is enabled (often the default in development) and the route does not validate the caller’s API key before resolving the introspection query, the API key mechanism is bypassed for schema discovery. For example, a route like /graphql that accepts POST bodies with { "query": "{ __schema { queryType { name } } }" } will return schema details even when the request includes an API key that is only validated after route entry or not validated for introspection operations specifically.
Consider an Express setup where API keys are checked via middleware, but the GraphQL handler does not gate introspection behind the same check:
const { graphqlHTTP } = require('express-graphql');
const schema = require('./schema');
app.post('/graphql', (req, res, next) => {
const key = req.headers['x-api-key'];
if (!validKeys.includes(key)) return res.status(401).send('Unauthorized');
next();
}, graphqlHTTP({ schema, graphiql: true }));
If an attacker sends an introspection query without a valid API key to a misconfigured route, or if the GraphQL handler allows introspection regardless of the middleware outcome in certain error paths, the schema is returned. This can reveal queries like users(id: $id) or fields that should be protected, effectively mapping the attack surface for further exploitation such as IDOR.
The risk is elevated when combined with other unchecked operations. Introspection can expose arguments that are susceptible to injection or authorization flaws. For instance, if a user(id: ID!) query is exposed and lacks proper ownership checks, an attacker can iterate over IDs knowing the exact field names and types. middleBrick’s scans detect such exposure as part of its Authorization and Input Validation checks, highlighting the need to restrict introspection in production.
Api Keys-Specific Remediation in Express — concrete code fixes
To secure GraphQL introspection in Express when using API keys, ensure introspection is disabled or strictly gated by the same authorization checks applied to other operations. Do not rely on network-level obscurity or development-only settings in production.
1) Disable introspection in production builds:
const { graphqlHTTP } = require('express-graphql');
const schema = require('./schema');
app.post('/graphql', (req, res, next) => {
const key = req.headers['x-api-key'];
if (!validKeys.includes(key)) return res.status(401).send('Unauthorized');
next();
}, graphqlHTTP({ schema, graphiql: false, introspection: false }));
Setting introspection: false ensures the __schema and __type root fields are unavailable regardless of authentication, eliminating enumeration via introspection.
2) Conditional introspection with API key validation inside the handler:
app.post('/graphql', (req, res) => {
const key = req.headers['x-api-key'];
if (!validKeys.includes(key)) return res.status(401).json({ errors: [{ message: 'Unauthorized' }] });
const { query } = req.body || {};
const isIntrospection = query && (query.includes('__schema') || query.includes('__type'));
if (isIntrospection) {
return res.status(403).json({ errors: [{ message: 'Introspection disabled' }] });
}
// proceed with validated key and business logic
graphqlHTTP({ schema, graphiql: false })(req, res);
});
This pattern validates the API key first, then explicitly blocks introspection queries before they reach the GraphQL handler, ensuring that only authorized callers can perform operations and that schema discovery is not accidentally exposed.
3) Use Apollo Server with plugins for more structured control:
const { ApolloServer, gql } = require('apollo-server-express');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const typeDefs = gql`
type Query { hello: String }
`;
const resolvers = { Query: { hello: () => 'world' } };
const schema = makeExecutableSchema({ typeDefs, resolvers });
const server = new ApolloServer({
schema,
plugins: [{
requestDidStart() {
return {
didResolveOperation({ request, operation }) {
const key = request.http.headers.get('x-api-key');
if (!validKeys.includes(key)) {
throw new Error('Unauthorized');
}
if (operation.query && (operation.query.includes('__schema') || operation.query.includes('__type'))) {
throw new Error('Introspection disabled');
}
}
};
}
}]
});
await server.start();
server.applyMiddleware({ app });
With this setup, API key validation and introspection blocking are enforced at the GraphQL request level, aligning the security posture with the authorization model used across the API.
Finally, if you need limited introspection for tooling in specific environments (e.g., staging), restrict it by IP allowlist or require a special API key scope, and ensure the route is not exposed to the public internet. middleBrick’s scans can verify whether introspection remains exposed and map findings to frameworks like OWASP API Top 10 to prioritize remediation.
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 |