David Mraz
Posted on February 19, 2020
Introduction
GraphQL is a powerful tool, but with great power comes great security risks. One of the biggest advantages of GraphQL is that you are able to get data across multiple resources within a single request. This allows the potential attacker to make complex queries that quickly result in resource exhaustion. In this brief article, we will go through some tips to minimize these risks and secure your GraphQL schema against potential attackers. If you are looking for a starter kit for building secure GraphQL APIs, you can take a look at our company repository for building GraphQL APIs in Node.js.
You can quickly start with the following commands:
git clone git@github.com:atherosai/graphql-gateway-apollo-express.git
install dependencies with
npm i
and start the server in development mode with
npm run dev
Use HTTPS and do not forget about HTTPS redirect
I would not say this issue is GraphQL specific, but nearly all websites should use HTTPS. Moreover, you are communicating with the server in a more secure way. This will also improve your SEO. We often find that some developers forget to add HTTPS redirect or an hts header to your server. Then, if you access http://atheros.ai you will not be redirected to the HTTPS version and then communicate with the using http protocol by accident. If you use express it is also good practice from a security point of view to add helmet middleware to existing server. This library will adjust headers in each request to be more secure. The code for such server can look for example like this:
import express from 'express';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import { PORT, NODE_ENV } from './config/config';
import apolloServer from './initGraphQLServer';
import { httpsRedirect, wwwRedirect } from './lib/http-redirect';
const app = express();
app.enable('trust proxy');
app.use(helmet());
// redirects should be ideally setup in reverse proxy like nignx
if (NODE_ENV === 'production') {
app.use('/*', httpsRedirect());
app.get('/*', wwwRedirect());
app.use(rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
}));
}
// GraphQL server setup
apolloServer.applyMiddleware({ app, path: '/graphql' });
app.listen(PORT, () => {
// eslint-disable-next-line no-console
console.info(`Express listen at http://localhost:${PORT} `);
});
You can see that we have also added redirect from www to non-www, which is beneficial for SEO. These redirects can be done also, for example, reverse proxy like Nginx.
You might also notice that we limit number of requests with Express middleware for each IP. This is especially important in GraphQL servers.
Resource exhaustion prevention
I think the biggest issue in GraphQL (especially if you want to open the schema to the public) comes with its biggest advantage, and that is the ability to query for various sources with one single request. However, there are certain concerns about this feature. The issue is that potential attackers can easily call complex queries, which may be extremely expensive for your server and network. We can reduce the load on the database a lot by batching and caching with Data Loader. Load on the network, however, can not be reduced easily and has to be restricted. There are various ways to limit the capabilities of the attacker to execute malicious queries. In my opinion, the most important and useful methods are the following:
- Rejecting based on query complexity (cost analysis) great for public schema, but needed even for queries behind authorization. A great library for this use case is graphql-cost-analysis as it also provides different cost analysis rules based on the query and not for the whole schema.
- Amount limiting restrict the number of objects someone is able to fetch from the database. Instead of fetching every object, it’s better to use cursor-based pagination.
- Depth limiting block recursive queries, which are too costly. Usually limiting the amount to depth 7 is good enough.
The following code implementes Apollo server with depth limiting as well as query complexity:
import { ApolloServer } from 'apollo-server-express';
import { GraphQLError } from 'graphql';
import depthLimit from 'graphql-depth-limit';
import queryComplexity, {
simpleEstimator,
} from 'graphql-query-complexity';
import schema from './schema';
import { NODE_ENV, CUSTOM_ENV } from './config/config';
const queryComplexityRule = queryComplexity({
maximumComplexity: 1000,
variables: {},
// eslint-disable-next-line no-console
createError: (max: number, actual: number) => new GraphQLError(`Query is too complex: ${actual}. Maximum allowed complexity: ${max}`),
estimators: [
simpleEstimator({
defaultComplexity: 1,
}),
],
});
const apolloServer = new ApolloServer({
schema,
introspection: NODE_ENV !== 'production' && CUSTOM_ENV !== 'production',
validationRules: [depthLimit(7), queryComplexityRule],
formatError: (err): Error => {
if (err.message.startsWith('Database Error: ')) {
return new Error('Internal server error');
}
return err;
},
});
export default apolloServer;
The amount limiting can be for example implemented with custom scalar.
There are many more methods you can implement, but the combination of these three you will cover most cases of malicious queries. None of these methods will solve the problem for every query. Therefore we need to implement a combination of these methods.
Disable introspection
If you are familiar with the tools like GraphQL Playground, you are maybe wondering, how you can know everything about the schema. In GraphQL there is an option to execute the so-called introspection queries of the schema. You can use this tool to know basically everything about the type system of the schema including, what you can query for, available mutations, etc. If you are in a development environment, it is definitely useful to allow introspection for various purposes, In production, however, it can leak important information for potential attackers or it will just reveal information about your new feature, which is not implemented on the fronted. If you want to solve this issue you can use the library called GraphQL Disable Introspection. It allows you to add validation rules that disable introspection. If you are using code above you can pass the options of enabling/disabling introspection in Apollo server. To disable the introspection for everyone is sometimes a bit limited. Therefore it is much better to add introspection on per request bases or to enable introspection only for certain scopes.
Masking errors
When it comes to error handling it is helpful to have a clearly defined method to deal with errors in your GraphQL project. However, it is important to mask every error that users are not allowed to view. For example, if you use an SQL builder such as knex.js, you can then reveal information about your database schema and leak important facts about the project structure to attacker. If you use Apollo server you can define the format error callback like this:
formatError: (err): Error => {
if (err.message.startsWith('Database Error: ')) {
return new Error('Internal server error');
}
return err;
},
Such callback will mask only database errors to not reveal your schema to potential attackers.
Use npm audit in your CI
One of the biggest security issues in your Node.js project is that you can accidentally use a malicious package or a package with security holes. The danger exists not only for lesser-known npm packages as described in this article, but also for the packages with a large user base. Let’s take the example of the latest incident, which affected the package eslint-scope, which in turn is dependent on some widely used packages like babel-eslint and webpack, see postmortem. In this incident, the credentials of one of the contributors were compromised, and then the new version of the packages with malicious code was published. You will never be able to defend yourself fully if you use some external packages, but you can significantly decrease the risk by using npm audit in your continuous integration pipeline.
Summary
The list definitely does not end here. This is only a small subset of security concerns that you need to consider when deploying your GraphQL app to production. I would suggest to check out our repository, where a lot of security concerns are already addressed. In the project we also use the Eslint Security plugin, which helps you suggest common Node.js security issues.
Did you like this post? You can clone the whole repository with examples from GitHub. Feel free to send any questions about the topic to david@atheros.ai.
Posted on February 19, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.