We love working directly with founders who want to build beautiful, user-centric apps on tight timelines and often tighter budgets.
Development in this environment can mean that time on features is prioritised over protection. This is great for an MVP launch but it doesn’t hold up as digital products begin to scale and attract attention.
To avoid this, we make sure we secure the back end of the digital products we build, whilst not impacting the quality or speed for clients.
We recently put that to the test when one of our startup partners was acquired by a larger organisation. They naturally wanted to perform an enterprise-level Penetration Test on the app before dishing out their cash.
So in this post, I cover some of GraphQL-specific security practices we can now say we've battle-tested.
It is by no means an exhaustive list but includes some helpful recipes for securing your API. For a more in-depth reference, you should refer to OWASP
Although most of these points are relevant to any tech stack (or should be translatable), the tech stack we most commonly use is:
NodeJS
GraphQL
Postgres
React/React Native
This post assumes knowledge of directives in GraphQL. However, if you’ve not seen them before it should hopefully still be readable! (Apollo is a good place to learn about them)
Access Control, also known as Authorisation, is how your application permits access to resources in your app. Security 101, right, but in anything bigger than a tiny hobby app, it's easy to leave holes in the bucket.
GraphQL makes it simple and elegant to manage access to resources. Suppose the following:
1type Query {2getMyData: Data @authenticate somePublicData: String3}45type Mutation {6updateAnItem(id: ID!, name: String!): Item @authenticate7}
We authenticate each private field with a custom directive and leave the public fields directive-free. The danger is that we stop here, call the system ‘secure’ and release. The likelihood is that, without being security conscious, you’ve gone and implemented the updateAnItem resolver like this:
1const updateAnItem = (root, args) => {2return Item.update({ name: args.name }).where({ id: args.id });3};
Although you have authenticated generic access to 'updateAnItem', you haven’t checked that this user should be allowed to update this item. Granular authentication must happen at the application logic level, which makes it very hard to keep track of as your application and team grows. The corrected resolver could look like:
1const updateAnItem = (root, args, ctx) => {2return Item.update({ name: args.name }).where({ id: args.id, userId: ctx.userId });3};
Keep an eye out, educate your team to check for them during code reviews and write automated tests to help guide best practices and prevent regressions.
Most applications are under threat from brute-force attacks on passwords, verification codes or any string that provides some sort of authorised access to your system. All it takes is a hacker to execute a script that iterates through likely combinations until it strikes gold.
The first step to at least partially cover you would be to add error tracking and alerts to your system. We use Sentry, but there are others that do a similar job. Log every error that is thrown and set an alert for when it occurs at a frequent rate.
However, if you don’t want to be woken to a trillion alerts from Sentry, then I’d also recommend adding rate-limiting to your vulnerable fields. We’ve built a small directive library for controlling this, it’s as easy as:
1import { createRateLimitDirective } from 'graphql-rate-limit';23const server = new ApolloServer({4typeDefs: gql`5type Mutation {6# Limit to 10 per minute7login(email: String!, password: String!): String! @rateLimit(max: 10, window: 60000)8}9`,10resolvers: {11Mutation: {12login: () => { /* ... */ }13}14},15schemaDirectives: {16rateLimit: createRateLimitDirective({17identifyContext: ctx => ctx.req.ip18})19}20});
More info here: https://github.com/teamplanes/graphql-rate-limit
It can be easy to forget that your UI is not the only interface to your system with the ability to make updates and interact with your business logic, 🤦🏻♂️. A side-effect of this could be that validation checks are baked into the client, and not the server. An example is email verification, perhaps the user should be able to log in whilst their email is not verified yet, but they shouldn’t be able to update their profile or create some data.
Validation checks should be built into your Access Control, not your UI. So perhaps you can abstract it out with an 'withEmailVerified' directive or you may need to implement it in your data layer.
1type Mutation {2login(email String!, password: String!): String3updateProfile: Profile @withEmailVerified4}
A silly mistake, but an easy one to clear up. In development, it’s handy to deliver the full error message and stack trace to the client, but doing this in production could give a hacker enough understanding of the inner workings of your system to exploit it.
Take the following, you have a simple query that accepts an ID (a string) and the resolver of getItemById passes the ID into a Postgres query.
1# Shema2type Query {3getItemById(id: ID!): Item4}56# Query made by client7query myQuery {8getItemById(id: "*") {9id10}11}
You’d end up with an error that could look a little like:
select
from \”items\” where \”id\” = $1 — invalid input syntax for integer: \”
\”
Although this particular message doesn’t give us much other than telling us that there is a table ‘items’ and it has a column named ‘id’. You can easily see how if we had a few other tables referenced, you could build up a picture of the database structure. An attacker could follow a similar process to piece together business logic structure or class implementations.
Most GraphQL server libraries allow you to mask or reformat error messages, with Apollo Server you’d end up with:
1new ApolloServer({2typeDefs,3resolvers,4formatError: error => new Error('Internal server error')5});
Although this is certainly in-exhaustive, hopefully, these points help you in starting on a journey to securing your GraphQL API. Securing your API is never ‘done’, I’d strongly recommend taking a browse through OWASP, a good place to start is with the OWASP Node Goat project’s Top 10.
For further reading on GraphQL-specific security concepts, try Max Stoiber’s post on Securing Your GraphQL API from Malicious Queries.