Skip to content

Repository files navigation

GradientQL

License: MIT Python Open in Colab

GradientQL is an autonomous vulnerability scanner for GraphQL APIs. Give it an endpoint and a model API key, and a single language model runs the assessment without an operator: it reads the schema, signs up for an account when a flow needs one, works the attack surface, and writes a report. Each step is a single model call that returns one action from a fixed set, so a scan follows the model's own read of the target rather than a fixed payload list.

Warning

Only run GradientQL against a target you own or are authorized to test. It sends live attacks to the endpoint, runs unattended, and has no consent gate. Staying in scope is your responsibility.

How it works

flowchart TB
    ep(["GraphQL endpoint"]) --> intro["Introspect<br/>(or recover schema<br/>from validation errors)"]
    intro --> plan["Fingerprint engine<br/>+ draft attack plan"]
    plan --> loop

    subgraph loop["Agent loop (repeats until done, or budget runs out)"]
      direction LR
      b["Build prompt"] --> m["Model call"] --> a["Pick 1 action"] --> o["Run + observe"]
      o -. "writes to memory" .-> b
    end

    loop --> rec["Collect OOB callbacks<br/>+ remove duplicates"]
    rec --> rep(["Report"])
Loading

A scan is a short pipeline with the model in the middle:

  1. The agent reads the schema by introspection, or rebuilds it from the server's validation errors when introspection is disabled.
  2. It fingerprints the server engine and runs a set of misconfiguration checks.
  3. It drafts an initial attack plan from the schema.
  4. It hands control to the model for the main loop.
  5. On exit, it collects the out-of-band callbacks, removes duplicate findings, and prints the report.

The main loop is where the assessment happens. A run has a fixed step budget, and each step is one model call plus the single action it returns. On every turn the agent hands the model a compact view of the run: the schema, a summary of what it has already tried, and the facts and credentials gathered so far. The model replies with one action as JSON:

{"thought": "...", "action": "fuzz", "args": {"field": "userByToken"}, "learned": "...", "verdict": {}}

The learned and verdict fields are how the model writes to its own memory: learned banks a fact, and verdict marks a field dead, open, or exploited so the next prompt reflects it. Most actions send a single request; battery actions like sweep and a full fuzz ladder fan out to roughly 16 requests in one step. The budget setting caps the number of steps.

The agent works from a fixed set of about two dozen actions: raw queries and schema search, the payload batteries, JWT forgery, out-of-band probes, and an authentication flow that can register an account, read the confirmation email from a temporary mailbox, and log in. It retracts a finding when a later step disproves it, and removes duplicates before writing the report.

Coverage

The agent works from a fixed set of actions that span the GraphQL attack surface:

Area Techniques
Reconnaissance Introspection, schema recovery from validation-error suggestions when introspection is off, field brute-forcing (clairvoyance), engine fingerprinting, and misconfiguration checks
Access control BOLA and IDOR, broken function-level authorization, and mass assignment, tested across anonymous, user, and forged-admin identities
Injection SQL, NoSQL, OS command, SSTI, and template injection, fired as payload batteries at individual field arguments
Authentication JWT forgery (alg:none, weak secret, kid, RS256 to HS256 confusion), credential and OTP brute-forcing, and full signup, email-confirmation, and login chains
Out-of-band SSRF and blind injection confirmed through out-of-band callbacks, plus CSRF and CORS checks
Transport and protocol Denial of service, request smuggling, race conditions, APQ cache poisoning, WebSocket subscriptions, and abuse of the @defer and @stream directives

Install

Clone the repository and install it in editable mode. You need Python 3.10 or newer and an API key for a model on OpenRouter.

pip install -e ".[dev]"

Two optional extras add capabilities. Skip either and the scanner still runs; you only lose that one feature.

  • semantic (pip install -e ".[dev,semantic]"): semantic schema search for large schemas (80+ fields), via FAISS, sentence-transformers, and CPU PyTorch. Without it, schema search falls back to a lexical match.
  • ws (pip install -e ".[dev,ws]"): the subscribe action, which probes GraphQL subscriptions over WebSocket. Without it, that action reports itself unavailable.

Set your key and run against an endpoint you're authorized to test:

export OPENROUTER_API_KEY=sk-...
python -m gradientql --url https://your-target.example/graphql

The gradientql command is equivalent to python -m gradientql. Run it with no arguments to open the interactive interface.

Try it without a target

Both paths scan a bundled copy of DVGA, the standard intentionally-vulnerable GraphQL app. You supply only an OpenRouter key.

  • Colab. Open the notebook and run the cells; no local install needed.

  • Docker. Brings up DVGA and scans it in one command (the image includes the semantic extra):

    OPENROUTER_API_KEY=sk-... docker compose -f docker/docker-compose.yml up --build
    

Usage

Run a scan

The base invocation points the scanner at a target and runs to completion:

python -m gradientql --url https://your-target.example/graphql

Add --trace to record every step. It's the main way to inspect a scan after it finishes (see Output for what it writes):

python -m gradientql --url https://your-target.example/graphql --trace

GradientQL picks the output mode automatically:

  • No --url in an interactive terminal opens the interactive interface.
  • --url, a pipe or redirect, or --no-tui prints plain logs.
  • --tui forces the interface when a terminal is attached, even with --url set.

Command-line arguments

Argument Effect
--url URL Target GraphQL endpoint. Overrides target.url from the settings file.
--settings PATH Path to the settings file. Defaults to config/settings.yaml.
--trace [PATH] Record every step to a .jsonl log and a paired .md digest. Bare --trace writes output/agent_trace_<timestamp>.*; pass a path or prefix to write elsewhere.
-v, --verbose Print each step's full thought and observations (plain-log mode).
--resume RUN_ID Resume a previous run from its last checkpoint: a run id (gql-...) or a checkpoint path. See output/checkpoints/.
--max-tokens N Override the model's max output tokens per step (llm.attacker_max_tokens).
--tui Force the interactive interface. Falls back to plain logs when no terminal is attached.
--no-tui Force plain logs even in an interactive terminal.
-h, --help Show usage and exit.

The interactive interface

Run gradientql with no arguments to open the interactive interface (TUI). It has three screens:

  • Menu: the current target, budget, model, proxy, and whether an API key is set, with buttons to start a scan or open settings.
  • Settings: the target URL, budget, model, proxy, request delay and timeout, and the fuzz payload cap, plus a submenu of per-technique attack toggles.
  • Dashboard: shown once a scan starts. It first checks that the API key authenticates, stopping with a clear message if the key is missing or rejected, then updates in place as the run proceeds:
    • a status line: step, elapsed time, request rate, findings, and model;
    • a coverage map of the schema;
    • an activity feed streaming the model's thought and answer as they arrive;
    • a loot pane with harvested credentials, the current session token, and recorded facts;
    • a table of findings.

The coverage map marks each root field as untested, probed, auth-gated, data, exhausted, or a finding, so you can watch the attack surface fill in as the agent works. Press m, or click the activity pane, to open the full model output for any step.

Screenshots of each screen are in docs/: menu, settings, and the live dashboard.

Steer the agent

The agent runs on its own, but you can redirect it mid-scan. Whatever you send is injected into the model's next prompt as an operator instruction that takes priority over its own plan, and it's recorded in the trace. Use it to focus the run (test the upload field for path traversal), flag a miss (you skipped importPaste), or change tack (stop recon, try DoS now).

  • Interactive interface: type into the steering box at the bottom of the dashboard and press Enter. The message shows in the activity feed as operator: ....
  • Plain-log mode: in an interactive terminal, type a line and press Enter at any point during the scan; it's picked up on the next step. This is disabled when input is piped or redirected.

An operator message stays in view for a few steps so the agent doesn't lose it mid-action.

Configuration

Settings are YAML. config/settings.yaml is a template you copy and edit, and --settings points the scanner at a different file. Any key you omit falls back to a built-in default, so a config file only needs the values you want to change. The knobs you'll reach for most:

scanner:
  budget: 60           # max steps per scan (one model call each; a battery
                       # action sends several requests within a step)
  safe_mode: false     # turn off every destructive technique at once
  attacks:             # toggle individual techniques
    injection: true
    ssrf: true
    dos: false         # off by default; it can knock a target over
    jwt: true
    bola: true         # systematic BOLA and BFLA across identities

http:
  proxy: ""            # route traffic through Burp or mitmproxy

llm:
  attacker_model: "z-ai/glm-5.2"

config/settings.yaml documents every field, and docs/CONFIGURATION.md is the full reference: the API-key resolution order, proxy and TLS options, and out-of-band callback settings.

Reaching authenticated objects, where bugs like broken object-level authorization live, needs a session. Give the agent one by putting a valid token in target.headers, or let it earn its own through the signup, email-confirmation, and login flow with the temp_mail action.

Results

Against a fresh DVGA, three models each ran five times at a 30-step budget with the default attack config. All three reliably find the easy categories (introspection, batch-query denial of service, stack-trace leakage) and separate on the multi-step authentication chains, where glm leads. Mean findings per run: glm 7.4, qwen 6.0, gpt-oss 4.8. Per-category detection is in this chart.

A larger budget lets the strongest model go deeper. In one 200-step run, glm stopped itself at step 119 with 20 findings and no false positives; one chain used a confirmed command injection to read the target's own source, recovered the hardcoded JWT_SECRET_KEY, and forged a token from it that bypassed authentication. Full methodology, per-category counts, larger-budget runs, and token usage are in docs/results.md.

Reference

Docker

Two images are provided: gradientql (the scanner) and gradientql-dvga (a patched DVGA target with the gevent concurrency fix baked in). The Compose file brings up DVGA and scans it in one command, as shown above. Full instructions, including scanning your own target from a container, are in docs/docker.md.

Output

Everything the scanner writes goes under output/, which is git-ignored:

  • agent_trace_<timestamp>.jsonl and the paired .md digest, written when --trace is on. Each step records the exact prompt sent to the model, the raw reply, the parsed action, the observation fed back in, and a state snapshot.
  • vuln_stream.jsonl, the findings. They're written as they're confirmed, so a crash partway through a run doesn't lose them.

Caution

These files hold secrets. Traces contain the full prompts, including any credentials and tokens harvested during the run, and checkpoints under output/checkpoints/ store the run's identity headers and credentials. Don't attach them to bug reports or commit them anywhere.

Run the tests

python -m pytest -q

The suite is fast and runs entirely offline.

License

MIT. See LICENSE.

Releases

Packages

Used by

Contributors

Languages