Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions packages/wabe-documentation/docs/documentation/security/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
},
},
});
Expand All @@ -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.
Expand Down
84 changes: 84 additions & 0 deletions packages/wabe/src/security.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>(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()
Expand Down
3 changes: 3 additions & 0 deletions packages/wabe/src/utils/testHelper.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -21,6 +22,7 @@ export const setupTests = async (
isProduction?: boolean
disableCSRFProtection?: boolean
rootKey?: string
rateLimit?: RateLimitOptions
} = {},
) => {
const databaseId = uuid()
Expand All @@ -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'],
Expand Down