GraphQL Injection: SQLi and NoSQL Through Resolver Arguments
GraphQL is not a shield. Resolvers still build database queries. If an argument lands in a string concat or a raw Mongo filter, you get the same injection class you already know—just wrapped in a prettier JSON body.
Stop treating /graphql as "API magic." Treat every leaf argument as untrusted input.
Find the wire shape first
Capture a real mutation or query from the UI. Note argument types in the schema (or from introspection if it is open). Strings that look like ids, search filters, sort keys, or email lookups are your first candidates.
Send the same operation twice with a single character flipped. Watch for 500s, stack traces, or latency spikes. GraphQL errors that echo database driver messages are gifts—copy them into the report.
I've seen filter: "admin'--" bounce off a typed schema while filter: { $gt: "" } sailed through because the resolver accepted a generic JSON scalar. Type systems lie when the server uses JSON or Any for convenience.
Variables and inline literals can diverge. A typed variable may reject an object, while the same argument pasted into the query document is accepted as an untyped AST node. Always fuzz both.
SQL-shaped probes
Classic payloads still work when the resolver builds SQL:
'and''for quote handling1 OR 1=1style boolean tests on numeric fields that are actually strings- stacked queries only if the driver and program policy allow it—most do not
- time-based sleeps only if the program allows DoS-adjacent testing—prefer boolean differentials first
Prefer proving a data difference over dumping tables. Two accounts, one private row, one injected filter that returns the private row: that is the report triage wants.
Watch nested inputs. user(where: { email: { equals: "..." } }) style ORMs still break when someone concatenates orderBy or search outside the query builder. Sort and search fields are chronically under-validated. Pagination cursors that decode to SQL fragments are another quiet sink—decode one, then try a broken cursor and read the error.
NoSQL operator objects
If the backend is Mongo-ish, try swapping a string for an object:
{ "email": { "$ne": "" } }
or "$regex" / "$gt" on login, reset, and lookup fields. GraphQL variable types sometimes block this; inline literals in the query document sometimes do not. Test both paths.
Also try array injection where a single id is expected: ids: ["mine", "victim"] or id_in style args that skip ownership checks. Login and password-reset mutations that accept a filter object instead of a plain email are high value—operator injection there is account enumeration or takeover, not just a noisy 500.
What to put in the ticket
Show the exact operation document, variables, and the unauthorized row or auth bypass. Name the resolver argument. Recommend parameterized queries, allowlisted filter keys, and rejecting non-scalar JSON where a string was declared.
If you only have a syntax error that proves concatenation, say so and keep severity honest. A blind boolean that leaks another tenant's invoice is the cleaner story.
GraphQL injection is still injection. The schema is a map. The database is the target.