How to survive a Penetration Test as a GraphQL developer
Some of our favorite clients are startups — 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 prioritized over protection which is great for an MVP launch but doesn’t hold up as products begin to scale and attract attention. We experienced this first hand at a previous startup that we worked at, and have carried that lesson with us into our current client work. We‘ve been evolving our process to ensure we secure our backends over the past couple of years, whilst not impacting our quality or speed for clients. Recently we were lucky enough to have that put to the test by one of our clients. The company was acquired by a larger organization who naturally wanted to perform an enterprise level Penetration Test on the app before dishing out their cash.
In this post, I outline a few GraphQL specific security practices that we can now say have been 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 in question and the one we most commonly use is:
- 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 Authorization, 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:
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 an implemented the
updateAnItem resolver like this:
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:
Keep an eye out, educate your team to check for them during code-reviews and write automated tests to help guide best practice and prevent regressions.
Most applications are under threat from brute-force attacks on passwords, verification codes or any string that provides some-sort of authorized 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 no doubt 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:
More info here: https://github.com/teamplanes/graphql-rate-limit
Server-Side Validation Checks
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 being 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 a
withEmailVerified directive or you may need to implement in your data layer.
Verbose Error Messaging
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.
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:
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.
Have an idea but not really sure what to do next? Get in touch and we can help point you in the right direction and outline the next steps to bring it to life. At the very least we will enjoy a coffee and some above average banter.