DEV Community

Cover image for Securing GraphQL APIs with Shield: Best Practices and Common Pitfalls
Rami for Outshift By Cisco

Posted on

Securing GraphQL APIs with Shield: Best Practices and Common Pitfalls

GraphQL is a query language used to interact with APIs, and it has gained popularity among developers for its flexibility and efficiency. As an alternative to REST, GraphQL is known to be great in terms of usability, flexibility, and developer-friendliness. It comes with a powerful declarative-style architecture, allowing you to query only the bits of data that you want to retrieve, making GraphQL a specialised tool to build and consume APIs.
 
However, just like any technology, GraphQL has its own security shortcomings that developers need to be aware of to protect their applications from vulnerabilities.
 
In this deep dive, we will go over some of the top security issues with GraphQL and showcase how GraphQL Shield can be used for security best practices to avoid the common pitfalls that lead to breaches. GraphQL’s differentiators in data exchange are also directly its attack vectors.

Denial of Service Attacks

are attacks meant to impact the availability of a resource. In GraphQL, queries are complex and can be nested, making them susceptible to DoS attacks. A malicious user can construct a query that is so complex that it takes a long time to execute, causing the server to become unresponsive and stuck in an ‘infinite loop of computing’. To prevent this, developers should limit the complexity of queries by setting a maximum depth, complexity, and execution time. Certain Denial of Service attacks are built and executed as Injection Attacks, motivated by the ultimate goal of causing the Denial of Service. However, these are not the typical Denial of Service attacks (Fragments attacks, Direct Injection Attacks).

Injection Attacks

are common web attacks where a malicious query is injected into a mutation or query that can execute arbitrary code. These attacks can result in various security threats, such as unauthorised access to sensitive data, privilege escalation, and remote code execution.
 
Malicious actors conduct injection attacks by targeting a specific stage of the query.

  • Query Depth Attacks
    Typical GraphQL DoS attack risking resource exhaustion and availability

  • Query Timeouts
    Common DoS attack due to a long-running query, causing server queue congestion.

  • Fragment attacks
    Data leakage risks or access control bypass, if an attacker modifies the structure in a query using fragments.

  • Direct injection attacks
    Exploiting input validation measures to inject malicious payloads into the query or mutation parameters, lead

To prevent GraphQL injection attacks, developers should ensure that inputs are properly sanitised and validated before execution. Additionally, indirect security measures are essential to the availability and security of your environment, setting query complexity limits and rate limiting impacts the performance/availability and the security of your data. Finally, active (automated/manual) testing of your environment is necessary to test the implemented measures for stability, correctness, and conformity to security standards.
 
Consider the following example, the input for password includes SQL injection code that can drop the table "Users." To prevent this, developers should sanitise and validate input parameters before executing queries.

mutation createUser($input: CreateUserInput) {
  createUser(input: $input) {
    id
    name
  }
}

{
  "input": {
    "name": "John Doe",
    "email": "johndoe@example.com",
    "password": "password123'; DROP TABLE Users; --"
  }
}

Enter fullscreen mode Exit fullscreen mode

Excessive Data Exposure

occurs when sensitive Personally Identifiable Information (PII) is revealed in API responses. GraphQL allows clients to request only the data they need, but it also allows them to request multiple queries at once, which can result in excessive data exposure. For example, consider the following GraphQL query:

{
  allUsers {
    id
    name
    email
    password
  }
}

Enter fullscreen mode Exit fullscreen mode

In this query, the client is requesting all user data, including the password, which is a sensitive piece of information. To prevent this, developers should ensure that sensitive information is not included in the schema, and authentication and authorization mechanisms are implemented to control access to sensitive data. Overly permissive queries or mutations, such as in the given example are common in GraphQL APIs. These are easily exploitable and can have the highest yield for an attacker, returning all data for a specific resource, rather than only the data that the user is authorized to view, allowing the attack to obtain sensitive data, such as passwords, or payment information, that they should not have access to.
 
Another way that excessive data exposure can occur is through poorly designed authorization mechanisms. For example, if an API relies on cookies or other client-side storage mechanisms to authenticate users, an attacker could potentially obtain sensitive data by manipulating or intercepting these mechanisms.
 
With GraphQL Shield, you can define authorization rules to enforce granular access control rules on query operations. Once these authorization rules are defined, GraphQL shield can wrap your GraphQL schema and enforce those rules, ensuring that incoming query requests are validated against your authorization rules before they are executed. If the authorization requirements are not met by the request, GraphQL shield will intervene and return an error response preventing the data from being accessed.

Over-fetching and Under-fetching

are issues where clients request more or less data than they need, which can result in inefficient queries and reduced performance. GraphQL was originally designed to address the issues of over-fetching and under-fetching, that are commonly associated with the use of REST APIs, but it does require developers to use the appropriate tools and code methods that will allow GraphQL to prevent over-fetching and under-fetching.
 
For example, when fetching data about a user, a REST API might return not only the user's name and email address but also their entire address and phone number. This results in unnecessary data being transferred over the network, which can slow down the response time, increase data usage, and put data in-transit, which directly puts that data in an additional layer of risk.
 
GraphQL solves these issues by allowing clients to specify exactly what data they need, and only that data is returned in the response. This is achieved by defining a GraphQL schema that describes the available data and operations, and allowing clients to specify queries that request only the necessary data.

Security risks typically arise from over-fetching, as additional unwanted data is passed in the response data. To prevent this, developers should optimize queries by ensuring that clients only request the data they need and no more.  

Another fetching related risk is the behavioural predictability of a GraphQL Schema, if an attacker understands the logic flow of a schema, they can construct a query that will retrieve additional data such as e-mail address fields, or PII.

There are several open-source solutions available for improving GraphQL security. One such solution is the graphql-shield library, which provides a declarative syntax for defining and enforcing access control rules. It allows developers to define rules based on the type of operation, the type of user, or any other arbitrary criteria.

GraphQL Shield examples

GraphQL Shield is a tool that helps you create a permission layer around your GraphQL APIs.

This code demonstrates how GraphQL shield can be used to define fine-grained permissions for a GraphQL API, helping to prevent unauthorised access to sensitive data.

import { rule, shield, allow } from 'graphql-shield';

// Define a rule to check if the user is authenticated
const isAuthenticated = rule({ cache: 'contextual' })(
  async (parent, args, { user }, info) => {
    return user !== null;
  },
);

// Define permissions using GraphQL Shield
const permissions = shield({
  // Restrict access to certain fields based on the type of user
  User: {
    // Allow all fields to be accessed by authenticated users
    '*': isAuthenticated,
    // Restrict access to password field to only the authenticated user
    password: rule({ cache: 'contextual' })(
      async (parent, args, { user }, info) => {
        return user.id === parent.id;
      },
    ),
  },
});

export default permissions;

Enter fullscreen mode Exit fullscreen mode

The rule ‘isAuthenticated’ verifies whether the ‘user’ object is not ‘null’. If the ‘user’ object is not ‘null’, the user is authenticated, so the rule remains ‘true’. Otherwise, it returns ‘false’.
 
The ‘cache: ‘contextual’ option is used to set the caching behavior of the rule. In this case, the caching is based on the ‘context’ object, which means that the rule will be re-evaluated for each request.
 
The '*': isAuthenticated line allows authenticated users to access all fields of the User type. The password field, however, is restricted to only the authenticated user. This is achieved by defining another rule that checks whether the id of the authenticated user matches the id of the User object being queried. If the ids match, the rule returns true and allows the query to proceed. Otherwise, it returns false, and the query is denied.

Denial of Service (DoS) attacks:
 
In this example, a rule is defined to limit the maximum number of items that can be fetched in a single query. The maxItems rule takes a limit parameter and returns a rule that checks if the length of the query result is less than or equal to the limit.
 
Permissions are then defined using GraphQL Shield, and the allUsers query is restricted to a maximum of 100 items that can be fetched in a single query.
 
By doing this, GraphQL Shield makes the application more resistant against DoS attacks, as the number of items that can be fetched in a single query is limited, preventing an attacker from overwhelming the server with a large number of queries.

import { rule, shield, allow } from 'graphql-shield';

// Define a rule to limit the maximum number of items that can be fetched in a single query
const maxItems = limit => rule({ cache: 'contextual' })(
  async (parent, args, context, info) => {
    const { length } = await info.mergeInfo.delegateToSchema({
      schema: context.schema,
      operation: 'query',
      fieldName: info.fieldName,
      args: args,
      context: context,
      info: info,
    });
    return length <= limit;
  },
);

// Define permissions using GraphQL Shield
const permissions = shield({
  Query: {
    // Restrict the number of items that can be fetched by the `allUsers` query to 100
    allUsers: maxItems(100),
  },
});

export default permissions;

Enter fullscreen mode Exit fullscreen mode

Injection Attacks 

In this code snippet, a rule is defined to sanitise and validate input parameters before executing the createUser mutation. This helps prevent injection attacks that can execute arbitrary code.

import { rule, shield, deny } from 'graphql-shield';
import { sanitize } from 'sanitize-html';

const sanitizeInput = rule()(
  async (parent, { input }, context, info) => {
    const sanitizedInput = sanitize(input, { allowedTags: [] });
    return input === sanitizedInput;
  },
);

const permissions = shield({
  Mutation: {
    createUser: sanitizeInput,
  },
  User: deny,
});

export default permissions;
Enter fullscreen mode Exit fullscreen mode

In this example, a rule is defined to check if the user is an admin, and then permissions are defined using GraphQL Shield. The permissions restrict access to certain fields based on the type of user. The Query type has two rules: the first one allows users to access their own data, while the second rule restricts access to the allUsers query to only admins. The User type has a rule that allows all fields to be accessed by authenticated users.
 
By doing this, the security risks involved with Over-fetching and Under-fetching are mitigated. Users can only access the data they need, and admins have the necessary permissions to access all user data.

Over-fetching and Under-fetching

import { rule, shield, allow } from 'graphql-shield';

// Define a rule to check if the user is an admin
const isAdmin = rule({ cache: 'contextual' })(
  async (parent, args, { user }, info) => {
    return user.role === 'admin';
  },
);

// Define permissions using GraphQL Shield
const permissions = shield({
  // Restrict access to certain fields based on the type of user
  Query: {
    // Allow all users to access their own data
    me: allow,
    // Restrict access to allUsers query to only admins
    allUsers: isAdmin,
  },
  User: {
    // Allow all fields to be accessed by authenticated users
    '*': allow,
  },
});

export default permissions;

Enter fullscreen mode Exit fullscreen mode

In this example, a rule is defined to check if the user is authenticated, and then permissions are defined using GraphQL Shield. The permissions restrict access to certain fields based on the type of user. The User type has two rules: the first one allows all fields to be accessed by authenticated users, while the second rule restricts access to the password field to only the authenticated user who owns the password. By doing this, the Excessive Data Exposure vulnerability is mitigated, as sensitive data such as passwords cannot be accessed by unauthorised users.

Great! Should we rely on GraphQL Shield now?

Tools are a means to an end, and it should not be the aim to risk everything on a single tool. GraphQL shield helps you in securing in defining and enforcing the best-practice security measures for GraphQL APIs. Ultimately, it comes down to the developers, and their awareness of these vulnerabilities to spot the creation of attack vectors in their APIs. What Shield can do, is help you in grasping the nature of these attack vectors, and what it takes to take control of them.

Top comments (0)