GraphQL Introspection Enabled in Production Exposing Schema Information

Medium Risk API Security
graphqlintrospectioninformation-disclosureschema-exposurereconnaissanceapi-discovery

What it is

A medium-severity vulnerability where GraphQL APIs have introspection queries enabled in production environments, allowing attackers to discover the complete API schema, including sensitive fields, internal operations, and system architecture. This information disclosure can be used to craft targeted attacks, identify sensitive data fields, and understand the application's internal structure for further exploitation.

// VULNERABLE: GraphQL server with exposed introspection const { ApolloServer, gql } = require('apollo-server-express'); const express = require('express'); // Sensitive schema with internal operations const typeDefs = gql` type User { id: ID! email: String! passwordHash: String! # Sensitive field adminLevel: Int! # Internal authorization field apiKeys: [String!]! # Sensitive API keys internalNotes: String # Confidential data } type Query { users: [User!]! # Internal admin queries exposed getUserSecrets(userId: ID!): UserSecrets systemMetrics: SystemStatus databaseHealth: DbStatus } type Mutation { # Dangerous admin operations exposed promoteToAdmin(userId: ID!): User deleteUserCompletely(userId: ID!): Boolean backdoorAccess(secret: String!): AuthToken resetAllPasswords: Boolean } # Internal types that shouldn't be discoverable type UserSecrets { encryptionKey: String! backupCodes: [String!]! recoveryEmail: String! } `; const resolvers = { Query: { users: () => User.find(), getUserSecrets: (_, { userId }) => UserSecrets.findByUserId(userId), systemMetrics: () => getSystemMetrics(), }, Mutation: { promoteToAdmin: (_, { userId }) => promoteUser(userId), backdoorAccess: (_, { secret }) => createBackdoorSession(secret), } }; // PROBLEM: No security configuration const server = new ApolloServer({ typeDefs, resolvers, // introspection: true (default) // playground: true (default) }); const app = express(); server.applyMiddleware({ app }); app.listen(4000, () => { console.log('GraphQL server running on http://localhost:4000/graphql'); });
// SECURE: GraphQL server with comprehensive security const { ApolloServer, gql } = require('apollo-server-express'); const { NoIntrospection, specifiedRules } = require('graphql'); const depthLimit = require('graphql-depth-limit'); const costAnalysis = require('graphql-query-complexity').costAnalysisValidator; const express = require('express'); const rateLimit = require('express-rate-limit'); const helmet = require('helmet'); const isProduction = process.env.NODE_ENV === 'production'; // Sanitized schema - sensitive fields removed or protected const typeDefs = gql` type User { id: ID! email: String! # passwordHash removed from schema role: Role! # Enum instead of internal levels createdAt: DateTime! # Sensitive fields removed } enum Role { USER ADMIN } type Query { # Public queries only me: User # Current user only publicUsers: [User!]! # Limited public data # Internal admin queries removed from public schema } type Mutation { # Safe operations only updateProfile(input: ProfileInput!): User changePassword(currentPassword: String!, newPassword: String!): Boolean # Dangerous admin operations removed } input ProfileInput { firstName: String lastName: String bio: String } `; // Rate limiting for GraphQL const graphqlLimiter = rateLimit({ windowMs: 60 * 1000, // 1 minute max: isProduction ? 100 : 1000, // Stricter in production message: { error: 'Too many GraphQL requests', retryAfter: 60 }, keyGenerator: (req) => { // Rate limit by user ID if authenticated, otherwise by IP return req.user?.id || req.ip; } }); const resolvers = { Query: { me: (_, __, { user }) => { if (!user) throw new Error('Authentication required'); return User.findById(user.id).select('-passwordHash -internalNotes'); }, publicUsers: () => { return User.find({ isPublic: true }) .select('id email role createdAt') .limit(50); } }, Mutation: { updateProfile: async (_, { input }, { user }) => { if (!user) throw new Error('Authentication required'); return User.findByIdAndUpdate( user.id, { $set: input }, { new: true } ).select('-passwordHash'); } } }; // Security configuration const server = new ApolloServer({ typeDefs, resolvers, // SECURITY: Disable introspection and playground in production introspection: !isProduction, playground: !isProduction, // Query validation and complexity analysis validationRules: [ ...specifiedRules, ...(isProduction ? [NoIntrospection] : []), depthLimit(10), costAnalysis({ maximumCost: 1000, defaultCost: 1, createError: (max, actual) => { throw new Error(`Query cost ${actual} exceeds maximum ${max}`); } }) ], // Context with authentication context: ({ req }) => { const token = req.headers.authorization?.replace('Bearer ', ''); const user = token ? verifyToken(token) : null; return { user, isProduction, userAgent: req.headers['user-agent'], ip: req.ip }; }, // Error formatting - hide sensitive information formatError: (error) => { // Log full error server-side console.error('GraphQL Error:', { message: error.message, locations: error.locations, path: error.path, source: error.source?.body }); if (isProduction) { // Hide internal errors in production if (error.message.includes('introspection')) { return new Error('Query not allowed'); } if (error.originalError?.code === 'INTERNAL_ERROR') { return new Error('An internal error occurred'); } } return error; }, // Monitoring and security plugins plugins: [ { requestDidStart() { return { didResolveOperation(requestContext) { const query = requestContext.request.query; // Monitor for introspection attempts if (query?.includes('__schema') || query?.includes('__type')) { console.warn('Introspection attempt detected:', { query: query.substring(0, 200), userAgent: requestContext.request.http?.headers?.get('user-agent'), ip: requestContext.request.ip, timestamp: new Date().toISOString() }); if (isProduction) { throw new Error('Introspection is disabled'); } } // Monitor for complex queries if (query?.length > 2000) { console.warn('Large query detected:', { size: query.length, userAgent: requestContext.request.http?.headers?.get('user-agent'), ip: requestContext.request.ip }); } } }; } } ] }); const app = express(); // Additional security middleware app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https:"], }, }, })); // Apply rate limiting app.use('/graphql', graphqlLimiter); // Apply GraphQL middleware server.applyMiddleware({ app, path: '/graphql', cors: { origin: isProduction ? process.env.ALLOWED_ORIGINS?.split(',') : true, credentials: true } }); app.listen(4000, () => { console.log(`GraphQL server running on http://localhost:4000${server.graphqlPath}`); console.log(`Introspection: ${!isProduction ? 'enabled' : 'disabled'}`); console.log(`Playground: ${!isProduction ? 'enabled' : 'disabled'}`); });

💡 Why This Fix Works

The secure implementation comprehensively addresses GraphQL introspection vulnerabilities by disabling introspection and playground in production, implementing query complexity analysis, adding rate limiting, and sanitizing the schema to remove sensitive fields. It includes proper error handling that doesn't leak information, authentication context, and monitoring for suspicious activities.

Why it happens

Apollo Server enables introspection by default, and developers often forget to disable it in production. This exposes the entire GraphQL schema, including sensitive fields, internal mutations, and administrative operations that should remain hidden from external users.

Root causes

Default Apollo Server Configuration in Production

Apollo Server enables introspection by default, and developers often forget to disable it in production. This exposes the entire GraphQL schema, including sensitive fields, internal mutations, and administrative operations that should remain hidden from external users.

Preview example – JAVASCRIPT
// VULNERABLE: Apollo Server with default introspection settings
const { ApolloServer, gql } = require('apollo-server-express');

const typeDefs = gql`
  type User {
    id: ID!
    email: String!
    passwordHash: String!  # Sensitive field exposed
    adminLevel: Int!       # Internal field exposed
    internalNotes: String  # Confidential data exposed
  }
  
  type Query {
    users: [User!]!
    # Internal admin operations exposed
    getUserSecrets(userId: ID!): UserSecrets
    systemHealth: SystemStatus
  }
  
  type Mutation {
    # Dangerous administrative mutations exposed
    deleteAllUsers: Boolean
    backdoorLogin(secret: String!): AuthToken
    updateSystemConfig(config: String!): Boolean
  }
`;

// PROBLEM: No introspection configuration
const server = new ApolloServer({
  typeDefs,
  resolvers,
  // introspection: true (default in development)
  // Missing: introspection: false for production
});

// Attacker can discover sensitive schema:
// query IntrospectionQuery {
//   __schema {
//     types {
//       name
//       fields {
//         name
//         type { name }
//       }
//     }
//   }
// }

Express GraphQL with Enabled GraphiQL Interface

Using express-graphql with GraphiQL enabled in production not only enables introspection but also provides an interactive interface for attackers to explore and test the API. This makes reconnaissance and attack development significantly easier.

Preview example – JAVASCRIPT
// VULNERABLE: express-graphql with GraphiQL enabled
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');

const schema = buildSchema(`
  type User {
    id: ID!
    username: String!
    email: String!
    role: String!
    apiKey: String!        # Sensitive API keys exposed
    billingInfo: Billing!  # Financial data structure exposed
  }
  
  type Billing {
    creditCard: String!    # Credit card info structure
    ssn: String!          # SSN field exposed in schema
    bankAccount: String!
  }
  
  type Query {
    user(id: ID!): User
    adminUsers: [User!]!   # Admin-only query exposed
    internalMetrics: Metrics
  }
`);

const app = express();

// DANGEROUS: GraphiQL enabled in production
app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: resolvers,
  graphiql: true,  // Should be false in production
  // introspection is enabled by default
}));

// Consequences:
// 1. Full schema discovery via introspection
// 2. Interactive testing interface at /graphql
// 3. Exposure of sensitive field names and types
// 4. Understanding of data relationships

Spring Boot GraphQL with Default Development Settings

Spring Boot GraphQL applications using default configurations often have introspection enabled. When deployed to production without proper configuration, the schema becomes discoverable, revealing internal business logic and sensitive data structures.

Preview example – JAVA
// VULNERABLE: Spring Boot GraphQL with default settings
@Configuration
@EnableWebMvc
public class GraphQLConfig {
    
    @Bean
    public GraphQL graphQL() {
        TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(
            loadSchemaFile("schema.graphqls")
        );
        
        RuntimeWiring runtimeWiring = buildWiring();
        
        GraphQLSchema graphQLSchema = new SchemaGenerator()
            .makeExecutableSchema(typeRegistry, runtimeWiring);
        
        return GraphQL.newGraphQL(graphQLSchema)
            // MISSING: .fieldVisibility(NoIntrospectionGraphqlFieldVisibility.NO_INTROSPECTION_FIELD_VISIBILITY)
            .build();
    }
    
    @Bean
    public GraphQLHttpHandler graphQLHandler() {
        return GraphQLHttpHandler.builder(graphQL())
            // MISSING: .introspectionEnabled(false)
            .build();
    }
}

# schema.graphqls - Exposed sensitive schema
type User {
  id: ID!
  username: String!
  passwordHash: String!     # Password hashes exposed in schema
  roles: [Role!]!
  personalData: PersonalInfo!
}

type PersonalInfo {
  ssn: String!             # Social Security Number structure
  creditScore: Int!
  medicalRecords: [Medical!]!
}

type Mutation {
  # Administrative operations exposed
  promoteUserToAdmin(userId: ID!): User!
  deleteUserData(userId: ID!): Boolean!
  backdoorAccess(token: String!): AdminSession!
}

Hasura GraphQL Engine with Development Mode

Hasura GraphQL Engine in development mode enables introspection and the GraphiQL interface by default. When deployed to production without changing these settings, it exposes the entire database schema structure and available operations to potential attackers.

Preview example – YAML
# VULNERABLE: Hasura production deployment with dev settings
# docker-compose.yml
version: '3.6'
services:
  hasura:
    image: hasura/graphql-engine:latest
    ports:
      - "8080:8080"
    environment:
      HASURA_GRAPHQL_DATABASE_URL: postgres://username:password@postgres:5432/mydb
      # PROBLEM: Development settings in production
      HASURA_GRAPHQL_ENABLE_CONSOLE: "true"     # Console enabled
      HASURA_GRAPHQL_DEV_MODE: "true"           # Dev mode enabled
      # MISSING: HASURA_GRAPHQL_ENABLE_INTROSPECTION: "false"
      
      # Sensitive admin secret exposed or weak
      HASURA_GRAPHQL_ADMIN_SECRET: "admin123"   # Weak secret
      
      # Metadata and migrations exposed
      HASURA_GRAPHQL_ENABLE_TELEMETRY: "true"
    
# Results in:
# 1. Full database schema introspection
# 2. All table relationships exposed
# 3. Permission structure visible
# 4. Administrative console accessible

# Example introspection reveals database structure:
# query {
#   __schema {
#     types {
#       name
#       fields {
#         name
#         type {
#           name
#           ofType { name }
#         }
#       }
#     }
#   }
# }

Fixes

1

Disable Introspection in Apollo Server Production

Explicitly disable introspection and GraphiQL in production Apollo Server deployments. Use environment-based configuration to ensure different settings for development and production environments.

View implementation – JAVASCRIPT
const { ApolloServer, gql } = require('apollo-server-express');
const { NoIntrospection } = require('graphql');

// Environment-based configuration
const isProduction = process.env.NODE_ENV === 'production';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  // Disable introspection in production
  introspection: !isProduction,
  
  // Disable GraphQL playground in production
  playground: !isProduction,
  
  // Add validation rules to prevent introspection
  validationRules: isProduction ? [NoIntrospection] : [],
  
  // Additional security settings
  context: ({ req }) => {
    return {
      // Pass user context for authorization
      user: getAuthenticatedUser(req),
      isProduction
    };
  },
  
  // Format errors without exposing internal details
  formatError: (error) => {
    if (isProduction) {
      // Log full error details server-side
      console.error('GraphQL Error:', error);
      
      // Return generic error to client
      if (error.message.includes('introspection')) {
        return new Error('Introspection is disabled');
      }
      
      return new Error('An error occurred processing your request');
    }
    
    return error;
  },
  
  // Plugin to monitor introspection attempts
  plugins: [
    {
      requestDidStart() {
        return {
          didResolveOperation(requestContext) {
            const query = requestContext.request.query;
            
            // Detect and log introspection attempts
            if (query && query.includes('__schema')) {
              console.warn('Introspection query attempted:', {
                query,
                userAgent: requestContext.request.http?.headers?.get('user-agent'),
                ip: requestContext.request.ip
              });
              
              if (isProduction) {
                throw new Error('Introspection is not allowed');
              }
            }
          }
        };
      }
    }
  ]
});
2

Secure Express GraphQL Configuration

Configure express-graphql to disable GraphiQL and introspection in production while implementing proper error handling and query validation to prevent information disclosure.

View implementation – JAVASCRIPT
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { NoIntrospection, specifiedRules } = require('graphql');
const depthLimit = require('graphql-depth-limit');

const app = express();
const isProduction = process.env.NODE_ENV === 'production';

// Custom error formatter
function formatError(error) {
  if (isProduction) {
    // Log full error details
    console.error('GraphQL Error:', error);
    
    // Return sanitized error
    return {
      message: 'An error occurred',
      code: 'INTERNAL_ERROR'
    };
  }
  
  return error;
}

// Custom validation rules for production
const productionValidationRules = [
  ...specifiedRules,
  NoIntrospection,        // Prevent introspection
  depthLimit(10),         // Limit query depth
];

app.use('/graphql', graphqlHTTP((req, res) => ({
  schema: schema,
  rootValue: resolvers,
  
  // Disable GraphiQL in production
  graphiql: !isProduction,
  
  // Disable introspection in production
  introspection: !isProduction,
  
  // Apply validation rules
  validationRules: isProduction ? productionValidationRules : specifiedRules,
  
  // Custom error formatting
  formatError: formatError,
  
  // Context with security information
  context: {
    user: getAuthenticatedUser(req),
    isProduction,
    userAgent: req.get('User-Agent'),
    ip: req.ip
  },
  
  // Extensions disabled in production
  extensions: !isProduction ? ({ document, variables, operationName, result }) => {
    return {
      runTime: Date.now() - startTime
    };
  } : undefined
})));

// Additional middleware to block introspection attempts
app.use('/graphql', (req, res, next) => {
  if (isProduction && req.body && req.body.query) {
    const query = req.body.query.toLowerCase();
    
    if (query.includes('__schema') || query.includes('__type')) {
      console.warn('Introspection attempt blocked:', {
        query: req.body.query,
        ip: req.ip,
        userAgent: req.get('User-Agent')
      });
      
      return res.status(400).json({
        errors: [{
          message: 'Introspection is not allowed',
          extensions: { code: 'INTROSPECTION_DISABLED' }
        }]
      });
    }
  }
  
  next();
});
3

Spring Boot GraphQL Security Configuration

Implement comprehensive GraphQL security in Spring Boot including introspection disabling, field visibility controls, and query validation to prevent schema exposure.

View implementation – JAVA
@Configuration
@EnableWebMvc
public class GraphQLSecurityConfig {
    
    @Value("${app.environment:production}")
    private String environment;
    
    @Bean
    public GraphQL graphQL() {
        TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(
            loadSchemaFile("schema.graphqls")
        );
        
        RuntimeWiring runtimeWiring = buildSecureWiring();
        
        GraphQLSchema.Builder schemaBuilder = new SchemaGenerator()
            .makeExecutableSchema(typeRegistry, runtimeWiring)
            .transform(builder -> {
                // Disable introspection in production
                if ("production".equals(environment)) {
                    builder.fieldVisibility(
                        NoIntrospectionGraphqlFieldVisibility.NO_INTROSPECTION_FIELD_VISIBILITY
                    );
                }
                return builder;
            });
        
        return GraphQL.newGraphQL(schemaBuilder.build())
            .queryExecutionStrategy(new AsyncExecutionStrategy(new SecurityDataFetcherExceptionHandler()))
            .build();
    }
    
    @Bean
    public GraphQLHttpHandler graphQLHandler() {
        return GraphQLHttpHandler.builder(graphQL())
            .introspectionEnabled(!"production".equals(environment))
            .schemaLocationPattern("**/*.graphqls")
            .build();
    }
    
    @Bean
    public RouterFunction<ServerResponse> graphQLRouterFunction() {
        return RouterFunctions
            .route(RequestPredicates.POST("/graphql"), this::handleGraphQLRequest)
            .filter(this::securityFilter);
    }
    
    private Mono<ServerResponse> handleGraphQLRequest(ServerRequest request) {
        return request.bodyToMono(String.class)
            .flatMap(body -> {
                // Check for introspection attempts
                if ("production".equals(environment) && 
                    (body.contains("__schema") || body.contains("__type"))) {
                    
                    // Log introspection attempt
                    log.warn("Introspection attempt blocked: {}", 
                        Map.of(
                            "query", body,
                            "userAgent", request.headers().firstHeader("User-Agent"),
                            "ip", request.remoteAddress().map(InetSocketAddress::getHostString).orElse("unknown")
                        )
                    );
                    
                    return ServerResponse.badRequest()
                        .bodyValue(Map.of("error", "Introspection is not allowed"));
                }
                
                return graphQLHandler().handleRequest(request);
            });
    }
    
    private Mono<ServerResponse> securityFilter(ServerRequest request, HandlerFunction<ServerResponse> next) {
        // Additional security checks
        return next.handle(request);
    }
    
    // Custom exception handler to prevent information disclosure
    private static class SecurityDataFetcherExceptionHandler implements DataFetcherExceptionHandler {
        @Override
        public DataFetcherExceptionHandlerResult onException(DataFetcherExceptionHandlerParameters handlerParameters) {
            Throwable exception = handlerParameters.getException();
            SourceLocation sourceLocation = handlerParameters.getSourceLocation();
            ResultPath path = handlerParameters.getPath();
            
            // Log full exception details
            log.error("GraphQL execution error at path {}: {}", path, exception.getMessage(), exception);
            
            // Return generic error in production
            if ("production".equals(environment)) {
                GraphQLError error = GraphQLError.newError()
                    .message("An error occurred processing your request")
                    .location(sourceLocation)
                    .path(path)
                    .build();
                
                return DataFetcherExceptionHandlerResult.newResult()
                    .error(error)
                    .build();
            }
            
            // Return detailed error in development
            GraphQLError error = GraphQLError.newError()
                .message(exception.getMessage())
                .location(sourceLocation)
                .path(path)
                .build();
            
            return DataFetcherExceptionHandlerResult.newResult()
                .error(error)
                .build();
        }
    }
}
4

Hasura Production Security Configuration

Configure Hasura GraphQL Engine for production with disabled introspection, console access restrictions, and proper authentication mechanisms to prevent schema exposure.

View implementation – YAML
# Production Hasura configuration
# docker-compose.prod.yml
version: '3.6'
services:
  hasura:
    image: hasura/graphql-engine:latest
    ports:
      - "8080:8080"
    environment:
      # Database connection
      HASURA_GRAPHQL_DATABASE_URL: postgres://username:password@postgres:5432/mydb
      
      # SECURITY: Disable development features
      HASURA_GRAPHQL_ENABLE_CONSOLE: "false"           # Disable web console
      HASURA_GRAPHQL_DEV_MODE: "false"                 # Disable dev mode
      HASURA_GRAPHQL_ENABLE_INTROSPECTION: "false"     # Disable introspection
      
      # Strong admin secret
      HASURA_GRAPHQL_ADMIN_SECRET: "${HASURA_ADMIN_SECRET}"  # Use env var
      
      # CORS configuration
      HASURA_GRAPHQL_CORS_DOMAIN: "https://yourdomain.com"
      
      # Rate limiting
      HASURA_GRAPHQL_RATE_LIMIT_GLOBAL: "1000"
      HASURA_GRAPHQL_RATE_LIMIT_PER_ROLE: "100"
      
      # Disable telemetry
      HASURA_GRAPHQL_ENABLE_TELEMETRY: "false"
      
      # Logging configuration
      HASURA_GRAPHQL_LOG_LEVEL: "warn"
      
      # Additional security settings
      HASURA_GRAPHQL_UNAUTHORIZED_ROLE: "anonymous"
      HASURA_GRAPHQL_JWT_SECRET: '{
        "type":"HS256",
        "key":"${JWT_SECRET}",
        "claims_format": "json"
      }'
      
    # Security labels for monitoring
    labels:
      - "security.introspection=disabled"
      - "security.console=disabled"
      
    # Health check
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:8080/healthz || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Reverse proxy with additional security
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - hasura
      
---
# nginx.conf - Additional security layer
events {
    worker_connections 1024;
}

http {
    # Rate limiting
    limit_req_zone $binary_remote_addr zone=graphql:10m rate=10r/s;
    
    server {
        listen 443 ssl;
        server_name yourdomain.com;
        
        ssl_certificate /etc/nginx/ssl/cert.pem;
        ssl_certificate_key /etc/nginx/ssl/key.pem;
        
        # Block introspection at proxy level
        location /v1/graphql {
            # Block introspection queries
            if ($request_body ~ "__schema|__type") {
                return 400 "Introspection queries not allowed";
            }
            
            # Rate limiting
            limit_req zone=graphql burst=20 nodelay;
            
            # Additional headers
            add_header X-Content-Type-Options nosniff;
            add_header X-Frame-Options DENY;
            add_header X-XSS-Protection "1; mode=block";
            
            proxy_pass http://hasura:8080;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
        
        # Block admin console completely
        location /console {
            return 404;
        }
    }
}

Detect This Vulnerability in Your Code

Sourcery automatically identifies graphql introspection enabled in production exposing schema information and many other security issues in your codebase.