// TECHNOLOGIES / GRAPHQL

GraphQL

GraphQL, schema-aware end to end.

A REST scanner pointed at /graphql sees one endpoint and one status code. NewScan reads the schema and tests the operations — introspection and field-suggestion leaks, batching, missing depth and complexity limits, verbose resolver errors, mutation CSRF, and injection or SSRF that is only reachable through a resolver argument. Five dedicated tools, every one false-positive-guarded, and nothing is recorded until the signal is reproduced.

Discovery & engine fingerprinting

// tools: discover_graphql · fingerprint_graphql

The endpoint is found first, then the operation surface behind it. Every root query, mutation and subscription becomes a scannable endpoint in its own right, so the rest of the scan tests operations rather than a single URL. The engine is fingerprinted from its error dialect — the way a server phrases a malformed-query rejection is distinctive — which selects the version-specific CVEs and the default-on features worth probing.

// MOUNT SWEEP + SURFACE ENUMERATION

POST /graphql · /api/graphql · /v1/graphql /query · /gql · /graphql/console # and more # then, per confirmed endpoint: {__schema{queryType{fields{name}}}} -> every query/mutation/subscription recorded as its own endpoint

// ENGINE DETECTION (GRAPHW00F-STYLE)

graphql-js / Apollo · graphene · graphql-ruby Hasura · Ariadne · gqlgen · graphql-php Sangria · Juniper

Malformed and edge-case probes are matched against known error-message signatures. The detected engine is recorded so its known CVEs and default-on behaviours (batching, suggestions, an IDE mount) can be checked specifically rather than generically.

The misconfiguration sweep

// tool: test_graphql

Seven checks that only exist in GraphQL, run against a confirmed endpoint. Each has its own reproduce-first guard — a hardened server stays silent on all seven.

// INTROSPECTION ENABLED  medium

{__schema{types{name}}} -> data.__schema present # full type system

The complete type system — every type, field and argument — handed to an anonymous caller. Recorded only when data.__schema actually comes back, never on a 200 alone.

// FIELD SUGGESTIONS  medium

{ usrs } # near-miss of a real field -> Cannot query field "usrs" on type "Query". Did you mean "users"?

Suggestions rebuild the schema field by field even with introspection switched off — the "clairvoyance" technique. Probed with both a random field and a deliberate typo of a real one, so a server that only leaks on near-misses is still caught.

// QUERY BATCHING  medium

[{"query":"{__typename}"}, {"query":"{__typename}"}] -> array of 2 results # batching accepted

One HTTP request carrying hundreds of operations defeats per-request rate limiting: a login brute-force amplifier and a cheap DoS lever. Flagged only when the server genuinely executes the batch.

// NO DEPTH / COMPLEXITY LIMIT  medium

# built from a REAL cycle in this schema { pastes{ owner{ pastes{ owner{ … }}}}} # and an alias-amplified query { a0:pastes{id} a1:pastes{id} … a7:pastes{id} }

Schema-aware: the nesting is built from an object cycle the target actually has, and the aliases from a real list field — no hardcoded field names, so it works on any engine. Fires if either query is accepted with no depth, complexity or alias rejection.

// VERBOSE RESOLVER ERRORS  medium

Two probes. First a crash query; if that stays quiet, an unbalanced quote is sent into each real String argument in the schema — string interpolation then leaks the database engine and the statement itself. Recorded only on a language stack trace or a database-engine error that echoes the raw query. A hardened server returns a generic message and never flags.

{ pastes(filter: "'") { id } } -> sqlite3.OperationalError: unrecognized token … SELECT * FROM pastes WHERE title LIKE '%'%' # engine + statement leaked

// GRAPHQL CSRF  medium — AND WHY IT DOESN'T FALSE-POSITIVE

A state-changing mutation executed over a GET or form-encoded "simple request" using only the cookies a browser sends automatically is forgeable from any site your users visit. Two guards keep this honest, both learned from real false positives:

# 1. probe a MUTATION, not a query # queries over GET are normal and safe — GitLab serves them # and rejects mutations, so {__typename} would false-positive GET /graphql?query=mutation{…} # 2. send ambient credentials ONLY (cookie jar, no Authorization) # an endpoint that executed only because WE supplied a bearer # token is not CSRF — it stays silent

Injection & SSRF through resolvers

// tools: test_graphql_injection · test_graphql_ssrf

The classic classes are still there — they just live behind a resolver argument instead of a query string, which is exactly why generic scanners miss them. NewScan introspects the schema, builds a valid query per field, and injects into the arguments that actually exist, reusing the same false-positive-safe oracles as the HTTP detectors: an evaluated arithmetic product or a command-output marker, never an echoed payload.

// SQL & COMMAND INJECTION  high–critical

query { systemDiagnostics(cmd: "1;id") } -> uid=0(root) gid=0(root) # output marker query { pastes(filter: "') --") { id } } -> error-based SQLi, engine named

// BLIND SSRF VIA A MUTATION  high

mutation { importPaste( scheme:"http", host:"<canary>", port:80, path:"/") } # confirmed on the out-of-band callback

A resolver that fetches a URL for you is an SSRF primitive. NewScan points it at an OOB collaborator canary and records only on a real callback — with no collaborator configured the probe is inert, so an offline scan cannot produce this false positive.

What gets recorded

// schema leak · DoS · injection
Finding How NewScan confirms it Severity
Injection via resolver Command-output marker or an error-based SQL signal from an introspected argument — reproduced. high–critical
GraphQL SSRF A mutation fetches an attacker-supplied URL; confirmed on an out-of-band callback. high
Introspection enabled data.__schema returned to an anonymous caller. medium
Field suggestions A "Did you mean" response rebuilds the schema with introspection off. medium
Query batching An array of operations is executed in one request — rate-limit bypass. medium
No depth/complexity limit A schema-derived deep-nested or alias-amplified query is accepted. medium
GraphQL CSRF A mutation executes over GET/form-encoded with ambient cookies only. medium
Verbose errors A stack trace or database-engine error echoing the raw query. medium
IDE / playground exposed GraphiQL or a playground console is served to anonymous callers. low

Measured, not asserted

// against a deliberately vulnerable GraphQL app

These detectors are scored against Damn Vulnerable GraphQL Application and a synthetic offline benchmark on every change. The benchmark is the floor: full recall, zero false positives — a change that flags a safe control fails the build, so coverage never grows by getting noisier. The GraphQL work that closed DVGA's list/object-field SQLi, verbose-error disclosure and missing depth limits shipped only once the offline gate still read clean.

How it's recorded

// audit-ready evidence

CATEGORY

Injection maps to OWASP API8; SSRF to API7; schema and error leaks to A01/CWE-200.

SEVERITY

One calibrated severity per finding that translates into each framework's risk rating and remediation SLA — no inflated numbers.

EVIDENCE

The exact operation document, the response that proved it, and — for SSRF — the out-of-band callback that confirmed it.

Point it at your schema.

NewScan is free and self-hosted — bring your own key. Every check on this page runs in the deterministic floor with no model or provider key at all; only the blind-SSRF confirmation needs an OOB collaborator.