Enhancing security when using GraphQL

Introduction

In previous articles, I have provided guidance on using GraphQL in project development, however, the flexibility of GraphQL also comes with security risks that are not fully supported by default. In this article, I will guide you through two simple but effective ways to enhance security including

  • Limit data: This applies not only to GraphQL but also to Restful APIs, this is the minimum necessary action to prevent Massive Data Retrieval attacks, because by default when querying data, it will fetch all records in the table, if your database has millions of records, it will cause your system to suffer an Out of Memory (RAM crash) immediately due to processing and parsing a huge amount of JSON data.
  • GraphQL deep limit: Prevent Deep Nested Query attacks, in practical use cases, tables will always have relations with each other. If hackers discover this relationship, they can write nested queries 20-30 levels deep (such as users -> orders -> products -> order -> user...). This query does not pull out many rows of data, but it forces the Database to execute dozens of complex nested JOIN commands, causing the Database CPU to spike to 100% and hanging the entire system.

I will specify how to handle this in the content below, you can apply it immediately to your project to protect the system effectively.

Prerequisites

This article uses GraphQL, Prisma, PostgreSQL in a NextJS project, if you have not set up enough of these components, please review the previous articles where I provided instructions.

Detail

Create file prisma/schema.prisma

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

generator nestgraphql {
  provider = "prisma-nestjs-graphql"
  output   = "../gen/graphql"
}

model Category {
  id       Int       @id @default(autoincrement())
  name     String
  products Product[]
}

model Product {
  id         Int      @id @default(autoincrement())
  name       String
  price      Float
  categoryId Int
  category   Category @relation(fields: [categoryId], references: [id])
}

After migrating and generating the necessary files for Prisma, create the file resolver/category.resolver.ts with the following content:

import {Parent, ResolveField, Resolver} from '@nestjs/graphql'
import {Category} from 'gen/graphql/category/category.model'
import {Product} from 'gen/graphql/product/product.model'
import {PrismaService} from 'src/service/prisma.service'

@Resolver(() => Category)
export class CategoryResolver {
  constructor(private readonly prisma: PrismaService) {}

  @ResolveField(() => [Product])
  async products(@Parent() category: Category) {
    return this.prisma.product.findMany({where: {categoryId: category.id}})
  }
}

Define a CategoryResolver using @ResolveField(). Its function is to establish a relationship from a Category object, automatically searching and returning a list of Product items belonging to that Category based on the corresponding categoryId in the database.

Create file resolver/product.resolver.ts

import {
  Args,
  Mutation,
  Parent,
  Query,
  ResolveField,
  Resolver,
} from '@nestjs/graphql'
import {Category} from 'gen/graphql/category/category.model'
import {CreateOneProductArgs} from 'gen/graphql/product/create-one-product.args'
import {DeleteOneProductArgs} from 'gen/graphql/product/delete-one-product.args'
import {FindManyProductArgs} from 'gen/graphql/product/find-many-product.args'
import {Product} from 'gen/graphql/product/product.model'
import {UpdateOneProductArgs} from 'gen/graphql/product/update-one-product.args'
import {PrismaService} from 'src/service/prisma.service'

@Resolver(() => Product)
export class ProductResolver {
  constructor(private readonly prisma: PrismaService) {}

  @Query(() => [Product], {name: 'products'})
  getProducts(@Args() args: FindManyProductArgs) {
    const MAX_LIMIT = 100
    const safeTake = args.take && args.take <= MAX_LIMIT ? args.take : MAX_LIMIT
    return this.prisma.product.findMany({
      ...args,
      take: safeTake,
    })
  }

  @ResolveField(() => Category)
  async category(@Parent() product: Product) {
    return this.prisma.category.findUnique({where: {id: product.categoryId}})
  }
}
  • getProducts: Provides a list reading API for products, allowing the client to pass in filtering, sorting and pagination parameters. Here, output data control is processed with MAX_LIMIT = 100 to force the number of retrieved records not to exceed a safe level.
  • category: Establishes the reverse relationship from the Product object, allowing querying and retrieving detailed information of the Category that the product belongs to via categoryId.

Please install the following package

yarn add graphql-depth-limit

Then update the file app.module.ts to import the resolvers

import {ApolloServerPluginLandingPageLocalDefault} from '@apollo/server/plugin/landingPage/default'
import {ApolloDriver, ApolloDriverConfig} from '@nestjs/apollo'
import {Module} from '@nestjs/common'
import {GraphQLModule} from '@nestjs/graphql'
import depthLimit from 'graphql-depth-limit'
import {join} from 'path'
import {CategoryResolver} from './resolver/category.resolver'
import {ProductResolver} from './resolver/product.resolver'
import {PrismaService} from './service/prisma.service'

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'gen/graphql/schema.gql'),
      sortSchema: true,
      playground: false,
      plugins: [
        ApolloServerPluginLandingPageLocalDefault({
          embed: true,
        }),
      ],
      validationRules: [depthLimit(3)],
    }),
  ],
  providers: [
    PrismaService,
    ProductResolver,
    CategoryResolver,
  ],
})
export class AppModule {}

graphql-depth-limit operates at the Gateway/Middleware layer of Apollo Server. It will parse the syntax of the query before running. If it detects a query nested deeper than the allowed level (such as exceeding 3 layers), it will refuse processing immediately without needing to connect to the Database.

Please use this query to test the limit, the maximum result only returns 100 items

query TestLimit {
  products(take: 200) {
    id
    name
    category {
      id
      name
    }
  }
}


Use this query to check nested loop query

query TestCircularLoopQuery {
  products(take: 1) {
    id
    name
    # Round 1
    category {
      id
      name
      products {
        id
        name
        # Round 2
        category {
          id
          name
          products {
            id
            name
            # Round 3
            category {
              id
              name
              products {
                id
                name
                # You could continue...
              }
            }
          }
        }
      }
    }
  }
}


If the nested loop query has not been blocked, the result will be as follows


This is the result after successfully blocking it

Happy coding!

See more articles here.

Comments

Popular posts from this blog

All Practice Series

Kubernetes Deployment for Zero Downtime

Deploying a NodeJS Server on Google Kubernetes Engine

Setting up Kubernetes Dashboard with Kind

Monitoring with cAdvisor, Prometheus and Grafana on Docker

Using Kafka with Docker and NodeJS

Sitemap

React Practice Series

Practicing with Google Cloud Platform - Google Kubernetes Engine to deploy nginx

Kubernetes Practice Series