GROQ queries and security in Sanity.io

5 replies
Last updated: Feb 7, 2024
Is there a place I can read about GROQ queries and security?I stumbled upon queries like
*[_type == "page" && slug.current in ${slugs}]
and given that in that case slugs come from the URL I assume there's an GROQ injection risk there?Can't find much material on the topic.
AI Update

You're absolutely right to be concerned about GROQ injection! The good news is that Sanity has built-in protection through parameterized queries, which is exactly what you should be using for cases like this.

The Problem with String Interpolation

When you write something like:

const query = `*[_type == "page" && slug.current in ${slugs}]`

You're doing JavaScript string interpolation, which means you're directly inserting user input into your query string. This is indeed vulnerable to GROQ injection attacks and can also cause syntax errors if the values contain special characters like quotes.

The Solution: Use GROQ Parameters

Instead, use GROQ parameters with the $ syntax:

const query = `*[_type == "page" && slug.current in $slugs]`
const params = { slugs: slugsFromUrl }

const results = await sanityClient.fetch(query, params)

Parameters are passed as the second argument to client.fetch(), and the Sanity client automatically handles proper encoding and escaping.

Why Parameters Are Secure

GROQ parameters provide several critical security benefits:

  1. Prevention of GROQ injection: Parameters must be JSON literals, so they can only contain values (not arbitrary GROQ expressions). This means user input can't alter the structure of your query.

  2. Automatic escaping: The client library properly escapes special characters, preventing syntax errors and injection attempts.

  3. Type safety: Parameters maintain their data types throughout the query execution.

As the Sanity documentation explains, since parameters are JSON literals, they cannot contain GROQ expressions that could alter the query structure, making them safe to pass from user input.

Best Practices

  • Always use $parameters for dynamic values, especially anything coming from user input (URLs, form fields, etc.)
  • Use the Sanity JavaScript client when possible—it handles parameter encoding automatically
  • If a parameter might be missing, pass null or use conditional logic with select()

The GROQ parameters specification has more details on the technical implementation if you want to dive deeper into how this works under the hood.

GROQ is arguably not vulnerable to this type of injection in a security sense, since anyone can just send arbitrary GROQ queries to the API to access your data.
Visibility of private data (e.g. drafts) is protected by
the role-based security system . This system isn't vulnerable to injection, as there is no way that anyone can escape the permission enforcement by crafting a special GROQ query.
However, if you use something like
slug.current in ${slugs}
and this is just doing JavaScript interpolation, then you're potentially exposed to syntax errors caused by things like quotes. You should use real query parameters for this, e.g.
slug.current in $slugs
(this is GROQ syntax, not JS syntax). You would then pass
slugs
as query parameter to the API. This is explained here .
I don't know if there is a document about GROQ and security.
Although if you have an authenticated query and are using this interpolation that would expose potentially secret data though?
Yes. But if a malicious actor has access to run authenticated queries, then you're already in a bad situation, and GROQ interpolation probably isn't your main problem?
Well, if I have an entirely private dataset but with some content that “actually public” and some that are secret, like a “secret.document” ID or some user data.
If we expose a endpoint for blog posts and be “lazy” with using string interpolation instead of query params like this:



const query = `*[_type == "post" && _id > ${lastId}]`
I guess we could pass lastId to the endpoint with something like this:


lastId = `${validLastId}] {
"allDocuments": *[],
}[`

We almost always use queryParams but I haven’t thought too much about this myself

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?