Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Query Whitelisting

Query Whitelisting (also known as Stored Operations or Persisted Operations) is a critical security feature that restricts which GraphQL queries can be executed. This is essential for public-facing GraphQL APIs and required for many compliance standards.

Why Query Whitelisting?

Security Benefits

  • Prevents Arbitrary Queries: Only pre-approved queries can be executed
  • Reduces Attack Surface: Prevents schema exploration and DoS attacks
  • Compliance: Required for PCI-DSS, HIPAA, SOC 2, and other standards
  • Performance: Known queries can be optimized and monitored
  • Audit Trail: Track exactly which queries are being used

Common Use Cases

  1. Public APIs: Prevent malicious actors from crafting expensive queries
  2. Mobile Applications: Apps typically have a fixed set of queries
  3. Third-Party Integrations: Control exactly what partners can query
  4. Compliance Requirements: Meet security standards for regulated industries

Configuration

Basic Setup

use grpc_graphql_gateway::{Gateway, QueryWhitelistConfig, WhitelistMode};
use std::collections::HashMap;

let mut allowed_queries = HashMap::new();
allowed_queries.insert(
    "getUserById".to_string(),
    "query getUserById($id: ID!) { user(id: $id) { id name } }".to_string()
);

let gateway = Gateway::builder()
    .with_query_whitelist(QueryWhitelistConfig {
        mode: WhitelistMode::Enforce,
        allowed_queries,
        allow_introspection: false,
    })
    .build()?;

Loading from JSON File

For production deployments, it’s recommended to load queries from a configuration file:

let config = QueryWhitelistConfig::from_json_file(
    "config/allowed_queries.json",
    WhitelistMode::Enforce
)?;

let gateway = Gateway::builder()
    .with_query_whitelist(config)
    .build()?;

Example JSON file (allowed_queries.json):

{
  "getUserById": "query getUserById($id: ID!) { user(id: $id) { id name email } }",
  "listProducts": "query { products { id name price } }",
  "createOrder": "mutation createOrder($input: OrderInput!) { createOrder(input: $input) { id } }"
}

Enforcement Modes

Enforce Mode (Production)

Rejects non-whitelisted queries with an error.

QueryWhitelistConfig {
    mode: WhitelistMode::Enforce,
    // ...
}

Error response:

{
  "errors": [{
    "message": "Query not in whitelist: Operation 'unknownQuery' (hash: 1234abcd...)",
    "extensions": {
      "code": "QUERY_NOT_WHITELISTED"
    }
  }]
}

Warn Mode (Staging)

Logs warnings but allows all queries. Useful for testing and identifying missing queries.

QueryWhitelistConfig {
    mode: WhitelistMode::Warn,
    // ...
}

Server log:

WARN grpc_graphql_gateway::query_whitelist: Query not in whitelist (allowed in Warn mode): Query hash: 0eb2d2f2e9111722

Disabled Mode (Development)

No whitelist checking. Same as not configuring a whitelist.

QueryWhitelistConfig::disabled()

Validation Methods

The whitelist supports two validation methods that can be used together:

1. Hash-Based Validation

Queries are validated by their SHA-256 hash. This is automatic and requires no client changes.

# This query's hash is calculated automatically
query { user(id: "123") { name } }

Query Normalization (v0.3.7+)

The gateway normalizes queries before hashing, so semantically equivalent queries produce the same hash. This means the following queries all match the same whitelist entry:

# Original
query { hello(name: "World") { message } }

# With extra whitespace
query   {   hello( name: "World" )   { message } }

# With comments stripped
query { # This is ignored
  hello(name: "World") { message }
}

# Multi-line format
query {
  hello(name: "World") {
    message
  }
}

Normalization rules:

  • Comments (# line comments and """ block comments) are removed
  • Whitespace is collapsed (multiple spaces β†’ single space)
  • Whitespace around punctuation ({, }, (, ), :, etc.) is removed
  • String literals are preserved exactly
  • Newlines are treated as whitespace

2. Operation ID Validation

Clients can explicitly reference queries by ID using GraphQL extensions:

Client request:

{
  "query": "query getUserById($id: ID!) { user(id: $id) { name } }",
  "variables": {"id": "123"},
  "extensions": {
    "operationId": "getUserById"
  }
}

The gateway validates the operationId against the whitelist.

Introspection Control

You can optionally allow introspection queries even in Enforce mode:

QueryWhitelistConfig {
    mode: WhitelistMode::Enforce,
    allowed_queries: queries,
    allow_introspection: true,  // Allow __schema and __type queries
}

This is useful for development and staging environments where developers need to explore the schema.

Runtime Management

The whitelist supports runtime modifications for dynamic use cases:

// Get whitelist reference
let whitelist = gateway.mux().query_whitelist().unwrap();

// Register new query at runtime
whitelist.register_query(
    "newQuery".to_string(),
    "query { newField }".to_string()
);

// Remove a query
whitelist.remove_query("oldQuery");

// Get statistics
let stats = whitelist.stats();
println!("Total allowed queries: {}", stats.total_queries);
println!("Mode: {:?}", stats.mode);

Best Practices

1. Use Enforce Mode in Production

Always use WhitelistMode::Enforce in production environments:

let mode = if std::env::var("ENV")? == "production" {
    WhitelistMode::Enforce
} else {
    WhitelistMode::Warn
};

2. Start with Warn Mode

When first implementing whitelisting:

  1. Deploy with Warn mode in staging
  2. Monitor logs to identify all queries
  3. Add missing queries to whitelist
  4. Switch to Enforce mode once complete

3. Version Control Your Whitelist

Store allowed_queries.json in version control alongside your application code.

4. Automated Query Extraction

For frontend applications, consider using tools to automatically extract queries from your codebase:

  • GraphQL Code Generator: Extract queries from React/Vue components
  • Apollo CLI: Generate persisted query manifests
  • Relay Compiler: Built-in persisted query support

5. CI/CD Integration

Validate the whitelist file in your CI pipeline:

# Validate JSON syntax
jq empty allowed_queries.json

# Run gateway with test queries
cargo test --test query_whitelist_validation

Working with APQ

Query Whitelisting and Automatic Persisted Queries (APQ) serve different purposes and work well together:

FeaturePurposeSecurity Level
APQBandwidth optimization (caches any query)Low
WhitelistSecurity (only allows pre-approved queries)High
BothBandwidth savings + SecurityMaximum

Example configuration with both:

Gateway::builder()
    // APQ for bandwidth optimization
    .with_persisted_queries(PersistedQueryConfig {
        cache_size: 1000,
        ttl: Some(Duration::from_secs(3600)),
    })
    // Whitelist for security
    .with_query_whitelist(QueryWhitelistConfig {
        mode: WhitelistMode::Enforce,
        allowed_queries: load_queries()?,
        allow_introspection: false,
    })
    .build()?

Migration Guide

Step 1: Inventory Queries

Use Warn mode to identify all queries currently in use:

.with_query_whitelist(QueryWhitelistConfig {
    mode: WhitelistMode::Warn,
    allowed_queries: HashMap::new(),
    allow_introspection: true,
})

Monitor logs for 1-2 weeks to capture all query variations.

Step 2: Build Whitelist

Extract unique query hashes from logs and build your whitelist file.

Step 3: Test in Staging

Deploy with the whitelist in Warn mode to staging:

# Monitor for any warnings
grep "Query not in whitelist" /var/log/gateway.log

Step 4: Production Deployment

Once confident, switch to Enforce mode:

.with_query_whitelist(QueryWhitelistConfig {
    mode: WhitelistMode::Enforce,
    allowed_queries: load_queries()?,
    allow_introspection: false,  // Disable in production
})

Troubleshooting

Query Rejected Despite Being in Whitelist

Problem: Query is in the whitelist but still gets rejected.

Solution: Ensure the query string exactly matches, including whitespace. Consider normalizing queries or using operation IDs.

Too Many Warnings in Warn Mode

Problem: Logs are flooded with warnings.

Solution: This is expected when first implementing. Collect all unique queries and add them to the whitelist.

Performance Impact

Problem: Concerned about validation overhead.

Solution: Hash calculation is fast (SHA-256). For 1000 RPS, overhead is <1ms. Consider caching if needed.

Example: Complete Production Setup

use grpc_graphql_gateway::{Gateway, QueryWhitelistConfig, WhitelistMode};
use std::path::Path;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Determine mode from environment
    let is_production = std::env::var("ENV")
        .map(|e| e == "production")
        .unwrap_or(false);
    
    // Load whitelist configuration
    let whitelist_config = if Path::new("config/allowed_queries.json").exists() {
        QueryWhitelistConfig::from_json_file(
            "config/allowed_queries.json",
            if is_production {
                WhitelistMode::Enforce
            } else {
                WhitelistMode::Warn
            }
        )?
    } else {
        QueryWhitelistConfig::disabled()
    };
    
    // Build gateway with production settings
    let gateway = Gateway::builder()
        .with_descriptor_set_bytes(DESCRIPTORS)
        .with_query_whitelist(whitelist_config)
        .with_response_cache(CacheConfig::default())
        .with_circuit_breaker(CircuitBreakerConfig::default())
        .with_compression(CompressionConfig::default())
        .build()?;
    
    gateway.serve("0.0.0.0:8888").await?;
    Ok(())
}

See Also