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 withMAX_LIMIT = 100to force the number of retrieved records not to exceed a safe level.category: Establishes the reverse relationship from theProductobject, allowing querying and retrieving detailed information of theCategorythat the product belongs to viacategoryId.
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!
Comments
Post a Comment