This tool is designed to perform checks (see Rules) against your GraphQL documents (which contain one or more query, mutation or subscription operations and any associated fragments) before you ship them to production, to help ensure the safety of your servers. You should use it alongside other tooling such as trusted documents and GraphQL-ESLint to ensure you're shipping the best GraphQL you can.
This tools is designed to be safe to incorporate into your existing projects - it enables you to capture (and allowlist) the current state of your existing operations, called the "baseline", whilst enforcing more rigorous rules against future operations (and against new fields added to existing operations). You can thus safely adopt this tool with strict rules enabled, even if you've been lenient with your GraphQL operations up until this point, without breaking existing operations. And you can change the configuration options at any point and simply reset the baseline to avoid having to adjust existing operations to match your new rules.
gqlcheck
by default uses all cores available on your processor to complete the
checks as fast as possible; to adjust this see Configuration.
Run a check passing your schema and a path to your operations (either .graphql
file(s), directory containing the same, or a
KJSONL file containing operations):
npx gqlcheck -s path/to/schema.graphqls path/to/operations
Output:
path/to/operations/doc2.graphql:
- [17:3] Self-reference limit for field 'User.friends' exceeded: 3 > 2
Problematic paths:
- FoFoF:query>currentUser>F1:User.friends>F2:User.friends>F3:User.friends
path/to/operations/doc3.graphql:
- [7:13] Maximum list nesting depth limit exceeded: 6 > 4
Problematic paths:
- FoFoFoFoFoF:query>currentUser>friends>friends>friends>friends>friends
- FoFoFoFoFoF:query>currentUser>friends>friends>friends>friends>friends>friends
- [5:9] Self-reference limit for field 'User.friends' exceeded: 6 > 2
Problematic paths:
- FoFoFoFoFoF:query>currentUser>friends>friends>friends
- FoFoFoFoFoF:query>currentUser>friends>friends>friends>friends
- FoFoFoFoFoF:query>currentUser>friends>friends>friends>friends>friends
- FoFoFoFoFoF:query>currentUser>friends>friends>friends>friends>friends>friends
Scanned 3 documents consisting of 3 operations (and 3 fragments). Visited 16
fields, 0 arguments, 3 named fragment spreads and 0 inline fragment spreads.
Errors: 0
Infractions: 3
This reveals all our documents are valid (no errors), but they don't comply with
our rules (3 infractions). But these are existing operations; we only want to
defend against newly introduced issues, all existing operations should be
allowed. To achieve this, we create a baseline using the -b
and -u
flags:
npx gqlcheck -s path/to/schema.graphqls path/to/operations -b baseline.json5 -u
New baseline written to baseline.json5
Scanned 3 documents consisting of 3 operations (and 3 fragments). Visited 16
fields, 0 arguments, 3 named fragment spreads and 0 inline fragment spreads.
Errors: 0
Infractions: 0 (ignored: 3)
Passing all these flags is a chore; instead, let's create a configuration file
graphile.config.mjs
:
export default {
gqlcheck: {
schemaSdlPath: "path/to/schema.graphqls",
baselinePath: "baseline.json5",
},
};
Then in CI we can check no new issues are introduced:
npx gqlcheck path/to/operations
Scanned 3 documents consisting of 3 operations (and 3 fragments). Visited 16
fields, 0 arguments, 3 named fragment spreads and 0 inline fragment spreads.
Errors: 0
Infractions: 0 (ignored: 3)
The gqlcheck
command exits with a status code indicating success or failure:
Exit status 0: no issues found.
Exit status 1: errors (including validation errors) found.
Exit status 2: infractions found.
This tool takes the SDL describing your GraphQL schema and your GraphQL documents as inputs, and as output gives you a pass or a fail (with details). So long as you can represent your GraphQL documents as a string using the GraphQL language, and you can introspect your schema to produce an SDL file, you can use this tool to check your documents. (Supports the latest draft of the GraphQL specification.)
This tool uses a worker pool to distribute validation work across all the cores of your CPU (memory allowing). We don't want to slow down your CI or deploy process any more than we have to! We're also very careful to write the rules in a performant manner, leaning into the visitor pattern exposed by GraphQL.js and avoiding manual AST traversal where possible.
Persisted queries, persisted operations, stored operations, trusted documents... Whatever you call them, this system is designed to perform checks on your new documents before you persist them. This means that your server will not have to run expensive validation rules (or know about your overrides) in production - only the operations that you have persisted (and have checked with this tool) should be allowed. Read more about trusted documents; a pattern that is usable with any GraphQL server in any language.
Other than the specified GraphQL validation rules, this tool also checks a few other things. Each check can be configured or disabled, and can be overridden to allow existing operations to pass.
Setting: maxDepth
(normal) and maxIntrospectionDepth
(for introspection
queries).
Check that your operations aren't too deep.
(Leaf nodes are ignored.)
Setting: maxListDepth
(normal) and maxIntrospectionListDepth
(for
introspection queries).
Checks that lists aren't being nested too many times, leading to potential
response amplification attacks; e.g.
{ user { friends { friends { friends { friends { friends { name } } } } } } }
.
(Leaf nodes are ignored, even if they are lists.)
Setting: maxSelfReferentialDepth
and maxDepthByFieldCoordinates[coords]
Attackers often look to exploit cycles in your schema; in general it's unlikely you'd want to visit the same field multiple times, so this rule only allows a field to be referenced once inside itself (i.e. it allows "friends of friends" but not "friends of friends of friends").
Should you need to, you can override this on a per-field basis by specifying a
higher limit using the field's
schema coordinate
(i.e. TypeName.fieldName
). For example, to allow "friends of friends of
friends" you might configure with:
maxSelfReferentialDepth: 2,
maxDepthByFieldCoordinates: {
"User.friends": 3,
},
When you get started with gqlcheck
you'll want to capture the current state of
your GraphQL operations (you can do this with the -u
CLI flag). This will
ensure that all existing operations continue to function (i.e. are allowed)
whilst trying to avoid any issues getting worse. For example, if you set a
default maxDepth
limit of 8
but one of your existing queries has a depth of
12
, you'd still want that existing query to continue working.
gqlcheck
is intelligent - it doesn't just capture the required settings values
for the operation as a whole, it captures the
operation expression path
that describes where the issue occurred, this allows it to bless that particular
path whilst still preventing depth creep in other areas of that same named
operation.
The baseline is only intended to be captured when you first start running the system, or when you do a software update that introduces new rules (and you're already in a "clean" state), since it will hide "known" issues from being reported.
If the CLI flags are not enough, configuration is performed via a
graphile.config.mjs
file. Global settings are stored under the
gqlcheck.config
path, and named-operation overrides under
gqlcheck.operationOverrides[operationName]
. In future, plugins
may be added
to the configuration to allow supporting additional rules.
The below graphile.config.mjs
file demonstrates the key settings you are
likely to want to change. For full configuration options, use TypeScript.
// @ts-check
// The following comment requires TypeScript 5.5+ to work
/** @import {} from 'gqlcheck' */
/** @type {GraphileConfig.Preset} */
const preset = {
gqlcheck: {
// How many workers should we spawn in the background? Defaults to the
// number of CPUs on this machine (or less if insufficient RAM is
// available) since walking ASTs is single threaded.
// workerCount: 4,
// Update this to be the path to your GraphQL schema in SDL format. We
// currently only support loading an SDL from the file system.
schemaSdlPath: "schema.graphqls",
// Enable this setting so that a baseline may be used to hide issues that
// were present before you adopted `gqlcheck`.
baselinePath: "baseline.json5",
config: {
// How many fields deep can you go?
maxDepth: 12,
// How many lists deep can you go?
maxListDepth: 4,
// How many layers deep can a field reference itself
maxSelfReferentialDepth: 2,
// If certain of your coordinates need to be deeply nested (or must never
// be nested), list them here with their maximum depth values (1+).
maxDepthByFieldCoordinates: {
// "User.friends": 1,
// "Comment.comments": 5,
},
},
},
};
export default preset;
If you aren't using TypeScript and want a shorter config, you can omit all comments and export directly:
export default {
gqlcheck: {
schemaSdlPath: "schema.graphqls",
baselinePath: "baseline.json5",
config: {
maxDepth: 12,
maxListDepth: 4,
maxSelfReferentialDepth: 2,
maxDepthByFieldCoordinates: {},
},
},
};
Overrides in gqlcheck
operate on a per-operation-name basis; this is because
your operations will likely evolve over time, so overrides need to apply not
only to the current version of the operation but also all past and future
versions too, even if they're not in the same file name. Every setting can be
overridden on a per-operation basis,
const preset = {
gqlcheck: {
config: {
maxDepth: 12,
maxListDepth: 4,
maxSelfReferentialDepth: 2,
},
operationOverrides: {
// Override the above global settings for the 'MyLegacyQuery' operation
MyLegacyQuery: {
maxDepth: 32,
maxListDepth: 10,
maxDepthByFieldCoordinates: {
"User.friends": 5,
},
},
},
},
};
export default preset;
npm install gqlcheck
If you have configured the schemaSdlPath
then you can run gqlcheck
passing
the paths to files to check:
gqlcheck query1.graphql query2.graphql query3.graphql
Otherwise, pass the -s path/to/schema.graphqls
option to specify where your
schema SDL is.
You can also pass -b baseline.json5
to identify your baseline file; and use
-u
to update the baseline such that all current documents are allowed.
Usage:
gqlcheck [-s schema.graphqls] [-b baseline.json5] [-u] [-l] [-e] doc1.graphql doc2.graphql
Flags:
--help
-h
Output available CLI flags
--version
-v
Output the version
--config <configPath>
-C <configPath>
The path to the config file
--schema <sdlPath>
-s <sdlPath>
Path to the GraphQL schema SDL file
--baseline <jsonPath>
-b <jsonPath>
Path to the baseline file (.json or .json5)
--update-baseline
-u
Update the baseline.json file to allow all passed documents even if they break the rules.
--list
-l
Don't output any details, just list the affected source names.
--only-errors
-e
Only output details about errors (not infractions); combine with `-l` to list only the files with errors.
No. It just helps to differentiate it from the executable documents.
I'm a member of the GraphQL TSC and one of the top contributors to the GraphQL spec.
I'm a community-funded open source maintainer, which means that sponsorship from folks like you helps me to take time off from client work to work on open source. This project was initially sponsored by Steelhead Technologies (thanks Steelhead! โค๏ธ) but over time there will almost certainly be feature requests, bug reports, and maintenance work required. Not to mention that I have a lot of other open source projects too!
๐ Please sponsor me: https://github.com/sponsors/benjie ๐
- GraphQL-ESLint for general GraphQL linting
- Trusted documents to protect your server from untrusted queries
- @graphile/depth-limit to help reduce query amplification attacks
The initial work on this project was funded by Steelhead Technologies - thanks Steelhead!