nextjs-secure 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +736 -688
- package/dist/audit.cjs +1337 -0
- package/dist/audit.cjs.map +1 -0
- package/dist/audit.d.cts +679 -0
- package/dist/audit.d.ts +679 -0
- package/dist/audit.js +1300 -0
- package/dist/audit.js.map +1 -0
- package/dist/bot.cjs +1521 -0
- package/dist/bot.cjs.map +1 -0
- package/dist/bot.d.cts +567 -0
- package/dist/bot.d.ts +567 -0
- package/dist/bot.js +1484 -0
- package/dist/bot.js.map +1 -0
- package/dist/index.cjs +2850 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2784 -10
- package/dist/index.js.map +1 -1
- package/package.json +26 -1
package/README.md
CHANGED
|
@@ -4,14 +4,18 @@
|
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[](https://www.typescriptlang.org/)
|
|
6
6
|
[](https://nextjs.org/)
|
|
7
|
+
[]()
|
|
7
8
|
|
|
8
|
-
Production-ready security middleware for Next.js App Router. Zero config, maximum protection.
|
|
9
|
+
Production-ready security middleware for Next.js 13+ App Router. Zero config, maximum protection.
|
|
9
10
|
|
|
10
11
|
```typescript
|
|
11
|
-
import { withRateLimit } from 'nextjs-secure'
|
|
12
|
+
import { withRateLimit, withJWT, withValidation } from 'nextjs-secure'
|
|
12
13
|
|
|
13
|
-
export const
|
|
14
|
-
|
|
14
|
+
export const POST = withRateLimit(
|
|
15
|
+
withJWT(
|
|
16
|
+
withValidation(handler, { body: schema }),
|
|
17
|
+
{ secret: process.env.JWT_SECRET }
|
|
18
|
+
),
|
|
15
19
|
{ limit: 100, window: '15m' }
|
|
16
20
|
)
|
|
17
21
|
```
|
|
@@ -27,6 +31,7 @@ Building secure APIs in Next.js shouldn't require hours of boilerplate. Most pro
|
|
|
27
31
|
- **Edge Ready** - Works on Vercel Edge, Cloudflare Workers, Node.js
|
|
28
32
|
- **Flexible** - Memory, Redis, or Upstash storage backends
|
|
29
33
|
- **Lightweight** - No bloated dependencies, tree-shakeable
|
|
34
|
+
- **Complete** - Rate limiting, auth, CSRF, headers, validation, audit logging
|
|
30
35
|
|
|
31
36
|
## Installation
|
|
32
37
|
|
|
@@ -42,32 +47,20 @@ pnpm add nextjs-secure
|
|
|
42
47
|
|
|
43
48
|
- [Quick Start](#quick-start)
|
|
44
49
|
- [Rate Limiting](#rate-limiting)
|
|
45
|
-
- [Basic Usage](#basic-usage)
|
|
46
|
-
- [Algorithms](#algorithms)
|
|
47
|
-
- [Storage Backends](#storage-backends)
|
|
48
|
-
- [Custom Identifiers](#custom-identifiers)
|
|
49
|
-
- [Response Customization](#response-customization)
|
|
50
50
|
- [CSRF Protection](#csrf-protection)
|
|
51
|
-
- [Basic Setup](#basic-setup)
|
|
52
|
-
- [Client-Side Usage](#client-side-usage)
|
|
53
|
-
- [Configuration](#configuration-1)
|
|
54
|
-
- [Manual Validation](#manual-validation)
|
|
55
51
|
- [Security Headers](#security-headers)
|
|
56
|
-
- [Quick Start](#quick-start-1)
|
|
57
|
-
- [Presets](#presets)
|
|
58
|
-
- [Custom Configuration](#custom-configuration)
|
|
59
52
|
- [Authentication](#authentication)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
- [Role-Based Access Control](#role-based-access-control)
|
|
64
|
-
- [Combined Authentication](#combined-authentication)
|
|
53
|
+
- [Input Validation](#input-validation)
|
|
54
|
+
- [Audit Logging](#audit-logging)
|
|
55
|
+
- [Bot Detection](#bot-detection)
|
|
65
56
|
- [Utilities](#utilities)
|
|
66
57
|
- [API Reference](#api-reference)
|
|
67
58
|
- [Examples](#examples)
|
|
68
59
|
- [Roadmap](#roadmap)
|
|
69
60
|
- [Contributing](#contributing)
|
|
70
61
|
|
|
62
|
+
---
|
|
63
|
+
|
|
71
64
|
## Quick Start
|
|
72
65
|
|
|
73
66
|
### Protect an API Route
|
|
@@ -81,48 +74,53 @@ export const GET = withRateLimit(
|
|
|
81
74
|
const posts = await db.posts.findMany()
|
|
82
75
|
return Response.json(posts)
|
|
83
76
|
},
|
|
84
|
-
{
|
|
85
|
-
limit: 100, // 100 requests
|
|
86
|
-
window: '15m' // per 15 minutes
|
|
87
|
-
}
|
|
77
|
+
{ limit: 100, window: '15m' }
|
|
88
78
|
)
|
|
89
79
|
```
|
|
90
80
|
|
|
91
|
-
###
|
|
81
|
+
### Full Security Stack
|
|
92
82
|
|
|
93
83
|
```typescript
|
|
94
|
-
//
|
|
95
|
-
import {
|
|
96
|
-
|
|
97
|
-
export const apiLimiter = createRateLimiter({
|
|
98
|
-
limit: 100,
|
|
99
|
-
window: '15m',
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
export const strictLimiter = createRateLimiter({
|
|
103
|
-
limit: 10,
|
|
104
|
-
window: '1m',
|
|
105
|
-
})
|
|
106
|
-
```
|
|
84
|
+
// app/api/admin/users/route.ts
|
|
85
|
+
import { withRateLimit, withJWT, withRoles, withValidation, withAuditLog } from 'nextjs-secure'
|
|
86
|
+
import { MemoryStore } from 'nextjs-secure/audit'
|
|
107
87
|
|
|
108
|
-
|
|
109
|
-
// app/api/users/route.ts
|
|
110
|
-
import { apiLimiter, strictLimiter } from '@/lib/rate-limit'
|
|
88
|
+
const auditStore = new MemoryStore({ maxEntries: 1000 })
|
|
111
89
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
90
|
+
const schema = {
|
|
91
|
+
email: { type: 'email', required: true },
|
|
92
|
+
role: { type: 'string', enum: ['user', 'admin'] }
|
|
93
|
+
}
|
|
115
94
|
|
|
116
|
-
export const POST =
|
|
117
|
-
|
|
118
|
-
|
|
95
|
+
export const POST = withAuditLog(
|
|
96
|
+
withRateLimit(
|
|
97
|
+
withJWT(
|
|
98
|
+
withRoles(
|
|
99
|
+
withValidation(
|
|
100
|
+
async (req, ctx) => {
|
|
101
|
+
// ctx.user = authenticated user
|
|
102
|
+
// ctx.validated = validated body
|
|
103
|
+
return Response.json({ success: true })
|
|
104
|
+
},
|
|
105
|
+
{ body: schema }
|
|
106
|
+
),
|
|
107
|
+
{ roles: ['admin'] }
|
|
108
|
+
),
|
|
109
|
+
{ secret: process.env.JWT_SECRET }
|
|
110
|
+
),
|
|
111
|
+
{ limit: 10, window: '1m' }
|
|
112
|
+
),
|
|
113
|
+
{ store: auditStore }
|
|
114
|
+
)
|
|
119
115
|
```
|
|
120
116
|
|
|
117
|
+
---
|
|
118
|
+
|
|
121
119
|
## Rate Limiting
|
|
122
120
|
|
|
123
|
-
|
|
121
|
+
Protect your APIs from abuse with configurable rate limiting.
|
|
124
122
|
|
|
125
|
-
|
|
123
|
+
### Basic Usage
|
|
126
124
|
|
|
127
125
|
```typescript
|
|
128
126
|
import { withRateLimit } from 'nextjs-secure'
|
|
@@ -144,29 +142,18 @@ export const GET = withRateLimit(handler, {
|
|
|
144
142
|
### Algorithms
|
|
145
143
|
|
|
146
144
|
#### Sliding Window (Default)
|
|
147
|
-
|
|
148
|
-
Prevents request bursts at window boundaries. Uses weighted counting between current and previous windows.
|
|
145
|
+
Prevents request bursts at window boundaries.
|
|
149
146
|
|
|
150
147
|
```typescript
|
|
151
148
|
export const GET = withRateLimit(handler, {
|
|
152
149
|
limit: 100,
|
|
153
150
|
window: '15m',
|
|
154
|
-
algorithm: 'sliding-window',
|
|
151
|
+
algorithm: 'sliding-window',
|
|
155
152
|
})
|
|
156
153
|
```
|
|
157
154
|
|
|
158
|
-
**How it works:**
|
|
159
|
-
```
|
|
160
|
-
Window 1: |----[80 requests]-----|
|
|
161
|
-
Window 2: |--[30 requests]-------|
|
|
162
|
-
^ 50% through window 2
|
|
163
|
-
|
|
164
|
-
Weighted count = 30 + (80 × 0.5) = 70 requests
|
|
165
|
-
```
|
|
166
|
-
|
|
167
155
|
#### Fixed Window
|
|
168
|
-
|
|
169
|
-
Simple counter that resets at fixed intervals. Lower memory usage but allows bursts at boundaries.
|
|
156
|
+
Simple counter that resets at fixed intervals.
|
|
170
157
|
|
|
171
158
|
```typescript
|
|
172
159
|
export const GET = withRateLimit(handler, {
|
|
@@ -176,83 +163,45 @@ export const GET = withRateLimit(handler, {
|
|
|
176
163
|
})
|
|
177
164
|
```
|
|
178
165
|
|
|
179
|
-
**Burst scenario:**
|
|
180
|
-
```
|
|
181
|
-
Window 1: |------------------[100]| <- 100 requests at :59
|
|
182
|
-
Window 2: |[100]------------------| <- 100 requests at :00
|
|
183
|
-
200 requests in 2 seconds!
|
|
184
|
-
```
|
|
185
|
-
|
|
186
166
|
#### Token Bucket
|
|
187
|
-
|
|
188
|
-
Allows controlled bursts while maintaining average rate. Tokens refill continuously.
|
|
167
|
+
Allows controlled bursts while maintaining average rate.
|
|
189
168
|
|
|
190
169
|
```typescript
|
|
191
170
|
export const GET = withRateLimit(handler, {
|
|
192
|
-
limit: 100,
|
|
193
|
-
window: '1m',
|
|
171
|
+
limit: 100,
|
|
172
|
+
window: '1m',
|
|
194
173
|
algorithm: 'token-bucket',
|
|
195
174
|
})
|
|
196
175
|
```
|
|
197
176
|
|
|
198
|
-
**Use case:** APIs where occasional bursts are acceptable but average rate must be controlled.
|
|
199
|
-
|
|
200
177
|
### Storage Backends
|
|
201
178
|
|
|
202
179
|
#### Memory Store (Default)
|
|
203
180
|
|
|
204
|
-
Built-in, zero-config. Perfect for development and single-instance deployments.
|
|
205
|
-
|
|
206
181
|
```typescript
|
|
207
182
|
import { withRateLimit, MemoryStore } from 'nextjs-secure'
|
|
208
183
|
|
|
209
184
|
const store = new MemoryStore({
|
|
210
|
-
cleanupInterval: 60000,
|
|
211
|
-
maxKeys: 10000,
|
|
185
|
+
cleanupInterval: 60000,
|
|
186
|
+
maxKeys: 10000,
|
|
212
187
|
})
|
|
213
188
|
|
|
214
|
-
export const GET = withRateLimit(handler, {
|
|
215
|
-
limit: 100,
|
|
216
|
-
window: '15m',
|
|
217
|
-
store,
|
|
218
|
-
})
|
|
189
|
+
export const GET = withRateLimit(handler, { limit: 100, window: '15m', store })
|
|
219
190
|
```
|
|
220
191
|
|
|
221
|
-
**Limitations:**
|
|
222
|
-
- Data lost on restart
|
|
223
|
-
- Not shared between instances
|
|
224
|
-
- Not suitable for serverless (cold starts)
|
|
225
|
-
|
|
226
192
|
#### Redis Store
|
|
227
193
|
|
|
228
|
-
For distributed deployments. Works with ioredis, node-redis, or any compatible client.
|
|
229
|
-
|
|
230
194
|
```typescript
|
|
231
195
|
import Redis from 'ioredis'
|
|
232
196
|
import { withRateLimit, createRedisStore } from 'nextjs-secure/rate-limit'
|
|
233
197
|
|
|
234
198
|
const redis = new Redis(process.env.REDIS_URL)
|
|
199
|
+
const store = createRedisStore({ client: redis, prefix: 'myapp:rl' })
|
|
235
200
|
|
|
236
|
-
const
|
|
237
|
-
client: redis,
|
|
238
|
-
prefix: 'myapp:rl', // Key prefix
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
export const GET = withRateLimit(handler, {
|
|
242
|
-
limit: 100,
|
|
243
|
-
window: '15m',
|
|
244
|
-
store,
|
|
245
|
-
})
|
|
201
|
+
export const GET = withRateLimit(handler, { limit: 100, window: '15m', store })
|
|
246
202
|
```
|
|
247
203
|
|
|
248
|
-
|
|
249
|
-
- Atomic operations via Lua scripts
|
|
250
|
-
- Automatic key expiration
|
|
251
|
-
- Cluster-ready
|
|
252
|
-
|
|
253
|
-
#### Upstash Store
|
|
254
|
-
|
|
255
|
-
Optimized for serverless and edge. Uses HTTP-based Redis.
|
|
204
|
+
#### Upstash Store (Edge/Serverless)
|
|
256
205
|
|
|
257
206
|
```typescript
|
|
258
207
|
import { withRateLimit, createUpstashStore } from 'nextjs-secure/rate-limit'
|
|
@@ -262,41 +211,20 @@ const store = createUpstashStore({
|
|
|
262
211
|
token: process.env.UPSTASH_REDIS_REST_TOKEN,
|
|
263
212
|
})
|
|
264
213
|
|
|
265
|
-
export const GET = withRateLimit(handler, {
|
|
266
|
-
limit: 100,
|
|
267
|
-
window: '15m',
|
|
268
|
-
store,
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
// Or from environment variables
|
|
272
|
-
import { createUpstashStoreFromEnv } from 'nextjs-secure/rate-limit'
|
|
273
|
-
const store = createUpstashStoreFromEnv()
|
|
214
|
+
export const GET = withRateLimit(handler, { limit: 100, window: '15m', store })
|
|
274
215
|
```
|
|
275
216
|
|
|
276
|
-
**Benefits:**
|
|
277
|
-
- No TCP connections
|
|
278
|
-
- Works on Edge Runtime
|
|
279
|
-
- Global distribution support
|
|
280
|
-
|
|
281
217
|
### Custom Identifiers
|
|
282
218
|
|
|
283
|
-
By default, rate limiting is per-IP. Customize with the `identifier` option:
|
|
284
|
-
|
|
285
|
-
#### By API Key
|
|
286
|
-
|
|
287
219
|
```typescript
|
|
220
|
+
// By API Key
|
|
288
221
|
export const GET = withRateLimit(handler, {
|
|
289
222
|
limit: 1000,
|
|
290
223
|
window: '1h',
|
|
291
|
-
identifier: (req) =>
|
|
292
|
-
return req.headers.get('x-api-key') ?? 'anonymous'
|
|
293
|
-
},
|
|
224
|
+
identifier: (req) => req.headers.get('x-api-key') ?? 'anonymous',
|
|
294
225
|
})
|
|
295
|
-
```
|
|
296
226
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
```typescript
|
|
227
|
+
// By User ID
|
|
300
228
|
export const GET = withRateLimit(handler, {
|
|
301
229
|
limit: 100,
|
|
302
230
|
window: '15m',
|
|
@@ -307,104 +235,20 @@ export const GET = withRateLimit(handler, {
|
|
|
307
235
|
})
|
|
308
236
|
```
|
|
309
237
|
|
|
310
|
-
#### By Route + IP
|
|
311
|
-
|
|
312
|
-
```typescript
|
|
313
|
-
export const GET = withRateLimit(handler, {
|
|
314
|
-
limit: 100,
|
|
315
|
-
window: '15m',
|
|
316
|
-
identifier: (req) => {
|
|
317
|
-
const ip = req.headers.get('x-forwarded-for') ?? '127.0.0.1'
|
|
318
|
-
return `${req.nextUrl.pathname}:${ip}`
|
|
319
|
-
},
|
|
320
|
-
})
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
### Response Customization
|
|
324
|
-
|
|
325
|
-
#### Custom Error Response
|
|
326
|
-
|
|
327
|
-
```typescript
|
|
328
|
-
export const GET = withRateLimit(handler, {
|
|
329
|
-
limit: 100,
|
|
330
|
-
window: '15m',
|
|
331
|
-
onLimit: (req, info) => {
|
|
332
|
-
return Response.json(
|
|
333
|
-
{
|
|
334
|
-
error: 'rate_limit_exceeded',
|
|
335
|
-
message: `Too many requests. Try again in ${info.retryAfter} seconds.`,
|
|
336
|
-
limit: info.limit,
|
|
337
|
-
reset: new Date(info.reset * 1000).toISOString(),
|
|
338
|
-
},
|
|
339
|
-
{ status: 429 }
|
|
340
|
-
)
|
|
341
|
-
},
|
|
342
|
-
})
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
#### Skip Certain Requests
|
|
346
|
-
|
|
347
|
-
```typescript
|
|
348
|
-
export const GET = withRateLimit(handler, {
|
|
349
|
-
limit: 100,
|
|
350
|
-
window: '15m',
|
|
351
|
-
skip: (req) => {
|
|
352
|
-
// Skip for internal services
|
|
353
|
-
const key = req.headers.get('x-internal-key')
|
|
354
|
-
return key === process.env.INTERNAL_API_KEY
|
|
355
|
-
},
|
|
356
|
-
})
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
#### Disable Headers
|
|
360
|
-
|
|
361
|
-
```typescript
|
|
362
|
-
export const GET = withRateLimit(handler, {
|
|
363
|
-
limit: 100,
|
|
364
|
-
window: '15m',
|
|
365
|
-
headers: false, // Don't add X-RateLimit-* headers
|
|
366
|
-
})
|
|
367
|
-
```
|
|
368
|
-
|
|
369
238
|
### Response Headers
|
|
370
239
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
|
374
|
-
|
|
375
|
-
| `X-RateLimit-
|
|
376
|
-
| `
|
|
377
|
-
| `X-RateLimit-Reset` | Unix timestamp when limit resets | `1699999999` |
|
|
378
|
-
| `Retry-After` | Seconds until retry (only on 429) | `60` |
|
|
379
|
-
|
|
380
|
-
### Manual Rate Limit Check
|
|
381
|
-
|
|
382
|
-
For existing handlers or complex logic:
|
|
383
|
-
|
|
384
|
-
```typescript
|
|
385
|
-
import { checkRateLimit } from 'nextjs-secure'
|
|
386
|
-
|
|
387
|
-
export async function GET(request: NextRequest) {
|
|
388
|
-
const { success, info, headers } = await checkRateLimit(request, {
|
|
389
|
-
limit: 100,
|
|
390
|
-
window: '15m',
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
if (!success) {
|
|
394
|
-
return Response.json(
|
|
395
|
-
{ error: 'Rate limited' },
|
|
396
|
-
{ status: 429, headers }
|
|
397
|
-
)
|
|
398
|
-
}
|
|
240
|
+
| Header | Description |
|
|
241
|
+
|--------|-------------|
|
|
242
|
+
| `X-RateLimit-Limit` | Maximum requests allowed |
|
|
243
|
+
| `X-RateLimit-Remaining` | Requests remaining |
|
|
244
|
+
| `X-RateLimit-Reset` | Unix timestamp when limit resets |
|
|
245
|
+
| `Retry-After` | Seconds until retry (only on 429) |
|
|
399
246
|
|
|
400
|
-
|
|
401
|
-
return Response.json({ data: '...' }, { headers })
|
|
402
|
-
}
|
|
403
|
-
```
|
|
247
|
+
---
|
|
404
248
|
|
|
405
249
|
## CSRF Protection
|
|
406
250
|
|
|
407
|
-
Protect
|
|
251
|
+
Protect forms against Cross-Site Request Forgery attacks.
|
|
408
252
|
|
|
409
253
|
### Basic Setup
|
|
410
254
|
|
|
@@ -414,7 +258,6 @@ import { generateCSRF } from 'nextjs-secure/csrf'
|
|
|
414
258
|
|
|
415
259
|
export async function GET() {
|
|
416
260
|
const { token, cookieHeader } = await generateCSRF()
|
|
417
|
-
|
|
418
261
|
return Response.json(
|
|
419
262
|
{ csrfToken: token },
|
|
420
263
|
{ headers: { 'Set-Cookie': cookieHeader } }
|
|
@@ -428,7 +271,6 @@ import { withCSRF } from 'nextjs-secure/csrf'
|
|
|
428
271
|
|
|
429
272
|
export const POST = withCSRF(async (req) => {
|
|
430
273
|
const data = await req.json()
|
|
431
|
-
// Safe to process - CSRF validated
|
|
432
274
|
return Response.json({ success: true })
|
|
433
275
|
})
|
|
434
276
|
```
|
|
@@ -444,111 +286,51 @@ fetch('/api/submit', {
|
|
|
444
286
|
method: 'POST',
|
|
445
287
|
headers: {
|
|
446
288
|
'Content-Type': 'application/json',
|
|
447
|
-
'x-csrf-token': csrfToken
|
|
289
|
+
'x-csrf-token': csrfToken
|
|
448
290
|
},
|
|
449
291
|
body: JSON.stringify({ data: '...' })
|
|
450
292
|
})
|
|
451
293
|
```
|
|
452
294
|
|
|
453
|
-
Or include in form body:
|
|
454
|
-
|
|
455
|
-
```typescript
|
|
456
|
-
fetch('/api/submit', {
|
|
457
|
-
method: 'POST',
|
|
458
|
-
headers: { 'Content-Type': 'application/json' },
|
|
459
|
-
body: JSON.stringify({
|
|
460
|
-
_csrf: csrfToken, // Token in body
|
|
461
|
-
data: '...'
|
|
462
|
-
})
|
|
463
|
-
})
|
|
464
|
-
```
|
|
465
|
-
|
|
466
295
|
### Configuration
|
|
467
296
|
|
|
468
297
|
```typescript
|
|
469
|
-
import { withCSRF } from 'nextjs-secure/csrf'
|
|
470
|
-
|
|
471
298
|
export const POST = withCSRF(handler, {
|
|
472
|
-
// Cookie settings
|
|
473
299
|
cookie: {
|
|
474
|
-
name: '__csrf',
|
|
475
|
-
httpOnly: true,
|
|
476
|
-
secure: true,
|
|
477
|
-
sameSite: 'strict',
|
|
478
|
-
maxAge: 86400
|
|
300
|
+
name: '__csrf',
|
|
301
|
+
httpOnly: true,
|
|
302
|
+
secure: true,
|
|
303
|
+
sameSite: 'strict',
|
|
304
|
+
maxAge: 86400
|
|
479
305
|
},
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
fieldName: '_csrf', // Body field name
|
|
484
|
-
|
|
485
|
-
// Token settings
|
|
486
|
-
secret: process.env.CSRF_SECRET, // Signing secret
|
|
487
|
-
tokenLength: 32, // Token size in bytes
|
|
488
|
-
|
|
489
|
-
// Protected methods (default: POST, PUT, PATCH, DELETE)
|
|
490
|
-
protectedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],
|
|
491
|
-
|
|
492
|
-
// Skip protection conditionally
|
|
306
|
+
headerName: 'x-csrf-token',
|
|
307
|
+
fieldName: '_csrf',
|
|
308
|
+
secret: process.env.CSRF_SECRET,
|
|
493
309
|
skip: (req) => req.headers.get('x-api-key') === 'trusted',
|
|
494
|
-
|
|
495
|
-
// Custom error response
|
|
496
|
-
onError: (req, reason) => {
|
|
497
|
-
return new Response(`CSRF failed: ${reason}`, { status: 403 })
|
|
498
|
-
}
|
|
499
310
|
})
|
|
500
311
|
```
|
|
501
312
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
```typescript
|
|
505
|
-
import { validateCSRF } from 'nextjs-secure/csrf'
|
|
506
|
-
|
|
507
|
-
export async function POST(req) {
|
|
508
|
-
const result = await validateCSRF(req)
|
|
509
|
-
|
|
510
|
-
if (!result.valid) {
|
|
511
|
-
console.log('CSRF failed:', result.reason)
|
|
512
|
-
// reason: 'missing_cookie' | 'invalid_cookie' | 'missing_token' | 'token_mismatch'
|
|
513
|
-
return Response.json({ error: result.reason }, { status: 403 })
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// Continue processing
|
|
517
|
-
}
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
### Environment Variable
|
|
521
|
-
|
|
522
|
-
Set `CSRF_SECRET` in your environment:
|
|
523
|
-
|
|
524
|
-
```env
|
|
525
|
-
CSRF_SECRET=your-secret-key-min-32-chars-recommended
|
|
526
|
-
```
|
|
313
|
+
---
|
|
527
314
|
|
|
528
315
|
## Security Headers
|
|
529
316
|
|
|
530
|
-
Add security headers to
|
|
317
|
+
Add security headers to protect against common attacks.
|
|
531
318
|
|
|
532
319
|
### Quick Start
|
|
533
320
|
|
|
534
321
|
```typescript
|
|
535
322
|
import { withSecurityHeaders } from 'nextjs-secure/headers'
|
|
536
323
|
|
|
537
|
-
|
|
538
|
-
export const GET = withSecurityHeaders(async (req) => {
|
|
539
|
-
return Response.json({ data: 'protected' })
|
|
540
|
-
})
|
|
324
|
+
export const GET = withSecurityHeaders(handler)
|
|
541
325
|
```
|
|
542
326
|
|
|
543
327
|
### Presets
|
|
544
328
|
|
|
545
|
-
Three presets available: `strict`, `relaxed`, `api`
|
|
546
|
-
|
|
547
329
|
```typescript
|
|
548
330
|
// Strict: Maximum security (default)
|
|
549
331
|
export const GET = withSecurityHeaders(handler, { preset: 'strict' })
|
|
550
332
|
|
|
551
|
-
// Relaxed: Development-friendly
|
|
333
|
+
// Relaxed: Development-friendly
|
|
552
334
|
export const GET = withSecurityHeaders(handler, { preset: 'relaxed' })
|
|
553
335
|
|
|
554
336
|
// API: Optimized for JSON APIs
|
|
@@ -558,36 +340,22 @@ export const GET = withSecurityHeaders(handler, { preset: 'api' })
|
|
|
558
340
|
### Custom Configuration
|
|
559
341
|
|
|
560
342
|
```typescript
|
|
561
|
-
import { withSecurityHeaders } from 'nextjs-secure/headers'
|
|
562
|
-
|
|
563
343
|
export const GET = withSecurityHeaders(handler, {
|
|
564
344
|
config: {
|
|
565
|
-
// Content-Security-Policy
|
|
566
345
|
contentSecurityPolicy: {
|
|
567
346
|
defaultSrc: ["'self'"],
|
|
568
347
|
scriptSrc: ["'self'", "'unsafe-inline'"],
|
|
569
348
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
570
349
|
imgSrc: ["'self'", 'data:', 'https:'],
|
|
571
350
|
},
|
|
572
|
-
|
|
573
|
-
// Strict-Transport-Security
|
|
574
351
|
strictTransportSecurity: {
|
|
575
352
|
maxAge: 31536000,
|
|
576
353
|
includeSubDomains: true,
|
|
577
354
|
preload: true,
|
|
578
355
|
},
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
xFrameOptions: 'DENY', // or 'SAMEORIGIN'
|
|
582
|
-
xContentTypeOptions: true, // X-Content-Type-Options: nosniff
|
|
356
|
+
xFrameOptions: 'DENY',
|
|
357
|
+
xContentTypeOptions: true,
|
|
583
358
|
referrerPolicy: 'strict-origin-when-cross-origin',
|
|
584
|
-
|
|
585
|
-
// Cross-Origin headers
|
|
586
|
-
crossOriginOpenerPolicy: 'same-origin',
|
|
587
|
-
crossOriginEmbedderPolicy: 'require-corp',
|
|
588
|
-
crossOriginResourcePolicy: 'same-origin',
|
|
589
|
-
|
|
590
|
-
// Permissions-Policy (disable features)
|
|
591
359
|
permissionsPolicy: {
|
|
592
360
|
camera: [],
|
|
593
361
|
microphone: [],
|
|
@@ -597,31 +365,6 @@ export const GET = withSecurityHeaders(handler, {
|
|
|
597
365
|
})
|
|
598
366
|
```
|
|
599
367
|
|
|
600
|
-
### Disable Specific Headers
|
|
601
|
-
|
|
602
|
-
```typescript
|
|
603
|
-
export const GET = withSecurityHeaders(handler, {
|
|
604
|
-
config: {
|
|
605
|
-
contentSecurityPolicy: false, // Disable CSP
|
|
606
|
-
xFrameOptions: false, // Disable X-Frame-Options
|
|
607
|
-
}
|
|
608
|
-
})
|
|
609
|
-
```
|
|
610
|
-
|
|
611
|
-
### Manual Header Creation
|
|
612
|
-
|
|
613
|
-
```typescript
|
|
614
|
-
import { createSecurityHeaders } from 'nextjs-secure/headers'
|
|
615
|
-
|
|
616
|
-
export async function GET() {
|
|
617
|
-
const headers = createSecurityHeaders({ preset: 'api' })
|
|
618
|
-
|
|
619
|
-
return new Response(JSON.stringify({ ok: true }), {
|
|
620
|
-
headers,
|
|
621
|
-
})
|
|
622
|
-
}
|
|
623
|
-
```
|
|
624
|
-
|
|
625
368
|
### Available Headers
|
|
626
369
|
|
|
627
370
|
| Header | Description |
|
|
@@ -636,9 +379,11 @@ export async function GET() {
|
|
|
636
379
|
| Cross-Origin-Embedder-Policy | Controls embedding |
|
|
637
380
|
| Cross-Origin-Resource-Policy | Controls resource sharing |
|
|
638
381
|
|
|
382
|
+
---
|
|
383
|
+
|
|
639
384
|
## Authentication
|
|
640
385
|
|
|
641
|
-
Flexible authentication
|
|
386
|
+
Flexible authentication supporting JWT, API keys, sessions, and RBAC.
|
|
642
387
|
|
|
643
388
|
### JWT Authentication
|
|
644
389
|
|
|
@@ -647,52 +392,17 @@ import { withJWT } from 'nextjs-secure/auth'
|
|
|
647
392
|
|
|
648
393
|
export const GET = withJWT(
|
|
649
394
|
async (req, ctx) => {
|
|
650
|
-
// ctx.user contains the authenticated user
|
|
651
395
|
return Response.json({ user: ctx.user })
|
|
652
396
|
},
|
|
653
397
|
{
|
|
654
398
|
secret: process.env.JWT_SECRET,
|
|
655
|
-
|
|
399
|
+
algorithms: ['HS256', 'RS256'],
|
|
400
|
+
issuer: 'https://myapp.com',
|
|
401
|
+
audience: 'my-api',
|
|
656
402
|
}
|
|
657
403
|
)
|
|
658
404
|
```
|
|
659
405
|
|
|
660
|
-
#### Configuration
|
|
661
|
-
|
|
662
|
-
```typescript
|
|
663
|
-
export const GET = withJWT(handler, {
|
|
664
|
-
// Secret for HMAC algorithms (HS256, HS384, HS512)
|
|
665
|
-
secret: process.env.JWT_SECRET,
|
|
666
|
-
|
|
667
|
-
// Public key for RSA/ECDSA (RS256, ES256, etc.)
|
|
668
|
-
publicKey: process.env.JWT_PUBLIC_KEY,
|
|
669
|
-
|
|
670
|
-
// Allowed algorithms (default: ['HS256'])
|
|
671
|
-
algorithms: ['HS256', 'RS256'],
|
|
672
|
-
|
|
673
|
-
// Validate issuer
|
|
674
|
-
issuer: 'https://myapp.com',
|
|
675
|
-
// or multiple issuers
|
|
676
|
-
issuer: ['https://auth.myapp.com', 'https://api.myapp.com'],
|
|
677
|
-
|
|
678
|
-
// Validate audience
|
|
679
|
-
audience: 'my-api',
|
|
680
|
-
|
|
681
|
-
// Clock tolerance in seconds (for exp/nbf claims)
|
|
682
|
-
clockTolerance: 30,
|
|
683
|
-
|
|
684
|
-
// Custom token extraction
|
|
685
|
-
getToken: (req) => req.headers.get('x-auth-token'),
|
|
686
|
-
|
|
687
|
-
// Custom user mapping from JWT payload
|
|
688
|
-
mapUser: (payload) => ({
|
|
689
|
-
id: payload.sub,
|
|
690
|
-
email: payload.email,
|
|
691
|
-
roles: payload.roles || [],
|
|
692
|
-
}),
|
|
693
|
-
})
|
|
694
|
-
```
|
|
695
|
-
|
|
696
406
|
### API Key Authentication
|
|
697
407
|
|
|
698
408
|
```typescript
|
|
@@ -703,42 +413,16 @@ export const GET = withAPIKey(
|
|
|
703
413
|
return Response.json({ user: ctx.user })
|
|
704
414
|
},
|
|
705
415
|
{
|
|
706
|
-
validate: async (apiKey
|
|
707
|
-
// Return user object if valid, null if invalid
|
|
416
|
+
validate: async (apiKey) => {
|
|
708
417
|
const user = await db.users.findByApiKey(apiKey)
|
|
709
418
|
return user || null
|
|
710
419
|
},
|
|
420
|
+
headerName: 'x-api-key',
|
|
421
|
+
queryParam: 'api_key',
|
|
711
422
|
}
|
|
712
423
|
)
|
|
713
424
|
```
|
|
714
425
|
|
|
715
|
-
#### Configuration
|
|
716
|
-
|
|
717
|
-
```typescript
|
|
718
|
-
export const GET = withAPIKey(handler, {
|
|
719
|
-
// Required: validation function
|
|
720
|
-
validate: async (apiKey, req) => {
|
|
721
|
-
// Lookup API key and return user or null
|
|
722
|
-
return db.apiKeys.findUser(apiKey)
|
|
723
|
-
},
|
|
724
|
-
|
|
725
|
-
// Header name (default: 'x-api-key')
|
|
726
|
-
headerName: 'x-api-key',
|
|
727
|
-
|
|
728
|
-
// Query parameter name (default: 'api_key')
|
|
729
|
-
queryParam: 'api_key',
|
|
730
|
-
})
|
|
731
|
-
```
|
|
732
|
-
|
|
733
|
-
API keys can be sent via header or query parameter:
|
|
734
|
-
```bash
|
|
735
|
-
# Via header
|
|
736
|
-
curl -H "x-api-key: YOUR_API_KEY" https://api.example.com/data
|
|
737
|
-
|
|
738
|
-
# Via query parameter
|
|
739
|
-
curl https://api.example.com/data?api_key=YOUR_API_KEY
|
|
740
|
-
```
|
|
741
|
-
|
|
742
426
|
### Session Authentication
|
|
743
427
|
|
|
744
428
|
```typescript
|
|
@@ -749,415 +433,769 @@ export const GET = withSession(
|
|
|
749
433
|
return Response.json({ user: ctx.user })
|
|
750
434
|
},
|
|
751
435
|
{
|
|
752
|
-
validate: async (sessionId
|
|
753
|
-
// Return user object if session valid, null if invalid
|
|
436
|
+
validate: async (sessionId) => {
|
|
754
437
|
const session = await db.sessions.find(sessionId)
|
|
755
438
|
return session?.user || null
|
|
756
439
|
},
|
|
440
|
+
cookieName: 'session',
|
|
757
441
|
}
|
|
758
442
|
)
|
|
759
443
|
```
|
|
760
444
|
|
|
761
|
-
#### Configuration
|
|
762
|
-
|
|
763
|
-
```typescript
|
|
764
|
-
export const GET = withSession(handler, {
|
|
765
|
-
// Required: session validation function
|
|
766
|
-
validate: async (sessionId, req) => {
|
|
767
|
-
const session = await redis.get(`session:${sessionId}`)
|
|
768
|
-
if (!session) return null
|
|
769
|
-
return JSON.parse(session)
|
|
770
|
-
},
|
|
771
|
-
|
|
772
|
-
// Cookie name (default: 'session')
|
|
773
|
-
cookieName: 'session',
|
|
774
|
-
})
|
|
775
|
-
```
|
|
776
|
-
|
|
777
445
|
### Role-Based Access Control
|
|
778
446
|
|
|
779
|
-
Use `withRoles` after an authentication middleware to enforce role/permission requirements.
|
|
780
|
-
|
|
781
447
|
```typescript
|
|
782
448
|
import { withJWT, withRoles } from 'nextjs-secure/auth'
|
|
783
449
|
|
|
784
|
-
|
|
785
|
-
const authenticatedHandler = withJWT(
|
|
450
|
+
export const GET = withJWT(
|
|
786
451
|
withRoles(
|
|
787
452
|
async (req, ctx) => {
|
|
788
453
|
return Response.json({ admin: true })
|
|
789
454
|
},
|
|
790
|
-
{
|
|
455
|
+
{
|
|
456
|
+
roles: ['admin', 'moderator'],
|
|
457
|
+
permissions: ['users:read', 'users:write'],
|
|
458
|
+
}
|
|
791
459
|
),
|
|
792
460
|
{ secret: process.env.JWT_SECRET }
|
|
793
461
|
)
|
|
794
|
-
|
|
795
|
-
export const GET = authenticatedHandler
|
|
796
|
-
```
|
|
797
|
-
|
|
798
|
-
#### Configuration
|
|
799
|
-
|
|
800
|
-
```typescript
|
|
801
|
-
withRoles(handler, {
|
|
802
|
-
// Required roles (any match = authorized)
|
|
803
|
-
roles: ['admin', 'moderator'],
|
|
804
|
-
|
|
805
|
-
// Required permissions (all must match)
|
|
806
|
-
permissions: ['read', 'write'],
|
|
807
|
-
|
|
808
|
-
// Custom role extraction
|
|
809
|
-
getUserRoles: (user) => user.roles || [],
|
|
810
|
-
|
|
811
|
-
// Custom permission extraction
|
|
812
|
-
getUserPermissions: (user) => user.permissions || [],
|
|
813
|
-
|
|
814
|
-
// Custom authorization logic
|
|
815
|
-
authorize: async (user, req) => {
|
|
816
|
-
// Return true if authorized, false otherwise
|
|
817
|
-
return user.subscriptionTier === 'pro'
|
|
818
|
-
},
|
|
819
|
-
})
|
|
820
462
|
```
|
|
821
463
|
|
|
822
464
|
### Combined Authentication
|
|
823
465
|
|
|
824
|
-
Use `withAuth` for flexible multi-strategy authentication:
|
|
825
|
-
|
|
826
466
|
```typescript
|
|
827
467
|
import { withAuth } from 'nextjs-secure/auth'
|
|
828
468
|
|
|
829
469
|
export const GET = withAuth(
|
|
830
470
|
async (req, ctx) => {
|
|
831
|
-
// Authenticated via any method
|
|
832
471
|
return Response.json({ user: ctx.user })
|
|
833
472
|
},
|
|
834
473
|
{
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
},
|
|
839
|
-
|
|
840
|
-
// Fall back to API key
|
|
841
|
-
apiKey: {
|
|
842
|
-
validate: (key) => db.apiKeys.findUser(key),
|
|
843
|
-
},
|
|
844
|
-
|
|
845
|
-
// Fall back to session
|
|
846
|
-
session: {
|
|
847
|
-
validate: (id) => db.sessions.findUser(id),
|
|
848
|
-
},
|
|
849
|
-
|
|
850
|
-
// Optional RBAC
|
|
851
|
-
rbac: {
|
|
852
|
-
roles: ['user', 'admin'],
|
|
853
|
-
},
|
|
854
|
-
|
|
855
|
-
// Callbacks
|
|
856
|
-
onSuccess: async (req, user) => {
|
|
857
|
-
// Log successful auth
|
|
858
|
-
console.log(`Authenticated: ${user.id}`)
|
|
859
|
-
},
|
|
860
|
-
|
|
861
|
-
onError: (req, error) => {
|
|
862
|
-
// Custom error response
|
|
863
|
-
return Response.json({ error: error.message }, { status: error.status })
|
|
864
|
-
},
|
|
474
|
+
jwt: { secret: process.env.JWT_SECRET },
|
|
475
|
+
apiKey: { validate: (key) => db.apiKeys.findUser(key) },
|
|
476
|
+
session: { validate: (id) => db.sessions.findUser(id) },
|
|
477
|
+
rbac: { roles: ['user', 'admin'] },
|
|
865
478
|
}
|
|
866
479
|
)
|
|
867
480
|
```
|
|
868
481
|
|
|
869
482
|
### Optional Authentication
|
|
870
483
|
|
|
871
|
-
For routes that work with or without authentication:
|
|
872
|
-
|
|
873
484
|
```typescript
|
|
874
485
|
import { withOptionalAuth } from 'nextjs-secure/auth'
|
|
875
486
|
|
|
876
487
|
export const GET = withOptionalAuth(
|
|
877
488
|
async (req, ctx) => {
|
|
878
489
|
if (ctx.user) {
|
|
879
|
-
// Authenticated user
|
|
880
490
|
return Response.json({ user: ctx.user })
|
|
881
491
|
}
|
|
882
|
-
// Anonymous access
|
|
883
492
|
return Response.json({ guest: true })
|
|
884
493
|
},
|
|
885
|
-
{
|
|
886
|
-
jwt: { secret: process.env.JWT_SECRET },
|
|
887
|
-
}
|
|
494
|
+
{ jwt: { secret: process.env.JWT_SECRET } }
|
|
888
495
|
)
|
|
889
496
|
```
|
|
890
497
|
|
|
891
|
-
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
## Input Validation
|
|
501
|
+
|
|
502
|
+
Validate and sanitize user input to prevent attacks.
|
|
503
|
+
|
|
504
|
+
### Schema Validation
|
|
892
505
|
|
|
893
506
|
```typescript
|
|
894
|
-
import {
|
|
507
|
+
import { withValidation } from 'nextjs-secure/validation'
|
|
508
|
+
|
|
509
|
+
// Built-in schema
|
|
510
|
+
const schema = {
|
|
511
|
+
email: { type: 'email', required: true },
|
|
512
|
+
password: { type: 'string', minLength: 8, maxLength: 100 },
|
|
513
|
+
age: { type: 'number', min: 18, max: 120 },
|
|
514
|
+
role: { type: 'string', enum: ['user', 'admin'] },
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export const POST = withValidation(handler, { body: schema })
|
|
895
518
|
|
|
896
|
-
//
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
519
|
+
// Or use Zod
|
|
520
|
+
import { z } from 'zod'
|
|
521
|
+
|
|
522
|
+
const zodSchema = z.object({
|
|
523
|
+
email: z.string().email(),
|
|
524
|
+
password: z.string().min(8),
|
|
900
525
|
})
|
|
901
526
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
527
|
+
export const POST = withValidation(handler, { body: zodSchema })
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### XSS Protection
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
import { withXSSProtection, withSanitization, sanitize, detectXSS } from 'nextjs-secure/validation'
|
|
905
534
|
|
|
906
|
-
//
|
|
907
|
-
const
|
|
908
|
-
// { header, payload, signature }
|
|
535
|
+
// Block XSS attempts
|
|
536
|
+
export const POST = withXSSProtection(handler)
|
|
909
537
|
|
|
910
|
-
//
|
|
911
|
-
const
|
|
912
|
-
|
|
538
|
+
// Sanitize specific fields
|
|
539
|
+
export const POST = withSanitization(handler, {
|
|
540
|
+
fields: ['content', 'bio'],
|
|
541
|
+
mode: 'escape', // 'escape' | 'strip' | 'allow-safe'
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
// Manual sanitization
|
|
545
|
+
const clean = sanitize(userInput, {
|
|
546
|
+
mode: 'allow-safe',
|
|
547
|
+
allowedTags: ['b', 'i', 'em', 'strong'],
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
// Detection only
|
|
551
|
+
const { hasXSS, matches } = detectXSS(input)
|
|
913
552
|
```
|
|
914
553
|
|
|
915
|
-
|
|
554
|
+
### SQL Injection Protection
|
|
916
555
|
|
|
917
|
-
|
|
556
|
+
```typescript
|
|
557
|
+
import { withSQLProtection, detectSQLInjection, hasSQLInjection } from 'nextjs-secure/validation'
|
|
558
|
+
|
|
559
|
+
// Block SQL injection
|
|
560
|
+
export const POST = withSQLProtection(handler, {
|
|
561
|
+
mode: 'block', // 'block' | 'detect'
|
|
562
|
+
minSeverity: 'medium', // 'low' | 'medium' | 'high'
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
// Manual detection
|
|
566
|
+
const result = detectSQLInjection(input)
|
|
567
|
+
// { hasSQLi: true, severity: 'high', patterns: ['UNION SELECT'] }
|
|
568
|
+
|
|
569
|
+
// Simple check
|
|
570
|
+
if (hasSQLInjection(input)) {
|
|
571
|
+
// Block request
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### Path Traversal Prevention
|
|
918
576
|
|
|
919
577
|
```typescript
|
|
920
|
-
import {
|
|
578
|
+
import { validatePath, sanitizePath, sanitizeFilename } from 'nextjs-secure/validation'
|
|
921
579
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
580
|
+
// Validate path
|
|
581
|
+
const result = validatePath(userPath, {
|
|
582
|
+
basePath: '/uploads',
|
|
583
|
+
allowedExtensions: ['.jpg', '.png'],
|
|
584
|
+
maxDepth: 3,
|
|
585
|
+
})
|
|
925
586
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
587
|
+
if (!result.valid) {
|
|
588
|
+
console.log(result.reason) // 'traversal_detected', 'invalid_extension', etc.
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Sanitize
|
|
592
|
+
const safePath = sanitizePath('../../../etc/passwd') // 'etc/passwd'
|
|
593
|
+
const safeFilename = sanitizeFilename('../../evil.exe') // 'evil.exe'
|
|
929
594
|
```
|
|
930
595
|
|
|
931
|
-
###
|
|
596
|
+
### File Validation
|
|
932
597
|
|
|
933
598
|
```typescript
|
|
934
|
-
import {
|
|
599
|
+
import { withFileValidation, validateFile } from 'nextjs-secure/validation'
|
|
935
600
|
|
|
936
|
-
|
|
937
|
-
|
|
601
|
+
export const POST = withFileValidation(handler, {
|
|
602
|
+
maxSize: 5 * 1024 * 1024, // 5MB
|
|
603
|
+
allowedTypes: ['image/jpeg', 'image/png', 'application/pdf'],
|
|
604
|
+
validateMagicNumbers: true, // Check actual file content
|
|
605
|
+
maxFiles: 10,
|
|
606
|
+
})
|
|
938
607
|
|
|
939
|
-
//
|
|
940
|
-
const
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
fallback: '0.0.0.0',
|
|
608
|
+
// Manual validation
|
|
609
|
+
const result = await validateFile(file, {
|
|
610
|
+
maxSize: 5 * 1024 * 1024,
|
|
611
|
+
allowedTypes: ['image/jpeg'],
|
|
944
612
|
})
|
|
613
|
+
```
|
|
945
614
|
|
|
946
|
-
|
|
947
|
-
anonymizeIp('192.168.1.100') // '192.168.1.xxx'
|
|
615
|
+
### Combined Security Validation
|
|
948
616
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
617
|
+
```typescript
|
|
618
|
+
import { withSecureValidation } from 'nextjs-secure/validation'
|
|
619
|
+
|
|
620
|
+
export const POST = withSecureValidation(handler, {
|
|
621
|
+
xss: true,
|
|
622
|
+
sql: { minSeverity: 'medium' },
|
|
623
|
+
contentType: ['application/json'],
|
|
624
|
+
})
|
|
952
625
|
```
|
|
953
626
|
|
|
954
|
-
|
|
627
|
+
---
|
|
955
628
|
|
|
956
|
-
|
|
629
|
+
## Audit Logging
|
|
957
630
|
|
|
958
|
-
|
|
631
|
+
Track requests and security events for monitoring and compliance.
|
|
632
|
+
|
|
633
|
+
### Request Logging
|
|
959
634
|
|
|
960
635
|
```typescript
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
store
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
636
|
+
import { withAuditLog, MemoryStore, ConsoleStore } from 'nextjs-secure/audit'
|
|
637
|
+
|
|
638
|
+
const store = new MemoryStore({ maxEntries: 1000 })
|
|
639
|
+
|
|
640
|
+
export const POST = withAuditLog(handler, {
|
|
641
|
+
store,
|
|
642
|
+
include: {
|
|
643
|
+
ip: true,
|
|
644
|
+
userAgent: true,
|
|
645
|
+
headers: false,
|
|
646
|
+
query: true,
|
|
647
|
+
response: true,
|
|
648
|
+
duration: true,
|
|
649
|
+
},
|
|
650
|
+
exclude: {
|
|
651
|
+
paths: ['/health', '/metrics'],
|
|
652
|
+
methods: ['OPTIONS'],
|
|
653
|
+
statusCodes: [304],
|
|
654
|
+
},
|
|
655
|
+
pii: {
|
|
656
|
+
fields: ['password', 'token', 'ssn', 'creditCard'],
|
|
657
|
+
mode: 'mask', // 'mask' | 'hash' | 'remove'
|
|
658
|
+
},
|
|
659
|
+
})
|
|
974
660
|
```
|
|
975
661
|
|
|
976
|
-
###
|
|
662
|
+
### Storage Backends
|
|
663
|
+
|
|
664
|
+
```typescript
|
|
665
|
+
import { MemoryStore, ConsoleStore, createDatadogStore, MultiStore } from 'nextjs-secure/audit'
|
|
666
|
+
|
|
667
|
+
// Memory (development)
|
|
668
|
+
const memoryStore = new MemoryStore({ maxEntries: 1000, ttl: 3600000 })
|
|
669
|
+
|
|
670
|
+
// Console (development)
|
|
671
|
+
const consoleStore = new ConsoleStore({ colorize: true, level: 'info' })
|
|
977
672
|
|
|
978
|
-
|
|
673
|
+
// Datadog (production)
|
|
674
|
+
const datadogStore = createDatadogStore({
|
|
675
|
+
apiKey: process.env.DATADOG_API_KEY,
|
|
676
|
+
service: 'my-api',
|
|
677
|
+
environment: 'production',
|
|
678
|
+
})
|
|
979
679
|
|
|
980
|
-
|
|
680
|
+
// Multiple stores
|
|
681
|
+
const multiStore = new MultiStore([consoleStore, datadogStore])
|
|
682
|
+
```
|
|
981
683
|
|
|
982
|
-
|
|
684
|
+
### Security Event Tracking
|
|
983
685
|
|
|
984
|
-
Returns:
|
|
985
686
|
```typescript
|
|
986
|
-
{
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
687
|
+
import { createSecurityTracker, trackSecurityEvent } from 'nextjs-secure/audit'
|
|
688
|
+
|
|
689
|
+
const tracker = createSecurityTracker({ store })
|
|
690
|
+
|
|
691
|
+
// Authentication failures
|
|
692
|
+
await tracker.authFailed({
|
|
693
|
+
ip: '192.168.1.1',
|
|
694
|
+
email: 'user@example.com',
|
|
695
|
+
reason: 'Invalid password',
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
// Rate limit exceeded
|
|
699
|
+
await tracker.rateLimitExceeded({
|
|
700
|
+
ip: '192.168.1.1',
|
|
701
|
+
endpoint: '/api/login',
|
|
702
|
+
limit: 10,
|
|
703
|
+
window: '15m',
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
// XSS detected
|
|
707
|
+
await tracker.xssDetected({
|
|
708
|
+
ip: '192.168.1.1',
|
|
709
|
+
field: 'comment',
|
|
710
|
+
endpoint: '/api/comments',
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
// SQL injection detected
|
|
714
|
+
await tracker.sqliDetected({
|
|
715
|
+
ip: '192.168.1.1',
|
|
716
|
+
field: 'username',
|
|
717
|
+
pattern: 'UNION SELECT',
|
|
718
|
+
severity: 'high',
|
|
719
|
+
endpoint: '/api/users',
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
// CSRF validation failure
|
|
723
|
+
await tracker.csrfInvalid({
|
|
724
|
+
ip: '192.168.1.1',
|
|
725
|
+
endpoint: '/api/transfer',
|
|
726
|
+
reason: 'Token mismatch',
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
// IP blocked
|
|
730
|
+
await tracker.ipBlocked({
|
|
731
|
+
ip: '192.168.1.1',
|
|
732
|
+
reason: 'Too many failed attempts',
|
|
733
|
+
duration: 3600,
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
// Custom events
|
|
737
|
+
await tracker.custom({
|
|
738
|
+
message: 'Suspicious activity detected',
|
|
739
|
+
severity: 'high',
|
|
740
|
+
details: { pattern: 'automated_scanning' },
|
|
741
|
+
})
|
|
992
742
|
```
|
|
993
743
|
|
|
994
|
-
###
|
|
744
|
+
### PII Redaction
|
|
995
745
|
|
|
996
746
|
```typescript
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
}
|
|
747
|
+
import { redactObject, redactEmail, redactCreditCard, redactIP, DEFAULT_PII_FIELDS } from 'nextjs-secure/audit'
|
|
748
|
+
|
|
749
|
+
// Redact object
|
|
750
|
+
const safeData = redactObject(userData, {
|
|
751
|
+
fields: DEFAULT_PII_FIELDS,
|
|
752
|
+
mode: 'mask',
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
// Specific redactors
|
|
756
|
+
redactEmail('john@example.com') // '****@example.com'
|
|
757
|
+
redactCreditCard('4111111111111111') // '**** **** **** 1111'
|
|
758
|
+
redactIP('192.168.1.100') // '192.168.*.*'
|
|
1004
759
|
```
|
|
1005
760
|
|
|
1006
|
-
|
|
761
|
+
### Request ID & Timing
|
|
762
|
+
|
|
763
|
+
```typescript
|
|
764
|
+
import { withRequestId, withTiming } from 'nextjs-secure/audit'
|
|
765
|
+
|
|
766
|
+
// Add request ID to responses
|
|
767
|
+
export const GET = withRequestId(handler, {
|
|
768
|
+
headerName: 'x-request-id',
|
|
769
|
+
generateId: () => `req_${Date.now()}`,
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
// Add response timing
|
|
773
|
+
export const GET = withTiming(handler, {
|
|
774
|
+
headerName: 'x-response-time',
|
|
775
|
+
log: true,
|
|
776
|
+
})
|
|
777
|
+
```
|
|
1007
778
|
|
|
1008
|
-
###
|
|
779
|
+
### Log Formatters
|
|
1009
780
|
|
|
1010
781
|
```typescript
|
|
1011
|
-
|
|
1012
|
-
import { withRateLimit } from 'nextjs-secure'
|
|
782
|
+
import { JSONFormatter, TextFormatter, CLFFormatter, StructuredFormatter } from 'nextjs-secure/audit'
|
|
1013
783
|
|
|
1014
|
-
//
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
784
|
+
// JSON (default)
|
|
785
|
+
const jsonFormatter = new JSONFormatter({ pretty: true })
|
|
786
|
+
|
|
787
|
+
// Human-readable text
|
|
788
|
+
const textFormatter = new TextFormatter({
|
|
789
|
+
template: '{timestamp} [{level}] {message}',
|
|
1018
790
|
})
|
|
1019
791
|
|
|
1020
|
-
//
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
792
|
+
// Apache/Nginx Common Log Format
|
|
793
|
+
const clfFormatter = new CLFFormatter()
|
|
794
|
+
|
|
795
|
+
// Key=value (ELK/Splunk)
|
|
796
|
+
const structuredFormatter = new StructuredFormatter({
|
|
797
|
+
delimiter: ' ',
|
|
798
|
+
kvSeparator: '=',
|
|
1024
799
|
})
|
|
800
|
+
```
|
|
1025
801
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
802
|
+
---
|
|
803
|
+
|
|
804
|
+
## Bot Detection
|
|
805
|
+
|
|
806
|
+
Protect your endpoints from automated bots, scrapers, and spam.
|
|
807
|
+
|
|
808
|
+
### Basic Usage
|
|
809
|
+
|
|
810
|
+
```typescript
|
|
811
|
+
import { withBotProtection } from 'nextjs-secure/bot'
|
|
812
|
+
|
|
813
|
+
export const POST = withBotProtection(handler, {
|
|
814
|
+
userAgent: {
|
|
815
|
+
blockAllBots: false,
|
|
816
|
+
allowList: ['Googlebot', 'Bingbot'],
|
|
817
|
+
},
|
|
818
|
+
honeypot: true,
|
|
819
|
+
behavior: {
|
|
820
|
+
maxRequestsPerSecond: 10,
|
|
821
|
+
},
|
|
1030
822
|
})
|
|
1031
823
|
```
|
|
1032
824
|
|
|
1033
|
-
###
|
|
825
|
+
### Presets
|
|
1034
826
|
|
|
1035
827
|
```typescript
|
|
1036
|
-
|
|
1037
|
-
import { createRateLimiter, createUpstashStore } from 'nextjs-secure/rate-limit'
|
|
828
|
+
import { withBotProtectionPreset } from 'nextjs-secure/bot'
|
|
1038
829
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
830
|
+
// Relaxed: Only blocks obvious bots
|
|
831
|
+
export const GET = withBotProtectionPreset(handler, 'relaxed')
|
|
832
|
+
|
|
833
|
+
// Standard: Good balance (default)
|
|
834
|
+
export const GET = withBotProtectionPreset(handler, 'standard')
|
|
835
|
+
|
|
836
|
+
// Strict: Maximum protection
|
|
837
|
+
export const GET = withBotProtectionPreset(handler, 'strict')
|
|
838
|
+
|
|
839
|
+
// API: Optimized for API endpoints
|
|
840
|
+
export const GET = withBotProtectionPreset(handler, 'api')
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
### User-Agent Detection
|
|
844
|
+
|
|
845
|
+
```typescript
|
|
846
|
+
import { withUserAgentProtection, analyzeUserAgent, KNOWN_BOT_PATTERNS } from 'nextjs-secure/bot'
|
|
847
|
+
|
|
848
|
+
// Middleware
|
|
849
|
+
export const GET = withUserAgentProtection(handler, {
|
|
850
|
+
blockAllBots: true,
|
|
851
|
+
allowCategories: ['search_engine', 'social_media'],
|
|
852
|
+
allowList: ['Googlebot', 'Twitterbot'],
|
|
853
|
+
blockList: ['BadBot'],
|
|
1042
854
|
})
|
|
1043
855
|
|
|
1044
|
-
//
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
856
|
+
// Manual detection
|
|
857
|
+
const result = analyzeUserAgent('Googlebot/2.1')
|
|
858
|
+
// { isBot: true, category: 'search_engine', name: 'Googlebot', confidence: 0.95 }
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
### Honeypot Protection
|
|
862
|
+
|
|
863
|
+
```typescript
|
|
864
|
+
import { withHoneypotProtection, generateHoneypotHTML, generateHoneypotCSS } from 'nextjs-secure/bot'
|
|
865
|
+
|
|
866
|
+
// Middleware
|
|
867
|
+
export const POST = withHoneypotProtection(handler, {
|
|
868
|
+
fieldName: '_hp_email',
|
|
869
|
+
additionalFields: ['_hp_name', '_hp_phone'],
|
|
1053
870
|
})
|
|
1054
871
|
|
|
1055
|
-
//
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
872
|
+
// Generate HTML for forms
|
|
873
|
+
const honeypotHTML = generateHoneypotHTML({ fieldName: '_hp_email' })
|
|
874
|
+
// Returns hidden input fields
|
|
875
|
+
|
|
876
|
+
// Generate CSS
|
|
877
|
+
const honeypotCSS = generateHoneypotCSS({ fieldName: '_hp_email' })
|
|
878
|
+
// Returns CSS to hide fields
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
### Behavior Analysis
|
|
882
|
+
|
|
883
|
+
```typescript
|
|
884
|
+
import { withBehaviorProtection, MemoryBehaviorStore } from 'nextjs-secure/bot'
|
|
885
|
+
|
|
886
|
+
const store = new MemoryBehaviorStore()
|
|
887
|
+
|
|
888
|
+
export const GET = withBehaviorProtection(handler, {
|
|
1059
889
|
store,
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
890
|
+
minRequestInterval: 100, // Min ms between requests
|
|
891
|
+
maxRequestsPerSecond: 10, // Max requests per second
|
|
892
|
+
patterns: {
|
|
893
|
+
sequentialAccess: true, // Detect sequential URL patterns
|
|
894
|
+
regularTiming: true, // Detect bot-like timing
|
|
895
|
+
missingHeaders: true, // Detect missing browser headers
|
|
1063
896
|
},
|
|
1064
897
|
})
|
|
1065
898
|
```
|
|
1066
899
|
|
|
1067
|
-
###
|
|
900
|
+
### CAPTCHA Integration
|
|
1068
901
|
|
|
1069
902
|
```typescript
|
|
1070
|
-
import {
|
|
1071
|
-
|
|
903
|
+
import { withCaptchaProtection, verifyCaptcha } from 'nextjs-secure/bot'
|
|
904
|
+
|
|
905
|
+
// reCAPTCHA v3
|
|
906
|
+
export const POST = withCaptchaProtection(handler, {
|
|
907
|
+
provider: 'recaptcha-v3',
|
|
908
|
+
siteKey: process.env.RECAPTCHA_SITE_KEY,
|
|
909
|
+
secretKey: process.env.RECAPTCHA_SECRET_KEY,
|
|
910
|
+
threshold: 0.5,
|
|
911
|
+
})
|
|
1072
912
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
913
|
+
// hCaptcha
|
|
914
|
+
export const POST = withCaptchaProtection(handler, {
|
|
915
|
+
provider: 'hcaptcha',
|
|
916
|
+
siteKey: process.env.HCAPTCHA_SITE_KEY,
|
|
917
|
+
secretKey: process.env.HCAPTCHA_SECRET_KEY,
|
|
918
|
+
})
|
|
1076
919
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
920
|
+
// Cloudflare Turnstile
|
|
921
|
+
export const POST = withCaptchaProtection(handler, {
|
|
922
|
+
provider: 'turnstile',
|
|
923
|
+
siteKey: process.env.TURNSTILE_SITE_KEY,
|
|
924
|
+
secretKey: process.env.TURNSTILE_SECRET_KEY,
|
|
925
|
+
})
|
|
1080
926
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
identifier: async (req) => {
|
|
1087
|
-
const session = await getServerSession()
|
|
1088
|
-
return session?.user?.id ?? 'anonymous'
|
|
1089
|
-
},
|
|
1090
|
-
}
|
|
1091
|
-
)
|
|
927
|
+
// Manual verification
|
|
928
|
+
const result = await verifyCaptcha(token, {
|
|
929
|
+
provider: 'recaptcha-v3',
|
|
930
|
+
secretKey: process.env.RECAPTCHA_SECRET_KEY,
|
|
931
|
+
})
|
|
1092
932
|
```
|
|
1093
933
|
|
|
1094
|
-
###
|
|
934
|
+
### Manual Bot Detection
|
|
1095
935
|
|
|
1096
936
|
```typescript
|
|
1097
|
-
import {
|
|
1098
|
-
import { headers } from 'next/headers'
|
|
1099
|
-
import crypto from 'crypto'
|
|
937
|
+
import { detectBot } from 'nextjs-secure/bot'
|
|
1100
938
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
return Response.json({ error: 'Invalid signature' }, { status: 401 })
|
|
1114
|
-
}
|
|
939
|
+
const result = await detectBot(request, {
|
|
940
|
+
userAgent: { blockAllBots: true },
|
|
941
|
+
honeypot: true,
|
|
942
|
+
behavior: { maxRequestsPerSecond: 10 },
|
|
943
|
+
})
|
|
944
|
+
|
|
945
|
+
if (result.isBot) {
|
|
946
|
+
console.log(`Bot detected: ${result.reason}`)
|
|
947
|
+
console.log(`Category: ${result.category}`)
|
|
948
|
+
console.log(`Confidence: ${result.confidence}`)
|
|
949
|
+
}
|
|
950
|
+
```
|
|
1115
951
|
|
|
1116
|
-
|
|
1117
|
-
const data = JSON.parse(body)
|
|
1118
|
-
await processWebhook(data)
|
|
952
|
+
### Bot Categories
|
|
1119
953
|
|
|
1120
|
-
|
|
1121
|
-
|
|
954
|
+
| Category | Examples |
|
|
955
|
+
|----------|----------|
|
|
956
|
+
| `search_engine` | Googlebot, Bingbot, Yandex |
|
|
957
|
+
| `social_media` | Twitterbot, FacebookBot, LinkedInBot |
|
|
958
|
+
| `ai_crawler` | GPTBot, Claude-Web, Anthropic |
|
|
959
|
+
| `monitoring` | UptimeRobot, Pingdom |
|
|
960
|
+
| `feed_reader` | Feedly, Feedbin |
|
|
961
|
+
| `preview` | Slackbot, Discord |
|
|
962
|
+
| `scraper` | Scrapy, DataMiner |
|
|
963
|
+
| `spam` | Spam bots, malicious crawlers |
|
|
964
|
+
| `unknown` | Unidentified automated traffic |
|
|
965
|
+
|
|
966
|
+
---
|
|
967
|
+
|
|
968
|
+
## Utilities
|
|
969
|
+
|
|
970
|
+
### Duration Parsing
|
|
971
|
+
|
|
972
|
+
```typescript
|
|
973
|
+
import { parseDuration, formatDuration } from 'nextjs-secure'
|
|
974
|
+
|
|
975
|
+
parseDuration('15m') // 900000
|
|
976
|
+
parseDuration('1h 30m') // 5400000
|
|
977
|
+
parseDuration('2d') // 172800000
|
|
978
|
+
|
|
979
|
+
formatDuration(900000) // '15m'
|
|
980
|
+
formatDuration(5400000) // '1h 30m'
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
### IP Utilities
|
|
984
|
+
|
|
985
|
+
```typescript
|
|
986
|
+
import { getClientIp, anonymizeIp, isPrivateIp, isLocalhost } from 'nextjs-secure'
|
|
987
|
+
|
|
988
|
+
// Extract client IP
|
|
989
|
+
const ip = getClientIp(request, {
|
|
990
|
+
trustProxy: true,
|
|
991
|
+
customHeaders: ['x-custom-ip'],
|
|
992
|
+
})
|
|
993
|
+
|
|
994
|
+
// Anonymize for GDPR
|
|
995
|
+
anonymizeIp('192.168.1.100') // '192.168.1.xxx'
|
|
996
|
+
|
|
997
|
+
// Check IP type
|
|
998
|
+
isPrivateIp('192.168.1.1') // true
|
|
999
|
+
isPrivateIp('8.8.8.8') // false
|
|
1000
|
+
isLocalhost('127.0.0.1') // true
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
---
|
|
1004
|
+
|
|
1005
|
+
## API Reference
|
|
1006
|
+
|
|
1007
|
+
### Rate Limiting
|
|
1008
|
+
|
|
1009
|
+
| Function | Description |
|
|
1010
|
+
|----------|-------------|
|
|
1011
|
+
| `withRateLimit(handler, config)` | Wrap handler with rate limiting |
|
|
1012
|
+
| `createRateLimiter(config)` | Create reusable rate limiter |
|
|
1013
|
+
| `checkRateLimit(request, config)` | Manual rate limit check |
|
|
1014
|
+
| `getRateLimitStatus(key, config)` | Get current status without incrementing |
|
|
1015
|
+
| `resetRateLimit(key, config)` | Reset rate limit for key |
|
|
1016
|
+
|
|
1017
|
+
### CSRF
|
|
1018
|
+
|
|
1019
|
+
| Function | Description |
|
|
1020
|
+
|----------|-------------|
|
|
1021
|
+
| `withCSRF(handler, config)` | Wrap handler with CSRF protection |
|
|
1022
|
+
| `generateCSRF(config)` | Generate CSRF token and cookie |
|
|
1023
|
+
| `validateCSRF(request, config)` | Manual CSRF validation |
|
|
1024
|
+
|
|
1025
|
+
### Security Headers
|
|
1026
|
+
|
|
1027
|
+
| Function | Description |
|
|
1028
|
+
|----------|-------------|
|
|
1029
|
+
| `withSecurityHeaders(handler, config)` | Add security headers |
|
|
1030
|
+
| `createSecurityHeaders(config)` | Create headers object |
|
|
1031
|
+
| `buildCSP(config)` | Build CSP header string |
|
|
1032
|
+
| `getPreset(name)` | Get preset configuration |
|
|
1033
|
+
|
|
1034
|
+
### Authentication
|
|
1035
|
+
|
|
1036
|
+
| Function | Description |
|
|
1037
|
+
|----------|-------------|
|
|
1038
|
+
| `withJWT(handler, config)` | JWT authentication |
|
|
1039
|
+
| `withAPIKey(handler, config)` | API key authentication |
|
|
1040
|
+
| `withSession(handler, config)` | Session authentication |
|
|
1041
|
+
| `withAuth(handler, config)` | Combined authentication |
|
|
1042
|
+
| `withRoles(handler, config)` | Role-based access control |
|
|
1043
|
+
| `withOptionalAuth(handler, config)` | Optional authentication |
|
|
1044
|
+
| `verifyJWT(token, config)` | Verify JWT token |
|
|
1045
|
+
| `decodeJWT(token)` | Decode JWT without verification |
|
|
1046
|
+
|
|
1047
|
+
### Validation
|
|
1048
|
+
|
|
1049
|
+
| Function | Description |
|
|
1050
|
+
|----------|-------------|
|
|
1051
|
+
| `withValidation(handler, config)` | Schema validation |
|
|
1052
|
+
| `withXSSProtection(handler)` | Block XSS attempts |
|
|
1053
|
+
| `withSanitization(handler, config)` | Sanitize input |
|
|
1054
|
+
| `withSQLProtection(handler, config)` | Block SQL injection |
|
|
1055
|
+
| `withFileValidation(handler, config)` | File upload validation |
|
|
1056
|
+
| `sanitize(input, config)` | Manual sanitization |
|
|
1057
|
+
| `detectXSS(input)` | Detect XSS patterns |
|
|
1058
|
+
| `detectSQLInjection(input)` | Detect SQL injection |
|
|
1059
|
+
| `validatePath(path, config)` | Validate file path |
|
|
1060
|
+
|
|
1061
|
+
### Audit Logging
|
|
1062
|
+
|
|
1063
|
+
| Function | Description |
|
|
1064
|
+
|----------|-------------|
|
|
1065
|
+
| `withAuditLog(handler, config)` | Request logging |
|
|
1066
|
+
| `withRequestId(handler, config)` | Add request ID |
|
|
1067
|
+
| `withTiming(handler, config)` | Add response timing |
|
|
1068
|
+
| `createSecurityTracker(config)` | Create event tracker |
|
|
1069
|
+
| `trackSecurityEvent(store, event)` | Track single event |
|
|
1070
|
+
| `redactObject(obj, config)` | Redact PII from object |
|
|
1071
|
+
|
|
1072
|
+
### Bot Detection
|
|
1073
|
+
|
|
1074
|
+
| Function | Description |
|
|
1075
|
+
|----------|-------------|
|
|
1076
|
+
| `withBotProtection(handler, config)` | Combined bot protection |
|
|
1077
|
+
| `withUserAgentProtection(handler, config)` | User-agent only protection |
|
|
1078
|
+
| `withHoneypotProtection(handler, config)` | Honeypot only protection |
|
|
1079
|
+
| `withBehaviorProtection(handler, config)` | Behavior analysis only |
|
|
1080
|
+
| `withCaptchaProtection(handler, config)` | CAPTCHA verification |
|
|
1081
|
+
| `withBotProtectionPreset(handler, preset)` | Use preset configuration |
|
|
1082
|
+
| `detectBot(request, config)` | Manual bot detection |
|
|
1083
|
+
| `analyzeUserAgent(userAgent, config)` | Analyze user-agent string |
|
|
1084
|
+
| `checkHoneypot(request, config)` | Check honeypot fields |
|
|
1085
|
+
| `checkBehavior(request, config)` | Check request behavior |
|
|
1086
|
+
| `verifyCaptcha(token, config)` | Verify CAPTCHA token |
|
|
1087
|
+
| `generateHoneypotHTML(config)` | Generate honeypot HTML |
|
|
1088
|
+
| `generateHoneypotCSS(config)` | Generate honeypot CSS |
|
|
1089
|
+
|
|
1090
|
+
---
|
|
1091
|
+
|
|
1092
|
+
## Examples
|
|
1093
|
+
|
|
1094
|
+
### Complete API with All Security Features
|
|
1095
|
+
|
|
1096
|
+
```typescript
|
|
1097
|
+
// lib/security.ts
|
|
1098
|
+
import { createRateLimiter, MemoryStore } from 'nextjs-secure/rate-limit'
|
|
1099
|
+
import { createSecurityTracker, MemoryStore as AuditStore } from 'nextjs-secure/audit'
|
|
1100
|
+
|
|
1101
|
+
export const apiLimiter = createRateLimiter({
|
|
1102
|
+
limit: 100,
|
|
1103
|
+
window: '15m',
|
|
1104
|
+
store: new MemoryStore(),
|
|
1105
|
+
})
|
|
1106
|
+
|
|
1107
|
+
export const strictLimiter = createRateLimiter({
|
|
1108
|
+
limit: 5,
|
|
1109
|
+
window: '1m',
|
|
1110
|
+
})
|
|
1111
|
+
|
|
1112
|
+
export const auditStore = new AuditStore({ maxEntries: 10000 })
|
|
1113
|
+
export const securityTracker = createSecurityTracker({ store: auditStore })
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
```typescript
|
|
1117
|
+
// app/api/users/route.ts
|
|
1118
|
+
import { withJWT, withRoles } from 'nextjs-secure/auth'
|
|
1119
|
+
import { withValidation } from 'nextjs-secure/validation'
|
|
1120
|
+
import { withAuditLog } from 'nextjs-secure/audit'
|
|
1121
|
+
import { apiLimiter, auditStore, securityTracker } from '@/lib/security'
|
|
1122
|
+
|
|
1123
|
+
const createUserSchema = {
|
|
1124
|
+
email: { type: 'email', required: true },
|
|
1125
|
+
name: { type: 'string', minLength: 2, maxLength: 100 },
|
|
1126
|
+
role: { type: 'string', enum: ['user', 'admin'] },
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
async function createUser(req, ctx) {
|
|
1130
|
+
const { email, name, role } = ctx.validated
|
|
1131
|
+
const user = await db.users.create({ email, name, role })
|
|
1132
|
+
return Response.json(user, { status: 201 })
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
export const POST = withAuditLog(
|
|
1136
|
+
apiLimiter(
|
|
1137
|
+
withJWT(
|
|
1138
|
+
withRoles(
|
|
1139
|
+
withValidation(createUser, { body: createUserSchema }),
|
|
1140
|
+
{ roles: ['admin'] }
|
|
1141
|
+
),
|
|
1142
|
+
{ secret: process.env.JWT_SECRET }
|
|
1143
|
+
)
|
|
1144
|
+
),
|
|
1122
1145
|
{
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
// Rate limit by webhook source
|
|
1127
|
-
return req.headers.get('x-webhook-source') ?? 'unknown'
|
|
1128
|
-
},
|
|
1146
|
+
store: auditStore,
|
|
1147
|
+
include: { ip: true, userAgent: true },
|
|
1148
|
+
pii: { fields: ['password'], mode: 'remove' },
|
|
1129
1149
|
}
|
|
1130
1150
|
)
|
|
1131
1151
|
```
|
|
1132
1152
|
|
|
1153
|
+
### Tiered Rate Limiting
|
|
1154
|
+
|
|
1155
|
+
```typescript
|
|
1156
|
+
import { createRateLimiter, createUpstashStore } from 'nextjs-secure/rate-limit'
|
|
1157
|
+
|
|
1158
|
+
const store = createUpstashStore({
|
|
1159
|
+
url: process.env.UPSTASH_REDIS_REST_URL,
|
|
1160
|
+
token: process.env.UPSTASH_REDIS_REST_TOKEN,
|
|
1161
|
+
})
|
|
1162
|
+
|
|
1163
|
+
const freeLimiter = createRateLimiter({
|
|
1164
|
+
limit: 100,
|
|
1165
|
+
window: '1d',
|
|
1166
|
+
store,
|
|
1167
|
+
identifier: (req) => `free:${req.headers.get('x-api-key')}`,
|
|
1168
|
+
})
|
|
1169
|
+
|
|
1170
|
+
const proLimiter = createRateLimiter({
|
|
1171
|
+
limit: 10000,
|
|
1172
|
+
window: '1d',
|
|
1173
|
+
store,
|
|
1174
|
+
identifier: (req) => `pro:${req.headers.get('x-api-key')}`,
|
|
1175
|
+
})
|
|
1176
|
+
|
|
1177
|
+
export async function GET(req) {
|
|
1178
|
+
const tier = await getUserTier(req)
|
|
1179
|
+
const limiter = tier === 'pro' ? proLimiter : freeLimiter
|
|
1180
|
+
return limiter(handler)(req)
|
|
1181
|
+
}
|
|
1182
|
+
```
|
|
1183
|
+
|
|
1184
|
+
---
|
|
1185
|
+
|
|
1133
1186
|
## Roadmap
|
|
1134
1187
|
|
|
1135
|
-
- [x]
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
- [x] Security Headers (v0.3.0)
|
|
1147
|
-
- [x] Content-Security-Policy
|
|
1148
|
-
- [x] Strict-Transport-Security
|
|
1149
|
-
- [x] X-Frame-Options, X-Content-Type-Options
|
|
1150
|
-
- [x] Permissions-Policy
|
|
1151
|
-
- [x] COOP, COEP, CORP
|
|
1152
|
-
- [x] Presets (strict, relaxed, api)
|
|
1153
|
-
- [x] Authentication (v0.4.0)
|
|
1154
|
-
- [x] JWT validation (HS256, RS256, ES256)
|
|
1155
|
-
- [x] API Key authentication
|
|
1156
|
-
- [x] Session/Cookie authentication
|
|
1157
|
-
- [x] Role-Based Access Control (RBAC)
|
|
1158
|
-
- [x] Combined multi-strategy auth
|
|
1159
|
-
- [ ] Input Validation (v0.5.0)
|
|
1160
|
-
- [ ] Audit Logging (v0.6.0)
|
|
1188
|
+
- [x] **v0.1.x** - Rate Limiting
|
|
1189
|
+
- [x] **v0.2.0** - CSRF Protection
|
|
1190
|
+
- [x] **v0.3.0** - Security Headers
|
|
1191
|
+
- [x] **v0.4.0** - Authentication
|
|
1192
|
+
- [x] **v0.5.0** - Input Validation
|
|
1193
|
+
- [x] **v0.6.0** - Audit Logging
|
|
1194
|
+
- [x] **v0.7.0** - Bot Detection
|
|
1195
|
+
|
|
1196
|
+
See [ROADMAP.md](ROADMAP.md) for detailed progress and future plans.
|
|
1197
|
+
|
|
1198
|
+
---
|
|
1161
1199
|
|
|
1162
1200
|
## Contributing
|
|
1163
1201
|
|
|
@@ -1178,10 +1216,20 @@ npm test
|
|
|
1178
1216
|
npm run build
|
|
1179
1217
|
```
|
|
1180
1218
|
|
|
1219
|
+
### Running Tests
|
|
1220
|
+
|
|
1221
|
+
```bash
|
|
1222
|
+
npm run test # Watch mode
|
|
1223
|
+
npm run test:run # Single run
|
|
1224
|
+
npm run test:coverage # With coverage
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
---
|
|
1228
|
+
|
|
1181
1229
|
## License
|
|
1182
1230
|
|
|
1183
1231
|
MIT License - see [LICENSE](LICENSE) for details.
|
|
1184
1232
|
|
|
1185
1233
|
---
|
|
1186
1234
|
|
|
1187
|
-
**
|
|
1235
|
+
**Made with security in mind for the Next.js community.**
|