Summary
The maybeQuoteArg function in graphql/resolve/query_rewriter.go explicitly skips sanitization for regexp filter arguments, returning user input raw. This allows DQL (Dgraph Query Language) injection through any GraphQL query or mutation that uses a regexp filter on a @search(by: [regexp]) field.
Root Cause
At query_rewriter.go:2349:
func maybeQuoteArg(fn string, arg interface{}) string {
switch arg := arg.(type) {
case string:
if fn == "regexp" {
return arg // <-- NO SANITIZATION
}
return fmt.Sprintf("%q", arg)
...
}
The raw value is written into the DQL query string by writeFilterArguments in graphquery.go:185 with zero escaping.
Attack Vector
An unauthenticated attacker sends a GraphQL query with a crafted regexp filter value that:
- Closes the regex literal with a second
/
- Closes the
regexp() function call with )
- Injects arbitrary DQL filter logic (
OR, has(), eq(), uid())
The DQL lexer tokenizes /pattern/flags as a single token, then processes the injected content as valid DQL syntax.
Exploitation
Filter Bypass (Queries)
An attacker can bypass intended query filters to retrieve all nodes of a type:
query {
queryUser(filter: { name: { regexp: "/x/i) OR has(User.name" }}) {
name
email
}
}
This produces DQL: @filter(regexp(User.name, /x/i) OR has(User.name)) — returning ALL users regardless of the original filter intent.
Mass Data Modification (Mutations)
The same injection works in update and delete mutation filters via mutation_rewriter.go:990:
mutation {
deleteUser(filter: { name: { regexp: "/x/i) OR has(User.name" }}) {
numUids
}
}
This expands the deletion set to ALL nodes of the type (subject to @auth rules if defined).
Impact
- Data disclosure: Retrieve all nodes of any type with a regexp-indexed field, bypassing filter constraints
- Mass data modification/deletion: Expand mutation targets beyond intended scope
- No authentication required for query-based exploitation
- Mutations require user-level auth but bypass intended scope restrictions
Affected Code Paths
graphql/resolve/query_rewriter.go:2167 → maybeQuoteArg(fn, val) in filter construction
graphql/resolve/mutation_rewriter.go:990 → addFilter(dgQuery[0], m.MutatedType(), filter)
graphql/dgraph/graphquery.go:185 → b.WriteString(arg.Value) serialization
Suggested Fix
Validate regexp filter arguments before passing to DQL. Either:
- Validate that the input matches the expected DQL regex format (
/pattern/flags) and contains no unescaped ) or DQL operators
- Use DQL variable bindings for regexp values (as was done for passwords in the GHSA-q2m9-6jp9-c6mc fix)
Prerequisites
- Target schema must have at least one type with
@search(by: [regexp]) or @search(by: [trigram]) on a String field
- This is a common configuration in Dgraph applications
Reported by zx (Jace)
Summary
The
maybeQuoteArgfunction ingraphql/resolve/query_rewriter.goexplicitly skips sanitization forregexpfilter arguments, returning user input raw. This allows DQL (Dgraph Query Language) injection through any GraphQL query or mutation that uses aregexpfilter on a@search(by: [regexp])field.Root Cause
At
query_rewriter.go:2349:The raw value is written into the DQL query string by
writeFilterArgumentsingraphquery.go:185with zero escaping.Attack Vector
An unauthenticated attacker sends a GraphQL query with a crafted
regexpfilter value that:/regexp()function call with)OR,has(),eq(),uid())The DQL lexer tokenizes
/pattern/flagsas a single token, then processes the injected content as valid DQL syntax.Exploitation
Filter Bypass (Queries)
An attacker can bypass intended query filters to retrieve all nodes of a type:
This produces DQL:
@filter(regexp(User.name, /x/i) OR has(User.name))— returning ALL users regardless of the original filter intent.Mass Data Modification (Mutations)
The same injection works in
updateanddeletemutation filters viamutation_rewriter.go:990:This expands the deletion set to ALL nodes of the type (subject to
@authrules if defined).Impact
Affected Code Paths
graphql/resolve/query_rewriter.go:2167→maybeQuoteArg(fn, val)in filter constructiongraphql/resolve/mutation_rewriter.go:990→addFilter(dgQuery[0], m.MutatedType(), filter)graphql/dgraph/graphquery.go:185→b.WriteString(arg.Value)serializationSuggested Fix
Validate regexp filter arguments before passing to DQL. Either:
/pattern/flags) and contains no unescaped)or DQL operatorsPrerequisites
@search(by: [regexp])or@search(by: [trigram])on a String fieldReported by zx (Jace)