GraphQL introspection and unprotected fields
CWE-200CWE-285OWASP A05:2021Updated September 4, 20265 min read
With introspection, anyone can retrieve the full GraphQL schema: every type, every field and every mutation, including what your own frontend never uses. Because the client decides which fields to request in GraphQL, authorisation has to happen per field. If it does not, everything in the schema is also retrievable.
GraphQL shifts part of the control to the client: it decides which fields to request. That is the strength of the model and also the reason security works differently there. A check per endpoint does not suffice here, and introspection gives away more than most teams expect.
What is introspection?
Introspection is a built-in feature of GraphQL allowing a client to retrieve the complete schema: all types, all fields with their data types, all queries and all mutations. The feature exists to enable tooling, an editor that autocompletes as you type, documentation that generates itself, and during development it is particularly useful.
In production it is a full table of contents of your data model, retrievable by anyone who can reach the endpoint. On top of that comes the second and more important point: because the client decides which fields to request, the schema is at the same time a list of what is available. Without a per-field check, everything in the schema can genuinely be retrieved.
The difference from a classic API sits exactly there. With REST you determine per endpoint what goes back; the number of possible responses is finite and manageable. With GraphQL the client determines the shape of its question, and your schema is the boundary. What you put in the schema, you should also secure.
How does an attacker abuse introspection?
Vulnerable:
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: true, // in production too
});
# Anyone can retrieve the full schema
query { __schema { types { name fields { name type { name } } } } }
The response reveals which types exist, along with fields the frontend never uses. An attacker then simply composes their own question:
query {
users {
id
name
email
passwordHash # sits in the schema, therefore retrievable
mfaSecret
internalNote
}
}
The official frontend asks only for id and name. That the other fields exist is enough: there is no check saying passwordHash is not meant for this user. A single query thereby yields data that would never have received an endpoint through a classic API.
Safe:
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production',
validationRules: [depthLimit(7), complexityLimit(1000)],
formatError: (error) => ({ message: 'Request could not be processed' }),
});
// Authorisation on the field itself, not on the query
const resolvers = {
User: {
email: (parent, args, ctx) => {
if (ctx.user.id !== parent.id && ctx.user.role !== 'admin') {
return null;
}
return parent.email;
},
// Fields that may never go out do not belong in the schema
},
Query: {
users: async (parent, args, ctx) => {
if (!ctx.user) throw new GraphQLError('Not authenticated');
return users.visibleTo(ctx.user);
},
},
};
Four things happen here. Introspection is on only outside production. Query depth and complexity are bounded, which prevents overload through deeply nested questions. Error messages no longer suggest field names. And most importantly: authorisation sits on the field, where it belongs. Fields that may never go out under any circumstance, password hashes, two-factor secrets, are not in the schema at all.
What is the impact of GraphQL introspection?
The severity runs from medium to high and depends on what is reachable without a field check. With a schema containing only public data, it stays at information about your data model. With sensitive fields lacking their own authorisation, it is a data breach executed with a single query.
What increases the risk is the combination with other properties of GraphQL. There is usually a single endpoint, which makes per-endpoint restrictions largely meaningless. One request can fetch data from many types at once, so an attacker obtains a lot in few requests. And nested relations make it possible to reach data through a detour that is protected at the top level, for instance by requesting the user of a message rather than the user directly.
The load side comes on top of that. A deeply nested query can force the server into an enormous amount of work, which without depth and complexity limits can lead to an outage.
How do you detect GraphQL introspection?
A tester first tries whether introspection is enabled with a standard schema query. If that works, the full schema is available and attention shifts immediately to the fields the frontend does not use.
With introspection off, the schema is reconstructed anyway: from the JavaScript bundle, from the queries the application itself sends, and by trying field names and watching the error messages, since implementations that suggest a correction on a typo thereby reveal which fields do exist. Each field found is then tested for authorisation using an account with minimal rights. They also look at nested paths that route around a check, at the bounding of depth and complexity, and at whether mutations have the same checks as queries. AssistSec tests GraphQL endpoints separately from the rest of the API, because standard tooling aimed at REST reveals little here.
How do you prevent GraphQL introspection?
- Disable introspection in production and keep it on only in development environments.
- Authorise per field in the resolvers, not only at query or mutation level.
- Do not include fields that may never go out in the schema at all.
- Bound the depth and complexity of queries, and cap the number of results per level.
- Return neutral error messages and switch off suggestions for similar field names.
- Set a rate limit that accounts for the cost of a query, not only the number of requests.
- Check nested relations, because they can form a detour around a protected top-level field.
- Apply the same authorisation to mutations as to queries.
- Treat your schema as public information and do not let security rest on obscurity.
Sources
Frequently asked questions
Is disabling introspection enough?
No, it is at most a hurdle. The schema can also be derived from your frontend's JavaScript bundle, from error messages suggesting field names, and by systematically trying names. The real measure is per-field authorisation; disabling only makes reconnaissance harder.
Why is per-field authorisation needed?
Because in GraphQL the client composes which fields it wants. A check at query level says nothing about whether this particular field is meant for this user. If a sensitive field sits in the schema without its own check, it is retrievable, even though your frontend never asks for it.
What is a depth attack?
A query built up deeply through relations, for instance author to posts to author and onward. Without limits, one request can put enormous load on the server. Set a maximum depth and a complexity limit, and cap the number of results per level.
Should I change error messages?
Yes. GraphQL implementations often suggest a similar name for an unknown field. That is helpful during development and a reconnaissance aid in production. Switch those suggestions off and return neutral error messages.
Related articles
- VulnerabilitiesCWE-285A01:2021Insufficient function level authorizationAn admin function merely hidden from the menu stays reachable through a direct request. Learn how to enforce authorisation per function.
- VulnerabilitiesCWE-639A01:2021Insufficient object level authorization in APIsAn API that only checks whether you are logged in, not whether this record is yours, hands over other people's data. Learn how to enforce it.
- VulnerabilitiesCWE-213A01:2021API responses contain too much dataAn API returning whole database records and leaving the filtering to the frontend leaks fields nobody was meant to see.
- VulnerabilitiesCWE-200A01:2021Information disclosureInformation disclosure explained: how stack traces, .git directories, source maps and over-sharing API responses leak data, and how to stop it.
- VulnerabilitiesCWE-770A04:2021No rate limiting on the APIWithout rate limiting an API can be queried without end. Learn how that leads to data theft, cost abuse and outages.