Simple and customizable security middleware for GraphQL servers in Deno.
Simple and customizable security middleware for GraphQL servers in Deno
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.
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.
A set up with gql and Opine out-of-the-box:
import { opine, OpineRequest } from "https://deno.land/x/[email protected]/mod.ts";
import { GraphQLHTTP } from "https://deno.land/x/[email protected]/mod.ts";
import { makeExecutableSchema } from "https://deno.land/x/[email protected]/mod.ts";
import { gql } from "https://deno.land/x/[email protected]/mod.ts";
import { readAll } from "https://deno.land/[email protected]/streams/conversion.ts";
import { guarDenoQL } from "https://deno.land/x/[email protected]/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
.
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-supplied maxDepth
optional callback
function: receives an Object
that maps the name of the
operation to its corresponding query depth
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-supplied maxCost
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 subfields
scalarCost
: represents the cost of a scalar
depthCostFactor
: the multiplicative cost of each depth level
optional callback
function: receives an Object
that maps the name of the
operation to its corresponding query cost
If you would like to contribute, please see CONTRIBUTING.md for more information.
Finley Decker: GitHub | LinkedIn
Hannah McDowell: GitHub | LinkedIn
Distributed under the MIT License. See LICENSE for more information.