Learn GraphQL + MongoDB Security Vulnerabilities

Mr. Thank You
9 min readNov 7, 2019

--

Pre-requisites

  • Basic understanding of MongoDB and MongoDB queries.
  • Basic understanding of GraphQL and GraphQL types.

Intro

The aim of this article is to discuss NoSQL injection vulnerabilities found in applications that use both GraphQL and MongoDB. We will first discuss what NoSQL injection vulnerabilities are. We will then discuss different types of NoSQL injection vulnerabilities. Finally, we will list general tips for developers and pentesters when working with GraphQL and MongoDB.

What Are NoSQL Injection Vulnerabilities

NoSQL injection vulnerabilities are a type of vulnerability that allows a hacker to modify/run commands in a NoSQL database. When an injection vulnerability exists, a hacker can send commands to a NoSQL database that then modifies, deletes, or retrieves records from the database. This is dangerous since a hacker will have full access to your database!

Types of Injection Vulnerabilities

Below are a list of injection vulnerabilities developers and pentesters should be aware of when working with both GraphQL and MongoDB:

  1. Malicious JSON

By default, GraphQL doesn’t support JSON as a scalar type. If a developer wants JSON as a scalar type, they will have to use a library like GraphQL-Type-JSON.

When using the JSON scalar type, the developer must be careful with how they handle the JSON. If the developer doesn’t validate the JSON that comes from a GraphQL request, they introduce security risk in the application. Let’s see how this can happen.

One way JSON can wreck havoc on a MongoDB database is when the developer converts the JSON into an object, forgets to validate and sanitize the object, and passes it into a MongoDB query. Below you can see a code example of a vulnerable application:

const typeDefs = [`
scalar JSON
scalar JSONObject
type Query {
posts(search: JSONObject): [Post]
}
type Post {
_id: String
title: String
content: String
}
`
const resolvers = {
Query: {
posts: async (root, params) => {
// The params.search value represents a JSON object. In this case we are parsing the JSON object into a Javascript object and then passing it into Posts.find(), which is a MongoDB query function. If the object is not validated or sanitized, the Javascript object can query for more than what the developer ever wanted.
let searchParams = JSON.parse(params.search)
return (await Posts.find(searchParams).toArray()).map(prepare)
}
}

As you can see the developer takes the JSON string, converts it into an object, and passes it into the Posts.find() query. Why is passing an object into MongoDB query such a problem?

If you know anything about MongoDB, MongoDB queries lets us pass in an object as an argument. Want to find a record if a field equals an exact value? Pass in an object. Want to find records that don’t equal a blank string? Pass in an object.

Below are two ways to write these argument objects:

// Finds post(s) based on the title matching 'My favorite music compressor'
var query_argument = {title: 'My favorite music compressor'};
Posts.find(query_argument);// Finds post(s) with a title that is not equal to a blank string. If all posts have a title, then the query will return all posts from the database.
var query_argument = {title: {"$ne": ""}};
Posts.find(query_argument);

If we put this all together, we can see that a hacker can pass a JSON object into a GraphQL request which if not sanitized/validated, can result in a MongoDB query running the hacker’s malicious code.

2. Custom Scalar types

Custom scalar types provides another potential vulnerability. When a developer wants to use a custom scalar type, they are required to write custom code to process the custom scalar type value. What does processing mean? Below I’ve added an example of how a developer has to process a custom scalar type:

import { GraphQLScalarType } from 'graphql';
import { Kind } from 'graphql/language';

const resolverMap = {
CountryCode: new GraphQLScalarType({
name: 'CountryCode',
description: 'This scalar type represents a two or three letter code that represents a country.',
// For more info on the parseValue, serialize, and parseLiteral options, see this link: https://github.com/graphql/graphql-js/issues/500
parseValue(value) {
// Here we parse the value that comes from the user's request query variables.
switch (value) {
case 'MEX':
return value;
break;
case 'JPN':
return value;
break;
default:
return 'USA';
}
},
serialize(value) {
// Here we have the option of doing something with the value before it's delivered to the user.
return value;
},
parseLiteral(ast) {
// Here we parse the value that comes from the user's request query. This is different from parseValue in that the value we are dealing with here comes from inside the query and NOT from a query variable.
return ast;
},
}),
};

Because the developer has to manually parse the value, there is always the chance that the developer forgot to sanitize/validate the value. If that non-sanitized value is then passed along to the MongoDB server, a vulnerability could exist in the application.

3. Type Mismatching

GraphQL is strongly typed. What this means is that GraphQL doesn’t exactly fit with MongoDB’s flexbile database structure. This has some interesting ramifications when using both technologies together.

For example, let’s say in the GraphQL schema you have a User type that has an email field with the type String. Through hijinks committed by a hacker, your MongoDB database has a User document with an email field that has a type Array. This results in a mismatch between how GraphQL represents the email field and how MongoDB represents the email field.

Therefore, if the application receives a GraphQL request that asks for the User record with an email field that has an Array type, GraphQL will return an error stating that the array doesn’t match the GraphQL User email type. See below:

{
"errors": [
{
"message": "String cannot represent value: [\"charles@xxx.com\", \"sally@xxx.com\"]",
"locations": [
{
"line": 4,
"column": 5
}
],
"path": [
"user",
"email"
],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"stacktrace": [
"TypeError: String cannot represent value: [\"charles@xxx.com\", \"sally@xxx.com\"]",
" at GraphQLScalarType.serializeString [as serialize] (/app/node_modules/graphql/type/scalars.js:159:9)",
" at completeLeafValue (/app/node_modules/graphql/execution/execute.js:635:37)",
" at completeValue (/app/node_modules/graphql/execution/execute.js:579:12)",
" at completeValueCatchingError (/app/node_modules/graphql/execution/execute.js:495:19)",
" at resolveField (/app/node_modules/graphql/execution/execute.js:435:10)",
" at executeFields (/app/node_modules/graphql/execution/execute.js:275:18)",
" at collectAndExecuteSubfields (/app/node_modules/graphql/execution/execute.js:713:10)",
" at completeObjectValue (/app/node_modules/graphql/execution/execute.js:703:10)",
" at completeValue (/app/node_modules/graphql/execution/execute.js:591:12)",
" at /app/node_modules/graphql/execution/execute.js:492:16"
]
}
}
}
],
"data": {
"user": {
"_id": "4daee0e6b200b30456212256",
"email": null
}
}
}

Although this is not a true security risk, a hacker can poison the database with mismatched type data resulting in potential vulnerabilities like information disclosure(an error appears to the user describing the Type mismatch) or a pseudo-denial-of-service attack(the application can’t process the mismatched type data in the database preventing the application from working properly).

One way to resolve this is to implement MongoDB’s schema validations and confirm that it matches up with the GraphQL schema. Also see GraphQL Code Generator for potential make-shift solutions.

4. Strings

Just like SQL injection, there is always the off chance that a developer will take a string value coming from a GraphQL request and inject it (aka string interpolation) into a MongoDB query. In an application this can look like:

let username = req.query.username;
// Here the developer uses string interpolation to inject the username variable into a string
let query = { $where: `username == '${username}'` }
User.find(query);

In the code above, the application takes the username from the GraphQL query and assigns it a variable. The variable is then injected (aka string interpolation) into a string that is finally passed into a MongoDB query. This creates an injection vulnerability.

If a hacker sends a request with the username valuing "' || 1==1", the above code will return every User from the MongoDB database. I won’t go into detail how the "' || 1==1" payload makes the MongoDB query returns every User. For now, just trust that it does. Below is an example of a GraphQL request with this malicious payload:

mutation {
createPost(args: "{\"title\":\"' || 1==1\"", \"content\":\"world\"}") {
_id
title
content
}
}

5. Enums (aka Enumeration Types)

GraphQL supports Enums. If an application utilizes Enums in the GraphQL schema, the developer must ensure that a hacker can’t set the Enums field to an inappropriate value. Still confused? Let’s look at an example to better understand this.

Imagine there is a User type. This User type has a field called role that is represented as a UserRole Enum type.

enum UserRole{
USER
MODERATOR
ADMIN
}
type User {
_id: String
name: String
role: UserRole
}

With this information, a hacker sends a request asking to update the User’s role field to ADMIN. If the developer hasn’t set any restrictions on what the role field can be updated to, the hacker will be able to turn that User into an admin. Below you can see the request sent by the hacker that makes this happen:

mutation {
updateUser(_id: "5acc5e425w22cc7725621821", role: ADMIN) {
role
}
}

General Tips for Developers

Based on these 5 vulnerabilities, below are a list of tips on how to shore up an application’s GraphQL/MongoDB security:

  • If you’re going to allow a user to pass in JSON, understand that the JSON can represent anything. If you have JSON, sanitize/validate it.
  • Be careful with all user input. Just because something has been type checked does not mean it’s safe. Assume it’s malicious and be careful how you “use” that input.
  • Ensure that the GraphQL schema matches with the MongoDB schema. Otherwise you can experience weird bugs that can open the door for numerous security vulnerabilities.
  • When using Apollo’s GraphQLScalarType object, be careful when creating the logic for the parseLiteral and parseValue options. Ideally, the logic should be the same (using the same function for both options if possible).
  • This bears repeating, GraphQL doesn’t validate whether a string is safe or not. So don’t take GraphQL values and pass it directly or indirectly(string interpolation) into a MongoDB query.
  • Every developer should consider restricting write access on fields with an Enum type. Otherwise, you run the risk of a hacker making changes they shouldn’t have access to change.

General Tips for Pentesters

  • Whenever you see a JSON scalar type in the GraphQL schema, probe it. Maybe the developers didn’t correctly validate the object to see if it’s a NoSQL injection payload. Below are two examples of malicious JSON objects you can pass in your GraphQL request to check for this vulnerability:
"{\"title\":{\"$ne\":\"\"}}"
"{}"
  • If you do discover JSON scalar types are utilized in the GraphQL schema, pass in arguments that don’t match up for what’s called for in the documentation (assuming the documentation exists). For example, let’s say that the following GraphQL schema exists:
scalar JSON
scalar JSONObject
type Query {
createPost(attributes: JSONObject): [Post]
}
type Post {
_id: String
title: String
content: String
}

If the documentation calls for the JSON object to match something like…

{\"title\": \"String\", \"content\": \"String\"}

Instead try…

{\"title\": [\"Favorite car?\", \"Favorite food?\"], \"content\": \"I like pizza!\"}

The difference between the two JSON objects is that in the second JSON object we passed an Array as the title instead of a String.

  • Another thing to look out for in a GraphQL schema is custom scalar types. Every GraphQL custom scalar type requires the developer to write custom code. Explore passing in a variety of payloads into the custom scalar type field. You never know if the developer wrote bad parsing logic.
  • When adding custom value(s) to a GraphQL request that represents a custom scalar type, send two requests. One request will have the values embedded inside the query. The other request will set the values as query variables. Why send two requests? You never know if the developer has different parsing logic for values passed in the query itself versus the query variables. See here for more info.
  • If you see an Enum used in a GraphQL schema, use all of the Enum values in a GraphQL request. You never know if you have write privileges with an Enum type that should have write access restrictions.
  • Finally, below are a list of potential payload injections you can pass into a field with a string type to see if an application utilizes string interpolation incorrectly. I recommend testing each payload individually in your own Mongo shell to try and understand why each payload can result in a malicious query.
true, $where: '1 == 1'
, $where: '1 == 1'
$where: '1 == 1'
', $where: '1 == 1
1, $where: '1 == 1'
{ $ne: 1 }
', $or: [ {}, { 'a':'a
' } ], $comment:'successful MongoDB injection'
db.injection.insert({success:1});
db.injection.insert({success:1});return 1;db.stores.mapReduce(function() { { emit(1,1
|| 1==1
' && this.password.match(/.*/)//+%00
' && this.passwordzz.match(/.*/)//+%00
'%20%26%26%20this.password.match(/.*/)//+%00
'%20%26%26%20this.passwordzz.match(/.*/)//+%00
{$gt: ''}
[$ne]=1
';sleep(5000);
';it=new%20Date();do{pt=new%20Date();}while(pt-it<5000);

Much thanks to cr0hn for open-sourcing the wordlist.

Conclusion

When working with GraphQL and MongoDB it’s important to be careful of user input. I know it’s been said time and time again, but always assume user input is malicious. Thanks for reading!

Sources

These articles were used when writing this article:

--

--