Injection attacks are a very common type of web attack, where an attacker sends malicious input to an API that has an interpreter or parser that processes the input and passes it on to a backend system or database. If the interpreter/parser doesn’t validate or sanitize the input, then the attacker can gain access to or manipulate confidential data.
GraphQL is uniquely vulnerable to injection attacks for a few reasons:
An injection attack can happen at any phase during the execution of a query:
Query: {
customer(obj, args, context, info) {
const stmt = `SELECT * FROM customers where id = ${id};`;
const customerData = db.query(stmt)
return customerData
}
}
If a value like id: "1' OR 1=1–" is provided, it will result in a SQL injection and the resolver will return all customers, triggering a data breach.
GraphQL injection attacks can lead to a few different scenarios:
Below, we’ll cover some specific tactics that attackers might use in an injection attack, including Aliases, SQL injections, command injections, XSS assaults, LDAP and NoSQL injections.
A SQL injection involves exploiting systems that use SQL to communicate with their databases. An attacker can inject SQL statements that manipulate a GraphQL query to access, modify or delete data.
For example, an attacker could include the following code in a GraphQL query that retrieves a particular customer's details:
query {
customer(id: "22371' OR 1=1–") {
name,
email,
address,
contact
}
}
During the execution phase, the above code would cause the query to return all the customer details instead of just the customer matching the original query.
A NoSQL injection attack is similar to a SQL injection attack. It targets systems or applications that use NoSQL databases.
The following query is an example of a GraphQL query that accepts a JSON input to search for a user via user names or email addresses.
query {
users(search: "{\"username\": {\"$regex\": \"jan\"}, \"email\": {\"$regex\": \"jan\"}}",
options: "{\"skip\": 0, \"limit\": 10}") {
_id
username
fullname
email
}
}
The attacker could modify the JSON data type fields to include MongoDB or other NoSQL database-specific commands, which would then be executed by the database when the GraphQL query is processed.
query {
users(search: "{\"email\": {\"$gte\": \"\"}}",
options: "{\"fields\": {}}") {
_id
username
fullname
email
}
}
During the execution phase, the above query will return all fields specified in the GraphQL schema for all users in the system.
LDAP is commonly used for storing, retrieving, modifying and searching for data in a hierarchical directory. A GraphQL query that accepts LDAP input could be used to retrieve data from an LDAP directory.
LDAP injection attacks typically target the search filter of an LDAP query, which is used to specify the conditions that must be met for a record to be returned. An attacker can modify the search filter to include malicious code, allowing them to retrieve sensitive information or perform other unauthorized actions. For example, an attacker might modify a search filter to include a wildcard character, which would return all records in the directory.
query {
user(username: "*") {
name
email
groups
}
}
During the execution phase, the above query returns details of all users.
Command injection is an exploit in which an attacker can inject shell commands or code into a GraphQL query. It allows the attacker to execute arbitrary commands on the server, thus potentially giving them wide access. For example, consider the following GraphQL query
query {
getUser(id: "1") {
name
email
}
}
An attacker can inject a command into the id field to execute a shell command on the server during the execution phase, such as:
query {
getUser(id: "1; ls -la") {
name
email
}
}
If the server is using an API that interprets a string as a shell command (for instance is using child_process.execFile), this would cause the server to execute the command ls -la
.
XSS attacks can occur when an attacker injects malicious code into trusted websites, generally in the form of browser scripts. For example, an attacker might inject malicious code into the comment field, such as:
query {
getComment(id: "1") {
user
comment: "<script>alert('XSS Attack')</script>"
}
}
When a user views the results of this query, the malicious script will be executed in their browser.
GraphQL actions can take an operation name as part of the query. For example, here is a query that uses CustomName as an operation name:
query CustomName {
getCustomName
{
first
last
}
}
In the audit log, the application will display all queries and mutations that users are executing on the system. If the query had an operation name, it will be recorded in the audit log under that operation name. This creates an opportunity for attackers to “spoof” the log by putting a query under a misleading operation name. For example, the mutation below logs the mutation as getUser when actually, it’s creating a user.
mutation getUser {
createUser(name:"<script>alert(1)</script>", content:"zzzz", public:true) {
userId
}
}
Or, the query below logs arbitrary strings to the audit log:
query arbitraryString {
systemHealth
}
HTML injection allows an attacker to inject malicious code into a vulnerable web page. HTML injections are similar to XSS attacks. The following query demonstrates how an attacker can create an HTML injection:
mutation {
createPaste(title:"<h1>hello!</h1><script>alert('Attack')</script>", content:"zzzz", public:true) {
paste {
id
}
}
}
Below, we outline some ways to protect your server from GraphQL injection attacks.
Input validation means having checks in place for GraphQL input. These checks validate that the input has an expected format and doesn’t include special characters frequently used in injection attacks.
For example, in the above SQL injection examples, we see that an attacker supplies input ( 1233' OR 1=1-) that includes the single quote special character.
The dynamic SQL statement in the resolvers might be:
"SELECT * from customers where id='" + input + "'"
After concatenating it with the provided input, the statement is evaluated as:
SELECT * from customers where id='1233' OR 1=1—'
The single quote acts as a string delimiter in SQL. By adding it to the input, the statement searches for customers with an id of 1233 or 1=1. Since 1=1 is always true, it returns all customers. The last single quote is commented out, then, since – and any text after it is considered a comment in SQL.
In this example, input validation would determine that the input contained dangerous special characters (single quote and -); input sanitization would then remove these characters.
While input validation and sanitization can be performed in the resolvers (execution stage), it is recommended to do it earlier, in the validation stage, when the AST is validated against the GraphQL schema to determine that only valid types and fields are being requested. In addition to the basic Int, Float, String, Booleans, types, you can additionally enforce that inputs match custom scalar types, like email addresses. This helps you identify the data and validate it before even transferring it to the server.
Prepared statements bind the actual data values to the placeholders in the template at runtime. For example, instead of writing a query like this:
SELECT * FROM users WHERE id = '123';
You would use a prepared statement in your resolver like this:
SELECT * FROM users WHERE id = ?;
The ? is a placeholder that temporarily takes the place of the data. The SQL query is pre-compiled with placeholders, and the user’s data is added later. This means that even if a nefarious actor submits a GraphQL query that looks like:
query {
user(id: "22371' OR 1=1 ") {
name,
email,
address,
contact
}
}
The initial SQL query logic won’t be changed. Instead, the database will look for a user admin whose password is literally “22371' OR 1=1 ”.
Several well-known languages have libraries and implementations of the GraphQL specification. Attackers take advantage of flaws in these servers or implementations. For example, the use of exec and execSync in @graphql-tools/git-loader before 6.2.6. allows arbitrary command injection. Therefore, it's crucial to pick secure libraries and versions while creating GraphQL APIs. You can learn more about public GraphQL implementations here.
GraphQL injection attacks may undermine critical web applications and systems. To prevent GraphQL injection attacks, it is essential to follow best practices around input validation and the use of secure libraries.
Security tooling often focus on REST APIs input validation and sanitization, leaving GraphQL APIs unaddressed. With Inigo, you can establish rigid guidelines for which operation names (as well as other elements of the document) to accept from clients. This ensures that your policy is rigid regardless of which GraphQL servers you use. The following is an example configuration that validates optional names for a GraphQL API:
kind: Security
name: my_server
spec:
validation:
alias_name: "^[a-zA-Z]+$"
directive_name: "^[a-zA-Z]+$"
operation_name: "^[a-zA-Z]+$"
arguments:
String: "^[a-zA-Z]+$"
There are many resources available to help developers learn about GraphQL security and injection attacks, including
Protecting your GraphQL APIs is critical, and this process may be aided with GraphQL security platforms like as Inigo, which operate with any GraphQL server. Inigo has schema-based access control, detailed analytics, dynamic rate-limiting, an easy UI, and much more to help you accelerate API adoption.