Skip to content

GraphQL Security Testing

Samuele Giampieri edited this page Nov 7, 2026 · 6 revisions

📖 Canonical version: read this page on the official docs site — https://www.redamon.org/docs/graphql-security-testing. The GitHub wiki is a mirror.

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 six sources and deduplicates them. Full recon and partial recon read the same data:

Source Extraction Logic
User-specified Comma-separated URLs from the GRAPHQL_ENDPOINTS setting, kept as given
HTTP probe Live URLs whose response Content-Type names GraphQL
Resource enumeration Endpoints the crawlers found (Katana, Hakrawler, FFuf, GAU, Kiterunner, any method) whose path contains graphql, gql or query, and any endpoint that takes query, mutation, variables or operationName as a parameter (that endpoint, not its site root)
JS Reconnaissance GraphQL endpoints JS Recon found in the bundles; a relative path is joined to the site that served the script
URLScan In-scope pages URLScan saw whose path contains graphql or gql
Pattern probing Common GraphQL paths appended to every live web root the HTTP probe found (never to a URLScan page)

Only candidates on a host in this scan's scope are kept: a live host from the HTTP probe, or a host the scan's target list admits. An absolute URL a script names on someone else's domain is never probed.

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 (the project's ROE_EXCLUDED_HOSTS, in full and partial recon alike) 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 probe — POST { __typename } with configurable auth headers. Non-200 responses short-circuit and mark the endpoint as non-GraphQL
  2. Simple introspection — POST { __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 ..."}']

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) 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 Fixed image, not a project setting (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.py — KNOWN_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, with the same matching the rest of the pipeline uses: a domain excludes itself and its subdomains, *.staging.example.com excludes the subdomains of staging.example.com only, an IP or a CIDR excludes those addresses, and case and port are ignored. Filtered endpoints are counted under summary.endpoints_skipped for visibility.

More sources mean more candidates, so a scan sends more introspection requests than before; they all run under GRAPHQL_RATE_LIMIT, which the engagement's rate ceiling caps, and stealth mode keeps GraphQL at 2 requests per second.


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

Home


Getting Started

Core Workflow

Scanning & OSINT

AI & Automation

Plugins

HackLab

Analysis & Reporting

Contributing

Reference & Help

Clone this wiki locally