Skip to content

GraphQL Security Testing

“samuele edited this page Apr 20, 2026 · 1 revision

GraphQL Security Testing

The GraphQL security testing module is a dedicated scanner for GraphQL APIs. It discovers GraphQL endpoints across the attack surface, tests each for exposed introspection, extracts and hashes the schema, flags sensitive fields, and optionally runs the external graphql-cop Docker container for 12 additional misconfiguration checks.

It runs as GROUP 6 Phase A — in parallel with Nuclei — because both scanners consume the same inputs (BaseURL, Endpoint, Technology) and produce Vulnerability nodes, but have zero data dependency on each other. Disabled by default. Enable in the project settings under the GraphQL Security tab.


Why a dedicated GraphQL scanner

Generic web scanners miss GraphQL-specific weaknesses because they depend on the schema being visible and because GraphQL collapses the REST surface onto a single URL with POST bodies:

  • Introspection exposure — schema leak that maps every query, mutation, and subscription, plus sensitive field names
  • Schema-only DoS — alias overloading, batch query, directive overloading, circular introspection
  • IDE exposure — GraphiQL / GraphQL Playground / Apollo Studio accessible in production
  • CSRF via non-POST methods — queries/mutations accepted via GET or url-encoded POST
  • Field suggestion leakage — "Did you mean..." replies that bypass introspection-off defences
  • Trace mode — Apollo tracing extension exposes query timings and resolver names

The module combines a native introspection/sensitive-field test (PR #93, Phase 1) with the external graphql-cop scanner (Phase 2) that runs 12 targeted checks per endpoint.


Pipeline position

GROUP 5  → Resource enumeration (Katana, Hakrawler, jsluice, FFuf, GAU, Kiterunner, Arjun)
GROUP 5b → JS Reconnaissance
GROUP 6 Phase A  → Nuclei  ||  GraphQL Security Testing     ← parallel fan-out
GROUP 6 Phase B  → MITRE enrichment (consumes Nuclei CVEs)

Phase A is fanned out via ThreadPoolExecutor, and each scanner uses an _isolated wrapper that deep-copies the shared combined_result so the two threads never race on the same dict.


Endpoint discovery

Before testing, the scanner builds a candidate list of GraphQL endpoints from five sources and deduplicates them:

Source Extraction Logic
User-specified Comma-separated URLs from the GRAPHQL_ENDPOINTS setting
HTTP probe Endpoints with application/graphql in the Content-Type header
Resource enumeration Katana/Hakrawler/FFuf/GAU/Kiterunner endpoints whose path contains graphql, gql, or query (POST only). Also any endpoint with query, mutation, variables, or operationName as a parameter
JS Reconnaissance Findings typed as graphql or graphql_introspection in the JS Recon output
Pattern probing Common GraphQL paths appended to every discovered base URL

Pattern probing — primary patterns (tried on every base URL):

/graphql    /api/graphql    /v1/graphql    /v2/graphql

Secondary patterns (tried only on base URLs that already showed GraphQL evidence):

/query         /api/query
/gql           /api/gql
/graphiql      /api/graphiql
/playground    /api/playground

All discovered endpoints then pass through Rules of Engagement filtering (ROE_EXCLUDED_HOSTS with *.example.com wildcard support) before any probe fires. Skipped endpoints are counted in endpoints_skipped.


Introspection test

For each in-scope endpoint, the scanner runs a 3-step probe sequence:

  1. Reachability probePOST { __typename } with configurable auth headers. Non-200 responses short-circuit and mark the endpoint as non-GraphQL
  2. Simple introspectionPOST { __schema { queryType { name } mutationType { name } } }. If this succeeds with data.__schema present, introspection is enabled
  3. Deep introspection — Full introspection query with a configurable TypeRef recursion depth (1-20). If the response exceeds 10 MB, the scanner falls back to the simple result to avoid memory pressure

Why TypeRef depth matters: GraphQL type references are singly-linked chains (e.g. NON_NULL → LIST → NON_NULL → NAMED). A fixed 3-level fragment truncates info on deeply-wrapped types — GRAPHQL_DEPTH_LIMIT lets the scanner match the actual wrapping depth of the target schema. Default is 10, which covers nearly all real-world schemas; the hard ceiling is 20 to avoid server-side query-depth rejection.

Schema extraction output:

Per endpoint, the scanner computes and stores:

  • introspection_enabled / schema_extracted — booleans
  • queries_count, mutations_count, subscriptions_count
  • operations.{queries,mutations,subscriptions} — name lists
  • schema_hash — 16-char SHA256 prefix (for change detection across scans)
  • List of sensitive fields matching: password, secret, token, key, api, private, credential, auth, ssn, credit, card, payment, bank, account, pin, cvv, salary, medical

The introspection finding severity is dynamic:

Condition Severity
Introspection enabled (baseline) info
More than 0 mutations found info + mutation count appended
More than 20 mutations found medium
Sensitive fields detected medium

graphql-cop external scanner (Phase 2)

When GRAPHQL_COP_ENABLED is on, the scanner launches the dolevf/graphql-cop:1.14 Docker container per endpoint for 12 additional checks. The container runs as Docker-in-Docker — the recon container needs access to /var/run/docker.sock.

Command shape:

docker run --rm [--network host]  dolevf/graphql-cop:1.14 \
       -t <endpoint> -o json [-f] [-d] \
       [-H '{"Authorization":"Bearer ..."}'] [-T] [-x <proxy>]

Network mode: defaults to the Docker bridge network. When USE_TOR_FOR_RECON is on at the project level, the container is launched with --network host and passed -T to route all probes through Tor. A global HTTP_PROXY is forwarded via -x.

The 12 tests

Test Key (graphql-cop) Title Severity Mapped Vulnerability Type DoS?
field_suggestions Field Suggestions info graphql_field_suggestions_enabled
introspection Introspection high graphql_introspection_enabled
detect_graphiql GraphQL IDE medium graphql_ide_exposed
get_method_support GET Method Query Support medium graphql_get_method_allowed
alias_overloading Alias Overloading low graphql_alias_overloading
batch_query Array-based Query Batching low graphql_batch_query_allowed
trace_mode Trace Mode info graphql_tracing_enabled
directive_overloading Directive Overloading low graphql_directive_overloading
circular_query_introspection Introspection-based Circular Query low graphql_circular_introspection
get_based_mutation Mutation is allowed over GET high graphql_get_based_mutation
post_based_csrf POST based url-encoded query medium graphql_post_csrf
unhandled_error_detection Unhandled Errors Detection info graphql_unhandled_error

The introspection test is disabled by default — the native introspection test above already covers it without spawning a container. Everything else runs by default.

DoS note: Stealth mode forces the four DoS tests (alias, batch, directive, circular) off. Because the 1.14 image on DockerHub does not honor the -e exclusion flag (it was added in v1.15 main but not yet released), the scanner still executes those probes and then filters the findings Python-side. For true zero-traffic suppression, use the master Enable graphql-cop toggle.

Endpoint capability flags

graphql-cop always records five capability flags on the GraphQL Endpoint node — even when the test returned negative — so the graph captures server state explicitly (e.g. "GraphiQL exposed: false" is stored, not just absent):

Flag Source Test Meaning
graphql_graphiql_exposed detect_graphiql IDE page served at the endpoint
graphql_tracing_enabled trace_mode Apollo tracing extension returns timing data
graphql_get_allowed get_method_support Endpoint accepts GET queries
graphql_field_suggestions_enabled field_suggestions "Did you mean..." responses enabled
graphql_batching_enabled batch_query Server responds to array-batched requests

A graphql_cop_ran = true flag is also set after the container exits, regardless of individual test results.


Authentication

The scanner attaches auth headers to every request (native introspection + graphql-cop). Five auth types are supported, all configured via three settings:

Setting Values
GRAPHQL_AUTH_TYPE bearer / cookie / header / basic / apikey
GRAPHQL_AUTH_VALUE Token / cookie string / raw header value / user:pass pair (basic)
GRAPHQL_AUTH_HEADER Custom header name (used with header or apikey)

Auth type behavior:

Type Emitted Header
bearer Authorization: Bearer <value>
cookie Cookie: <value>
basic Authorization: Basic <base64(user:pass)>
header <GRAPHQL_AUTH_HEADER or X-Auth-Token>: <value>
apikey <GRAPHQL_AUTH_HEADER or X-API-Key>: <value>

All auth values are masked in logs — long values show xxxx...yyyy, short values show xx***, basic auth shows username:***.


Full parameter reference

Core

Parameter Default Clamp Description
GRAPHQL_SECURITY_ENABLED false Master toggle
GRAPHQL_INTROSPECTION_TEST true Run the native introspection probe
GRAPHQL_TIMEOUT 30 1-600 Request timeout in seconds
GRAPHQL_RATE_LIMIT 10 0-100 Global max requests per second (0 = unlimited)
GRAPHQL_CONCURRENCY 5 1-20 Parallel endpoint-testing threads
GRAPHQL_DEPTH_LIMIT 10 1-20 TypeRef fragment recursion depth
GRAPHQL_RETRY_COUNT 3 0-10 Retry attempts for 429/5xx and network errors
GRAPHQL_RETRY_BACKOFF 2.0 0-10 urllib3 backoff_factor (exponential)
GRAPHQL_VERIFY_SSL true Verify TLS certificates
GRAPHQL_ENDPOINTS "" Comma-separated custom endpoint URLs

Authentication

Parameter Default Description
GRAPHQL_AUTH_TYPE "" bearer / cookie / header / basic / apikey
GRAPHQL_AUTH_VALUE "" Token, cookie, value, or user:pass
GRAPHQL_AUTH_HEADER "" Custom header name for header / apikey modes

graphql-cop

Parameter Default Description
GRAPHQL_COP_ENABLED false Master toggle for the external scanner
GRAPHQL_COP_DOCKER_IMAGE dolevf/graphql-cop:1.14 Docker image (pinned to 1.14 for stable -e behaviour)
GRAPHQL_COP_TIMEOUT 120 Per-endpoint container timeout in seconds
GRAPHQL_COP_FORCE_SCAN false Pass -f to override the built-in GraphQL detector
GRAPHQL_COP_DEBUG false Pass -d to emit X-GraphQL-Cop-Test header per request

graphql-cop per-test toggles

Parameter Default DoS?
GRAPHQL_COP_TEST_FIELD_SUGGESTIONS true
GRAPHQL_COP_TEST_INTROSPECTION false
GRAPHQL_COP_TEST_GRAPHIQL true
GRAPHQL_COP_TEST_GET_METHOD true
GRAPHQL_COP_TEST_ALIAS_OVERLOADING true
GRAPHQL_COP_TEST_BATCH_QUERY true
GRAPHQL_COP_TEST_TRACE_MODE true
GRAPHQL_COP_TEST_DIRECTIVE_OVERLOADING true
GRAPHQL_COP_TEST_CIRCULAR_INTROSPECTION true
GRAPHQL_COP_TEST_GET_MUTATION true
GRAPHQL_COP_TEST_POST_CSRF true
GRAPHQL_COP_TEST_UNHANDLED_ERROR true

Stealth mode forces the four DoS tests off.


Output structure

Results are stored under combined_result.graphql_scan:

{
  "summary": {
    "endpoints_discovered": 12,
    "endpoints_tested": 9,
    "endpoints_skipped": 3,
    "introspection_enabled": 2,
    "vulnerabilities_found": 7,
    "by_severity": {
      "critical": 0, "high": 1, "medium": 3, "low": 2, "info": 1
    }
  },
  "discovered_endpoints": ["https://api.example.com/graphql", "..."],
  "endpoints": {
    "https://api.example.com/graphql": {
      "tested": true,
      "introspection_enabled": true,
      "schema_extracted": true,
      "queries_count": 34,
      "mutations_count": 12,
      "subscriptions_count": 2,
      "schema_hash": "9d2e4b1a...",
      "operations": {"queries": [...], "mutations": [...], "subscriptions": [...]},
      "graphql_cop_ran": true,
      "graphql_graphiql_exposed": true,
      "graphql_tracing_enabled": false,
      "graphql_get_allowed": true,
      "graphql_field_suggestions_enabled": true,
      "graphql_batching_enabled": false,
      "error": null
    }
  },
  "vulnerabilities": [ /* normalized finding dicts */ ]
}

Graph schema

Consumes: BaseURL, Endpoint, Domain, Technology

Produces: Vulnerability, CVE

Enriches: Endpoint — adds the capability flags, schema hash, operation counts, introspection booleans, and error state listed above.

Ingestion is gated by a schema contract in graph_db/mixins/graphql_mixin.pyKNOWN_VULN_KEYS and KNOWN_ENDPOINT_INFO_KEYS define every field the scanner can emit. If a scanner adds a new key without updating the mixin, an ingest-time warning fires so the change doesn't silently drop data.


Rules of Engagement

Discovered endpoints are filtered by ROE_EXCLUDED_HOSTS before testing. Wildcards are supported: *.staging.example.com matches both staging.example.com and any of its subdomains. Filtered endpoints are counted under summary.endpoints_skipped for visibility.


Stealth mode

When stealth mode is enabled on the project:

Setting Stealth Override
GRAPHQL_RATE_LIMIT 2
GRAPHQL_CONCURRENCY 1 (sequential)
GRAPHQL_TIMEOUT 60
GRAPHQL_COP_TEST_ALIAS_OVERLOADING false
GRAPHQL_COP_TEST_BATCH_QUERY false
GRAPHQL_COP_TEST_DIRECTIVE_OVERLOADING false
GRAPHQL_COP_TEST_CIRCULAR_INTROSPECTION false

Other GraphQL settings (GRAPHQL_SECURITY_ENABLED, GRAPHQL_INTROSPECTION_TEST) stay unchanged — passive introspection probing is still valuable in stealth mode.


Partial recon

GraphQL scanning is available as a Partial Recon tool. The modal accepts custom URLs that must be within the project scope — they are injected via the GRAPHQL_ENDPOINTS setting and expanded by the same discovery pipeline used in the full recon run.

  • GRAPHQL_SECURITY_ENABLED is force-set to true for the partial run (overrides the project toggle)
  • settings_overrides from the modal bypass the stored project settings for that run
  • Graph targets are pulled from existing BaseURL, Endpoint, and JS Recon findings — toggleable via the "Include existing graph targets" checkbox

See Recon Pipeline Workflow — Partial Recon for the modal layout.


Related pages

Clone this wiki locally