From 9260bf3b2a2fdca17f5d2aaacfff13a8a1dce63f Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:56:31 +0100 Subject: [PATCH 1/2] feat(wabe): add tests for rateLimit --- packages/wabe/src/security.test.ts | 84 +++++++++++++++++++++++++++ packages/wabe/src/utils/testHelper.ts | 3 + 2 files changed, 87 insertions(+) diff --git a/packages/wabe/src/security.test.ts b/packages/wabe/src/security.test.ts index 1decc53e..63d2fb9f 100644 --- a/packages/wabe/src/security.test.ts +++ b/packages/wabe/src/security.test.ts @@ -17,6 +17,90 @@ import { Wabe } from './server' import type { DevWabeTypes } from './utils/helper' describe('Security tests', () => { + it('should apply rate limit on GraphQL endpoints when rateLimit is configured', async () => { + const setup = await setupTests( + [ + { + name: 'RateLimitTest', + fields: { name: { type: 'String' } }, + permissions: { + read: { requireAuthentication: false }, + create: { requireAuthentication: false }, + }, + }, + ], + { + rateLimit: { + interval: 60 * 1000, + numberOfRequests: 2, + }, + }, + ) + + const wabe = setup.wabe + const port = setup.port + const graphqlUrl = `http://127.0.0.1:${port}/graphql` + + const graphqlRequest = async () => + fetch(graphqlUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Wabe-Root-Key': 'dev', + }, + body: JSON.stringify({ + query: 'query { rateLimitTests { edges { node { id } } } }', + }), + }) + + const res1 = await graphqlRequest() + const res2 = await graphqlRequest() + const res3 = await graphqlRequest() + + expect(res1.status).toBe(200) + expect(res2.status).toBe(200) + expect(res3.status).toBe(429) + + await closeTests(wabe) + }) + + it('should not apply rate limit on GraphQL endpoints when rateLimit is not configured', async () => { + const setup = await setupTests([ + { + name: 'NoRateLimitTest', + fields: { name: { type: 'String' } }, + permissions: { + read: { requireAuthentication: false }, + create: { requireAuthentication: false }, + }, + }, + ]) + + const wabe = setup.wabe + const port = setup.port + const rootClient = getGraphqlClient(port) + + const requests = Array.from({ length: 5 }, () => + rootClient.request(gql` + query { + noRateLimitTests { + edges { + node { + id + } + } + } + } + `), + ) + + const results = await Promise.all(requests) + expect(results).toHaveLength(5) + results.forEach((res) => expect(res.noRateLimitTests).toBeDefined()) + + await closeTests(wabe) + }) + it('should throw at server startup when rootKey is empty', async () => { const databaseId = uuid() const port = await getPort() diff --git a/packages/wabe/src/utils/testHelper.ts b/packages/wabe/src/utils/testHelper.ts index 51f69a7b..f6a5dd71 100644 --- a/packages/wabe/src/utils/testHelper.ts +++ b/packages/wabe/src/utils/testHelper.ts @@ -1,4 +1,5 @@ import { v4 as uuid } from 'uuid' +import type { RateLimitOptions } from 'wobe' import { type ClassInterface, EmailDevAdapter, FileDevAdapter } from '..' import { Wabe } from '../server' import type { DevWabeTypes } from './helper' @@ -21,6 +22,7 @@ export const setupTests = async ( isProduction?: boolean disableCSRFProtection?: boolean rootKey?: string + rateLimit?: RateLimitOptions } = {}, ) => { const databaseId = uuid() @@ -37,6 +39,7 @@ export const setupTests = async ( security: { // To make test easier keep default value to true disableCSRFProtection: options.disableCSRFProtection ?? true, + ...(options.rateLimit && { rateLimit: options.rateLimit }), }, authentication: { roles: ['Client', 'Client2', 'Client3', 'Admin'], From 62b0594a07bebc76125f5159d745d034d59728fe Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:57:21 +0100 Subject: [PATCH 2/2] feat: clean doc --- .../docs/documentation/security/index.md | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/wabe-documentation/docs/documentation/security/index.md b/packages/wabe-documentation/docs/documentation/security/index.md index 2a4f88a0..178fac75 100644 --- a/packages/wabe-documentation/docs/documentation/security/index.md +++ b/packages/wabe-documentation/docs/documentation/security/index.md @@ -275,7 +275,11 @@ await run(); ## Rate limiting -Rate limiting is a security feature that allows you to limit the number of requests that can be made to your API within a certain time period. You can enable rate limiting by setting the `rateLimit` property in the `security` configuration object. +Rate limiting is a security feature that protects your API from abuse by limiting the number of requests a client can make within a given time window. When enabled, it applies to all endpoints including the GraphQL endpoint (`/graphql`), helping prevent denial-of-service attacks and ensuring fair resource usage. + +### Configuration + +Enable rate limiting by setting the `rateLimit` property in the `security` configuration object: ```ts import { Wabe } from "wabe"; @@ -285,8 +289,8 @@ const run = async () => { // ... other configuration fields security: { rateLimit: { - interval: 60 * 1000, // 1 minute - numberOfRequests: 100, // 100 requests per minute + interval: 60 * 1000, // Time window in milliseconds (e.g. 1 minute) + numberOfRequests: 100, // Max requests allowed per interval per client }, }, }); @@ -297,6 +301,38 @@ const run = async () => { await run(); ``` +### Options + +| Option | Type | Description | +|--------|------|-------------| +| `interval` | `number` | The time window in milliseconds during which requests are counted | +| `numberOfRequests` | `number` | The maximum number of requests allowed per client within the interval | + +### Behavior + +- The limit is applied **per client** (typically identified by IP address) +- When a client exceeds the limit, subsequent requests receive an **HTTP 429 (Too Many Requests)** response +- The counter resets at the end of each interval + +### Example: Strict rate limit for sensitive endpoints + +For APIs that need stricter protection, use a lower limit: + +```ts +security: { + rateLimit: { + interval: 60 * 1000, // 1 minute + numberOfRequests: 10, // 10 requests per minute + }, +}, +``` + +### Best practices + +- **Production**: Use reasonable limits (e.g. 100–200 requests per minute) to balance protection and usability +- **Public APIs**: Consider stricter limits to prevent abuse +- **Authentication endpoints**: Rate limiting helps mitigate brute-force attacks on login flows + ## Hide sensitive error message You can hide sensitive error messages, for example, to avoid leaking any information to an attacker during the authentication process.