Example finding How it works Coverage PENTEST METHODOLOGY DOCS PRICING FAQ MCP CONTACT LOG IN SIGN UP →

GraphQL introspection enabled in production: how to fix it

FIX / GRAPHQL INTROSPECTION / 9 MIN READ

GraphQL introspection in production gives every caller a full map of your API, so turn it off for the public endpoint and add an access check to every field. Introspection is not a bug. It is a built-in feature that returns the types, fields, and arguments of your schema. The danger is that an attacker uses the map to find a field that your code forgot to protect.

This guide shows how to confirm that the schema is open, how to close it, and how to fix the cause behind it.

What does “GraphQL introspection enabled” mean?

It means that your endpoint answers special queries that start with __schema or __type. The answer lists every type, query, mutation, and field that the server knows. Tools such as GraphQL Playground and Apollo Sandbox use these queries to show documentation and autocomplete.

The Apollo Server documentation describes the introspection option. When it is true, clients can read the schema. The default is true unless NODE_ENV is set to production. In that case, the default is false.

This default has one weak point. If your host does not set NODE_ENV, the server runs with introspection on. AI-generated projects often use another server, such as GraphQL Yoga, Hasura, or a custom setup, and each one has its own option. Read the documentation of your server for the exact setting.

OWASP gives a clear rule in its GraphQL cheat sheet: disable introspection queries system-wide in any production or publicly accessible environment. The cheat sheet also notes that you can keep access for approved consumers.

Be careful with the idea behind the rule. Introspection does not give access to data. It gives knowledge about the API. A schema that shows User.passwordHash, Query.allOrders, or Mutation.deleteUser helps an attacker to aim. If each of these fields checks the caller, the attacker still gets nothing. If one field does not check the caller, the attacker finds it in seconds.

For this reason, treat introspection as a warning sign. An open schema shows what you must protect. The fix has two parts:

Part Goal
Disable introspection in production Remove the free map
Add authorization and limits to every field and query Make the map useless

Do both. The first part is quick. The second part is the real security. For a wider view of API discovery problems, read the pattern on GraphQL, Swagger, and gRPC exposure.

How do I check if introspection is enabled?

Send an introspection query to your production endpoint. You do not need a login for the first test. The test shows what a stranger sees.

  1. Find the endpoint. The common paths are /graphql, /api/graphql, /gql, and /v1/graphql. Open DevTools on your app and look at the Network tab. A request with a query field in the body is a GraphQL call.
  2. Send a small query. This query asks for the name of the query type:
curl -s https://your-app.example/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ __schema { queryType { name } } }"}'
  1. Read the result. A response such as {"data":{"__schema":{"queryType":{"name":"Query"}}}} shows that introspection is on. An error that says introspection is disabled shows that it is off.
  2. List the fields. If the first query works, ask for the types and fields:
curl -s https://your-app.example/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ __schema { types { name fields { name } } } }"}'
  1. Read the names. Look for fields such as password, token, secret, admin, internal, export, and delete. Each name is a place to test.
  2. Test with a user token too. Repeat the call with the token of a normal user. A server can allow introspection for any logged-in user.
  3. Test the development URLs. Check /graphiql, /playground, and /sandbox. Some servers expose a browser IDE in production.

Also test field suggestions. Send a query with a misspelled field:

curl -s https://your-app.example/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ usr { id } }"}'

If the error says Did you mean "user"?, the server leaks field names even when introspection is off. OWASP warns about this in the cheat sheet. It says that attackers can guess field names by brute force and that the suggestions can leak them, so you must disable the suggestions too.

Warning: Run these tests only against your own app. Use a test account for the token test.

What do I do first? Fix it now, in order of urgency

Do the steps in this order. Step 1 is fast. Steps 2 and 3 do the real work.

Caution: Disable introspection only after you check your tools. A code generator, an IDE plugin, or a mobile app build step can call introspection at build time. Move that step to a development server before you deploy.

  1. Disable introspection in production. In Apollo Server, set the option in the constructor:
const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== "production",
});

This line is explicit. It does not depend on the default. Also set NODE_ENV=production in the host settings, because other libraries read it too. For other servers, search the documentation for “disable introspection”.

  1. Remove the browser IDE. Turn off GraphiQL, Playground, and Sandbox in production. Do not serve them from the same host as the API.
  2. Disable field suggestions. Check if your server has an option for it. If it has none, use a plugin or a small error-formatting step that removes the Did you mean text.
  3. Add an access check to every field that holds private data. See the next section.
  4. Add depth and cost limits. See the section after the next.
  5. Test the live endpoint again. Repeat the curl tests. The introspection query must fail, and the misspelled field must not return a suggestion.
  6. Check your logs. See the section on damage.

If your endpoint is not for the public, use a stronger step: require a login before the server runs any query. A private API does not need to answer anonymous callers at all.

How do I fix the real cause: missing authorization?

Check access in the code that returns the data. Do not rely on the schema being hard to find.

The Apollo documentation on authentication and authorization shows the usual design. You put the user in contextValue. A resolver can then check the role and return null or an error for a field that the user may not read. The same page advises that you move the checks into your business logic layer. Then every path to the same data, such as Query.user and Post.author, uses the same rule.

OWASP asks for the same checks at two levels:

  • Object level. Always check that the requester may read or change the object. Check this for edges and for nodes. A check on the list but not on the single object leaves a gap.
  • Field level. Check that the requester may read each field. A user may read their own email, and may not read the email of another user.

Do not treat a known object ID as permission. An attacker can read IDs from the schema, from other queries, or from guesses. This fault is the same as BOLA in AI-generated CRUD. The OWASP API list calls the field-level case Broken Object Property Level Authorization.

Here is a short example of a protected resolver:

const resolvers = {
  Query: {
    order: async (_, { id }, { user, orders }) => {
      if (!user) throw new GraphQLError("Unauthorized");
      const order = await orders.findById(id);
      if (!order || order.userId !== user.id) return null;
      return order;
    },
  },
  User: {
    email: (parent, _, { user }) =>
      user && user.id === parent.id ? parent.email : null,
  },
};

The query checks the login and the owner. The email field returns a value only for the owner. Replace orders.findById with your own data call.

Test each check with two accounts. Log in as user A and request the ID of an object of user B. The result must be null or an error.

Also remove fields that nobody needs. If the schema has User.passwordHash, delete it from the schema. A field that does not exist cannot leak. For general rules, read authorization patterns.

How do I limit the cost of a query?

GraphQL lets the client choose the query shape. A client can ask for deeply nested data, or ask for many objects in one request. This design makes abuse easy. One request can do the work of thousands.

The OWASP cheat sheet gives these rules:

  1. Add depth limiting. Reject queries that nest too deep. The cheat sheet says that depth and amount are unlimited by default, and you must set limits to prevent denial of service. A small custom rule or a library can do this.
  2. Limit the page size. Set a maximum for first, limit, and similar arguments. Do not let a client ask for 100000 rows.
  3. Consider cost analysis. OWASP suggests a maximum cost for each query. It also warns that you should be sure that you need it before you spend a lot of time on it. Start with depth and page limits.
  4. Add timeouts. OWASP says that timeouts at the application layer are usually more effective, because they can stop the work when the limit is reached.
  5. Limit the number of requests. Add a rate limit for each user and for each IP address. See API abuse protection.
  6. Control batching. Some servers accept an array of queries in one HTTP request. An attacker can use this to bypass a rate limit that counts HTTP requests. Limit the size of a batch, or turn batching off.

Set the limits to a value that your real app needs. Look at your normal queries and add a small margin.

How do I check the damage?

Introspection alone does not mean that data left your system. You must check if someone used the map to read data.

  1. Find the start date. Check the deploy history for the time when the endpoint went live with introspection on.
  2. Search the logs for introspection queries. Search request bodies for __schema and __type. If your logs do not keep bodies, check the request size and the route. A large response to a short request is a sign of an introspection call.
  3. Look for the tools. Look for user agents of known GraphQL tools and for requests with no browser user agent.
  4. Look for guessing. Many requests with errors about unknown fields show a person who guesses names.
  5. Look for access to sensitive fields. Search for queries for fields such as email, token, export, and admin. Check if the caller had the right to read them.
  6. Test each sensitive field yourself. Use the schema from the open endpoint as a checklist. For each query and mutation, call it with no login, with a user of the wrong role, and with the wrong object ID.
  7. Rotate secrets if a field exposed them. If a field returned a token or key, replace the value.

If the test finds a field that returns private data to the wrong caller, you have an access control problem. Fix it first. Then use the steps in the fix section for the data that left.

Note: A clean log does not prove that nobody used the schema. Anyone can run the introspection query once, and many servers do not log it.

How do I stop this problem in the future?

Make production safe by default. Then test it after each release.

  1. Set the flag from the environment. Use introspection: process.env.NODE_ENV !== "production" or the equivalent. Do not set it to true as a fixed value.
  2. Keep the development IDE on a development server. Do not deploy GraphiQL or Playground with the production build.
  3. Write a test. The test sends the introspection query to the staging URL and expects an error:
test("introspection is closed", async () => {
  const res = await fetch(STAGING_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query: "{ __schema { queryType { name } } }" }),
  });
  const body = await res.json();
  expect(body.data?.__schema).toBeUndefined();
});
  1. Test for suggestions. Add a test that sends a misspelled field and checks that the error has no Did you mean text.
  2. Check each new field. Add a review rule: each new field that returns user data needs a check on the user and on the object.
  3. Use an allow list of known queries if you can. If only your own app calls the API, some servers can accept only stored queries. Check the documentation of your server.
  4. Tell your coding tool the rule. Add this to the project rules: “Every resolver that returns user data must check the caller. Introspection stays off in production.”
  5. Scan the deployed app. A scan finds the endpoint that the browser really uses, and tests it from outside.

When should I get help?

Get help in these cases:

  • The schema shows fields with personal data, payment data, or credentials, and you cannot say who may read them. Ask a security engineer to review the resolvers.
  • Your logs show many introspection queries or many errors about unknown fields from the same source.
  • A test shows that a user can read the data of another user. Treat this as a data exposure and check the access logs.
  • You use a hosted GraphQL service and you cannot find the setting. Read the documentation of the service or ask its support.

After you finish, run the first curl test again. The response must be an error, not a schema.

Common questions

Is introspection a vulnerability by itself?
No. Introspection is a feature that shows the schema. It becomes a problem when it helps an attacker find fields that have no access check. The real fix is authorization on each field.
Does disabling introspection hide my schema?
Not fully. An attacker can still guess field names. Some servers return field suggestions in error messages, which leak names. Disable the suggestions too.
Will disabling introspection break my frontend?
Usually not. The app sends known queries and does not need introspection at run time. Code generation and IDE tools need it, so run them against a development server.
Does Apollo Server disable introspection in production?
Yes, by default, when NODE_ENV is set to production. If NODE_ENV is not set, introspection stays on. Set the variable and test the live endpoint.
Can I keep introspection on for my team only?
Yes. Allow it only for authenticated staff roles, or only from a private network. Do not allow it for anonymous callers.

Test your graphql endpoint

A VibeEval scan sends an introspection query to the GraphQL endpoints that your app exposes and reports the schema that comes back.

14-day free trial · No credit card · Cancel anytime

SCAN YOUR APP →