Autonomous GraphQL security testing, driven by a single language model. Give it an endpoint and a model API key and it runs the whole assessment on its own against the live target: it reads the schema, registers and logs into an account when it needs one, probes the full GraphQL attack surface, and prints a report you can act on.
It does not replay a payload list, it reasons. It maps the attack surface, forms hypotheses, and chains what it finds into what it tries next: in validation, a single command injection snowballed into a full auth bypass by way of the target's own source and its hardcoded JWT secret.
Warning
GradientQL attacks live endpoints autonomously, with no consent gate. Only run it against a target you own or are authorized to test. Scoping is your responsibility.
One loop covers the GraphQL attack surface end to end:
- Reconnaissance: introspection, schema recovery from the server's own validation errors when introspection is disabled, server-engine fingerprinting, and misconfiguration checks.
- Access control: BOLA / IDOR, broken function-level authorization, and mass assignment, tested systematically 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.
- Protocol and transport: denial of service, request smuggling, race conditions, persisted-query (APQ) poisoning, WebSocket subscriptions, and @defer / @stream abuse.
It grades its own work, too: findings it later disproves are retracted, so the final report is one you can trust.
A scan is a short pipeline: introspect the schema (or recover it from validation errors when introspection is disabled), fingerprint the server engine, check for common misconfigurations, draft an attack plan from the schema, then hand control to the model. When it finishes, out-of-band callbacks are collected, duplicate findings are removed, and a report is printed.
The middle step is where the work happens. A run is measured in steps, and each step is one model call plus the action it picks. On every turn the model gets a compressed view of the situation (the schema, a summary of what it has already tried, and the facts and credentials it has gathered) and replies with a single JSON action:
{"thought": "...", "action": "sweep", "args": {}, "learned": "...", "verdict": {}}The optional learned and verdict fields are how the model writes to its own memory: learned
banks a fact worth keeping, and verdict marks a field dead, open, or exploited. Most actions make
one request to the target; battery actions like sweep and a full fuzz ladder fan out to about 16
in a single step. The budget setting caps how many steps a scan runs.
The model draws on a fixed toolset of about two dozen actions: querying and schema search, the payload batteries, JWT forging, out-of-band probing, the authentication flow (including reading a confirmation email from a disposable mailbox), and the attack techniques listed above.
Run it in your browser. Open the notebook in Google Colab and step through it to scan the bundled DVGA target with no local install (you supply an OpenRouter key):
Try it in one command. Scan the bundled DVGA target with Docker, no local install needed:
OPENROUTER_API_KEY=sk-... docker compose -f docker/docker-compose.yml up --build
Install locally. Clone the repository and install it in editable mode:
pip install -e ".[dev]"
You need Python 3.10 or newer and an API key for a model on OpenRouter. Two optional extras add capabilities; skip either and the scanner still runs, losing only that one feature:
semantic(pip install -e ".[dev,semantic]"): semantic schema search on large schemas (80+ fields). Pulls in FAISS, sentence-transformers, and CPU PyTorch. Without it, schema search falls back to lexical matching and nothing else changes.ws(pip install -e ".[dev,ws]"): thesubscribeaction, which probes GraphQL-over-WebSocket subscriptions. Without it, that one action reports itself unavailable and the rest of the scan is unaffected.
The Docker image already includes the semantic extra.
Set your key and run against an endpoint you are authorized to test:
export OPENROUTER_API_KEY=sk-...
python -m gradientql --url https://your-target.example/graphql
The gradientql command runs the same thing as python -m gradientql. Run it with no arguments to
open the interactive interface; see Running a scan
for how the output mode is chosen and Configuration for other ways to supply the
key and set the target.
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 everything the model did, step by step. This is the main way to understand a
scan after it finishes (see Output for what it writes):
python -m gradientql --url https://your-target.example/graphql --trace
The output mode is chosen automatically:
- No
--urlin an interactive terminal opens the interactive interface. - Passing
--url, piping or redirecting the output, or adding--no-tuiprints plain logs. --tuiforces the interface even with--urlset, as long as a terminal is attached.
| 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 matching .md digest (see Output). Bare --trace writes output/agent_trace_<timestamp>.*; pass a path or prefix to write elsewhere. |
-v, --verbose |
Print each step's full, untruncated thought and observations to the console (plain-log mode). |
--resume RUN_ID |
Resume a previous run from its last checkpoint (a run id like gql-... or a checkpoint file 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 log output even in an interactive terminal. |
-h, --help |
Show usage and exit. |
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 stats line: step, elapsed time, request rate, findings, model;
- a coverage map of the schema (see below);
- an activity feed that streams the model's output live, its reasoning and its answer as they are generated;
- a loot pane holding any harvested credentials, the current session token, and recorded facts;
- a table of findings.
Press m, or click the activity pane, to open the full untruncated model output for any step. A
steering box along the bottom lets you redirect the agent mid-scan (see
Steering the agent). Screenshots of each screen live in docs/:
menu, settings, and the live dashboard.
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.
The interface is keyboard- and mouse-driven, with the active keys shown in the footer:
- Menu:
sscan,gsettings,qquit. - Settings:
Esc(or Back) saves and returns; changes apply to the next scan and are written back to the settings file (the API key is never persisted, it stays in the environment orconfig/api_key.local). - Dashboard:
Escstops the scan and returns to the menu.
The agent runs on its own, but you can redirect it while a scan is in progress. Whatever you send is injected into the model's next prompt as an operator instruction that takes priority over its own plan, and it is 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 and it is picked up on the next step. This is disabled when input is piped or redirected.
A steering message stays in view for a few steps so the agent does not lose it mid-action.
Settings are written in YAML. config/settings.yaml is a template you copy and edit, and you can
point the scanner at a different file with --settings. Any key you leave out falls back to a
built-in default, so a config file only needs the values you want to change. The knobs you reach for
most:
scanner:
budget: 60 # max steps per scan (one model call each; battery
# actions fire several requests within a step)
safe_mode: false # disable every destructive technique at once
attacks: # toggle individual techniques
injection: true
ssrf: true
dos: false # off by default; can knock a target over
jwt: true
bola: true # systematic BOLA/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, including the API-key resolution order, proxy and TLS options, and out-of-band
callback settings.
To reach authenticated objects, which is where bugs like broken object-level authorization live,
the scanner needs a session. Give it one by putting a valid token in target.headers, or let it
earn one itself through the signup, email confirmation, and login flow using the temp_mail action.
Against a fresh Damn Vulnerable GraphQL Application (DVGA), the standard intentionally-vulnerable GraphQL target, three models were each run five times at a 30-step budget with the default attack configuration; see the per-category detection results in this chart. All three find the easy categories (introspection, batch-query denial of service, stack-trace leakage) in nearly every run, and separate on the multi-step authentication chains, where glm is strongest. Mean findings per run: glm 7.4, qwen 6.0, gpt-oss 4.8.
Given a larger budget, the strongest model goes deeper. In a single 200-step run, glm self-terminated
at step 119 with 20 findings and no false positives: a confirmed command injection read the target's
own source, which yielded the hardcoded JWT_SECRET_KEY and from that a JSON-body auth bypass that
unmasks every user's password.
Full methodology, the exact per-category counts, larger-budget runs, and token usage are in docs/results.md.
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
(shown in the Quickstart). Full instructions, including scanning your own target from a
container, are in docs/docker.md.
Everything the scanner writes goes under output/, which is ignored by git:
agent_trace_<timestamp>.jsonland the matching.mdfile (the readable version), written when--traceis on. Each step records the exact prompt sent to the model, the raw reply, the parsed action, the observation fed back in, and a snapshot of the state.vuln_stream.jsonl, which holds the findings. They are written as they are confirmed, so a crash partway through a run does not lose them.
Caution
These files hold secrets. The 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.
python -m pytest -q
The test suite is fast and runs entirely offline.
MIT. See the LICENSE file.