GuarDenoQL
Simple and customizable security middleware for GraphQL servers in Deno
Features
- Integrates with an Opine server in a Deno runtime.
- Enables users to customize both a maximum depth and a cost limit for all GraphQL queries and mutations sent to the server.
- Validates queries and mutations against the depth limiter and/or cost limiter before they are executed by the server.
Why?
Depth Limiting
Because GraphQL schemas can be cyclic graphs, it is possible that a client could construct a query such as this one:
However, using a Depth Limiter, you can validate the depth of incoming queries against a user-defined limit and prevent these queries from going through.
Cost Limiting
Queries can still be very expensive even if they aren’t nested deeply. Using a Cost Limiter, your server will calculate the total cost of the query based on its types before execution.
Getting Started
A set up with gql and Opine out-of-the-box:
import { opine, OpineRequest } from "https://deno.land/x/opine@2.2.0/mod.ts";
import { GraphQLHTTP } from "https://deno.land/x/gql@1.1.2/mod.ts";
import { makeExecutableSchema } from "https://deno.land/x/graphql_tools@0.0.2/mod.ts";
import { gql } from "https://deno.land/x/graphql_tag@0.0.1/mod.ts";
import { readAll } from "https://deno.land/std@0.148.0/streams/conversion.ts";
import { guarDenoQL } from "https://deno.land/x/guardenoql@v1.0.1/mod.ts";
// update GuarDenoQL import URL with most recent version
type Request = OpineRequest & { json: () => Promise<any> };
const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = { Query: { hello: () => `Hello World!` } };
const dec = new TextDecoder();
const schema = makeExecutableSchema({ resolvers, typeDefs });
const app = opine();
app
.use("/graphql", async (req, res) => {
const request = req as Request;
request.json = async () => {
const rawBody = await readAll(req.raw);
const body = JSON.parse(dec.decode(rawBody));
const query = body.query;
const error = guarDenoQL(schema, query, {
depthLimitOptions: {
maxDepth: 4, // maximum depth allowed before a request is rejected
callback: (args) => console.log("query depth is:", args), // optional
},
costLimitOptions: {
maxCost: 5000, // maximum cost allowed before a request is rejected
mutationCost: 5, // cost of a mutation
objectCost: 2, // cost of retrieving an object
scalarCost: 1, // cost of retrieving a scalar
depthCostFactor: 1.5, // multiplicative cost of each depth level
callback: (args) => console.log("query cost is:", args), // optional
},
});
if (error !== undefined && !error.length) {
return body;
} else {
const errorMessage = { error };
return res.send(JSON.stringify(errorMessage));
}
};
const resp = await GraphQLHTTP<Request>({
schema,
context: (request) => ({ request }),
graphiql: true,
})(request);
for (const [k, v] of resp.headers.entries()) res.headers?.append(k, v);
res.status = resp.status;
res.send(await resp.text());
})
.listen(3000, () => console.log(`☁ Started on http://localhost:3000`));
GuarDenoQL is fully customizable.
Users can use either the depth limiter, cost limiter or both.
The first argument is the schema
, the second argument is the query
, and the
third argument is an Object
with up to two properties: depthLimitOptions
and/or costLimitOptions
.
Depth Limit Configuration
This feature limits the depth of a document.
const error = guarDenoQL(schema, query, {
depthLimitOptions: {
maxDepth: 4, // maximum depth allowed before a request is rejected
callback: (args) => console.log("query depth is:", args), // optional
},
});
The depthLimitOptions
object has two properties to configure:
maxDepth
: the depth limiter will throw a validation error if the document has a greater depth than the user-suppliedmaxDepth
optional
callback
function: receives anObject
that maps the name of the operation to its corresponding query depth
Cost Limit Configuration
This feature applies a cost analysis algorithm to block queries that are too expensive.
const error = guarDenoQL(schema, query, {
costLimitOptions: {
maxCost: 5000, // maximum cost allowed before a request is rejected
mutationCost: 5, // cost of a mutation
objectCost: 2, // cost of retrieving an object
scalarCost: 1, // cost of retrieving a scalar
depthCostFactor: 1.5, // multiplicative cost of each depth level
callback: (args) => console.log("query cost is:", args), // optional
},
});
The costLimitOptions
object has six properties to configure:
maxCost
: the cost limiter will throw a validation error if the document has a greater cost than the user-suppliedmaxCost
mutationCost
: represents the cost of a mutation (some popular cost analysis algorithms make mutations more expensive than queries)objectCost
: represents the cost of an object that has subfieldsscalarCost
: represents the cost of a scalardepthCostFactor
: the multiplicative cost of each depth leveloptional
callback
function: receives anObject
that maps the name of the operation to its corresponding query cost
Functionality
Depth Limiter
Cost Limiter
How to Contribute
If you would like to contribute, please see CONTRIBUTING.md for more information.
Authors
Finley Decker: GitHub | LinkedIn
Hannah McDowell: GitHub | LinkedIn
License
Distributed under the MIT License. See LICENSE for more information.