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:

SourceExtraction Logic
User-specifiedComma-separated URLs from the GRAPHQL_ENDPOINTS setting
HTTP probeEndpoints with application/graphql in the Content-Type header
Resource enumerationKatana/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 ReconnaissanceFindings typed as graphql or graphql_introspection in the JS Recon output
Pattern probingCommon 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:

ConditionSeverity
Introspection enabled (baseline)info
More than 0 mutations foundinfo + mutation count appended
More than 20 mutations foundmedium
Sensitive fields detectedmedium

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 ..."}']

Network mode: graphql-cop always runs with --network host so it can reach loopback and local-lab targets (e.g. 127.0.0.1, RFC1918 ranges).

The 12 tests

Test Key (graphql-cop)TitleSeverityMapped Vulnerability TypeDoS?
field_suggestionsField Suggestionsinfographql_field_suggestions_enabled
introspectionIntrospectionhighgraphql_introspection_enabled
detect_graphiqlGraphQL IDEmediumgraphql_ide_exposed
get_method_supportGET Method Query Supportmediumgraphql_get_method_allowed
alias_overloadingAlias Overloadinglowgraphql_alias_overloading
batch_queryArray-based Query Batchinglowgraphql_batch_query_allowed
trace_modeTrace Modeinfographql_tracing_enabled
directive_overloadingDirective Overloadinglowgraphql_directive_overloading
circular_query_introspectionIntrospection-based Circular Querylowgraphql_circular_introspection
get_based_mutationMutation is allowed over GEThighgraphql_get_based_mutation
post_based_csrfPOST based url-encoded querymediumgraphql_post_csrf
unhandled_error_detectionUnhandled Errors Detectioninfographql_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):

FlagSource TestMeaning
graphql_graphiql_exposeddetect_graphiqlIDE page served at the endpoint
graphql_tracing_enabledtrace_modeApollo tracing extension returns timing data
graphql_get_allowedget_method_supportEndpoint accepts GET queries
graphql_field_suggestions_enabledfield_suggestions"Did you mean..." responses enabled
graphql_batching_enabledbatch_queryServer 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:

SettingValues
GRAPHQL_AUTH_TYPEbearer / cookie / header / basic / apikey
GRAPHQL_AUTH_VALUEToken / cookie string / raw header value / user:pass pair (basic)
GRAPHQL_AUTH_HEADERCustom header name (used with header or apikey)

Auth type behavior:

TypeEmitted Header
bearerAuthorization: Bearer <value>
cookieCookie: <value>
basicAuthorization: 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

ParameterDefaultClampDescription
GRAPHQL_SECURITY_ENABLEDfalseMaster toggle
GRAPHQL_INTROSPECTION_TESTtrueRun the native introspection probe
GRAPHQL_TIMEOUT301-600Request timeout in seconds
GRAPHQL_RATE_LIMIT100-100Global max requests per second (0 = unlimited)
GRAPHQL_CONCURRENCY51-20Parallel endpoint-testing threads
GRAPHQL_DEPTH_LIMIT101-20TypeRef fragment recursion depth
GRAPHQL_RETRY_COUNT30-10Retry attempts for 429/5xx and network errors
GRAPHQL_RETRY_BACKOFF2.00-10urllib3 backoff_factor (exponential)
GRAPHQL_VERIFY_SSLtrueVerify TLS certificates
GRAPHQL_ENDPOINTS""Comma-separated custom endpoint URLs

Authentication

ParameterDefaultDescription
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

ParameterDefaultDescription
GRAPHQL_COP_ENABLEDfalseMaster toggle for the external scanner
GRAPHQL_COP_DOCKER_IMAGEdolevf/graphql-cop:1.14Docker image (pinned to 1.14 for stable -e behaviour)
GRAPHQL_COP_TIMEOUT120Per-endpoint container timeout in seconds
GRAPHQL_COP_FORCE_SCANfalsePass -f to override the built-in GraphQL detector
GRAPHQL_COP_DEBUGfalsePass -d to emit X-GraphQL-Cop-Test header per request

graphql-cop per-test toggles

ParameterDefaultDoS?
GRAPHQL_COP_TEST_FIELD_SUGGESTIONStrue
GRAPHQL_COP_TEST_INTROSPECTIONfalse
GRAPHQL_COP_TEST_GRAPHIQLtrue
GRAPHQL_COP_TEST_GET_METHODtrue
GRAPHQL_COP_TEST_ALIAS_OVERLOADINGtrue
GRAPHQL_COP_TEST_BATCH_QUERYtrue
GRAPHQL_COP_TEST_TRACE_MODEtrue
GRAPHQL_COP_TEST_DIRECTIVE_OVERLOADINGtrue
GRAPHQL_COP_TEST_CIRCULAR_INTROSPECTIONtrue
GRAPHQL_COP_TEST_GET_MUTATIONtrue
GRAPHQL_COP_TEST_POST_CSRFtrue
GRAPHQL_COP_TEST_UNHANDLED_ERRORtrue

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:

SettingStealth Override
GRAPHQL_RATE_LIMIT2
GRAPHQL_CONCURRENCY1 (sequential)
GRAPHQL_TIMEOUT60
GRAPHQL_COP_TEST_ALIAS_OVERLOADINGfalse
GRAPHQL_COP_TEST_BATCH_QUERYfalse
GRAPHQL_COP_TEST_DIRECTIVE_OVERLOADINGfalse
GRAPHQL_COP_TEST_CIRCULAR_INTROSPECTIONfalse

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.