nextjs-secure 0.3.0 → 0.6.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 +656 -501
- 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/auth.cjs +500 -6
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +180 -19
- package/dist/auth.d.ts +180 -19
- package/dist/auth.js +493 -6
- package/dist/auth.js.map +1 -1
- package/dist/index.cjs +3743 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3668 -8
- package/dist/index.js.map +1 -1
- package/dist/path-BVbunPfR.d.cts +534 -0
- package/dist/path-BVbunPfR.d.ts +534 -0
- package/dist/validation.cjs +2031 -0
- package/dist/validation.cjs.map +1 -0
- package/dist/validation.d.cts +42 -0
- package/dist/validation.d.ts +42 -0
- package/dist/validation.js +1964 -0
- package/dist/validation.js.map +1 -0
- 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,26 +47,19 @@ 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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
- [Authentication](#authentication)
|
|
53
|
+
- [Input Validation](#input-validation)
|
|
54
|
+
- [Audit Logging](#audit-logging)
|
|
59
55
|
- [Utilities](#utilities)
|
|
60
56
|
- [API Reference](#api-reference)
|
|
61
57
|
- [Examples](#examples)
|
|
62
58
|
- [Roadmap](#roadmap)
|
|
63
59
|
- [Contributing](#contributing)
|
|
64
60
|
|
|
61
|
+
---
|
|
62
|
+
|
|
65
63
|
## Quick Start
|
|
66
64
|
|
|
67
65
|
### Protect an API Route
|
|
@@ -75,48 +73,53 @@ export const GET = withRateLimit(
|
|
|
75
73
|
const posts = await db.posts.findMany()
|
|
76
74
|
return Response.json(posts)
|
|
77
75
|
},
|
|
78
|
-
{
|
|
79
|
-
limit: 100, // 100 requests
|
|
80
|
-
window: '15m' // per 15 minutes
|
|
81
|
-
}
|
|
76
|
+
{ limit: 100, window: '15m' }
|
|
82
77
|
)
|
|
83
78
|
```
|
|
84
79
|
|
|
85
|
-
###
|
|
80
|
+
### Full Security Stack
|
|
86
81
|
|
|
87
82
|
```typescript
|
|
88
|
-
//
|
|
89
|
-
import {
|
|
90
|
-
|
|
91
|
-
export const apiLimiter = createRateLimiter({
|
|
92
|
-
limit: 100,
|
|
93
|
-
window: '15m',
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
export const strictLimiter = createRateLimiter({
|
|
97
|
-
limit: 10,
|
|
98
|
-
window: '1m',
|
|
99
|
-
})
|
|
100
|
-
```
|
|
83
|
+
// app/api/admin/users/route.ts
|
|
84
|
+
import { withRateLimit, withJWT, withRoles, withValidation, withAuditLog } from 'nextjs-secure'
|
|
85
|
+
import { MemoryStore } from 'nextjs-secure/audit'
|
|
101
86
|
|
|
102
|
-
|
|
103
|
-
// app/api/users/route.ts
|
|
104
|
-
import { apiLimiter, strictLimiter } from '@/lib/rate-limit'
|
|
87
|
+
const auditStore = new MemoryStore({ maxEntries: 1000 })
|
|
105
88
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
89
|
+
const schema = {
|
|
90
|
+
email: { type: 'email', required: true },
|
|
91
|
+
role: { type: 'string', enum: ['user', 'admin'] }
|
|
92
|
+
}
|
|
109
93
|
|
|
110
|
-
export const POST =
|
|
111
|
-
|
|
112
|
-
|
|
94
|
+
export const POST = withAuditLog(
|
|
95
|
+
withRateLimit(
|
|
96
|
+
withJWT(
|
|
97
|
+
withRoles(
|
|
98
|
+
withValidation(
|
|
99
|
+
async (req, ctx) => {
|
|
100
|
+
// ctx.user = authenticated user
|
|
101
|
+
// ctx.validated = validated body
|
|
102
|
+
return Response.json({ success: true })
|
|
103
|
+
},
|
|
104
|
+
{ body: schema }
|
|
105
|
+
),
|
|
106
|
+
{ roles: ['admin'] }
|
|
107
|
+
),
|
|
108
|
+
{ secret: process.env.JWT_SECRET }
|
|
109
|
+
),
|
|
110
|
+
{ limit: 10, window: '1m' }
|
|
111
|
+
),
|
|
112
|
+
{ store: auditStore }
|
|
113
|
+
)
|
|
113
114
|
```
|
|
114
115
|
|
|
116
|
+
---
|
|
117
|
+
|
|
115
118
|
## Rate Limiting
|
|
116
119
|
|
|
117
|
-
|
|
120
|
+
Protect your APIs from abuse with configurable rate limiting.
|
|
118
121
|
|
|
119
|
-
|
|
122
|
+
### Basic Usage
|
|
120
123
|
|
|
121
124
|
```typescript
|
|
122
125
|
import { withRateLimit } from 'nextjs-secure'
|
|
@@ -138,29 +141,18 @@ export const GET = withRateLimit(handler, {
|
|
|
138
141
|
### Algorithms
|
|
139
142
|
|
|
140
143
|
#### Sliding Window (Default)
|
|
141
|
-
|
|
142
|
-
Prevents request bursts at window boundaries. Uses weighted counting between current and previous windows.
|
|
144
|
+
Prevents request bursts at window boundaries.
|
|
143
145
|
|
|
144
146
|
```typescript
|
|
145
147
|
export const GET = withRateLimit(handler, {
|
|
146
148
|
limit: 100,
|
|
147
149
|
window: '15m',
|
|
148
|
-
algorithm: 'sliding-window',
|
|
150
|
+
algorithm: 'sliding-window',
|
|
149
151
|
})
|
|
150
152
|
```
|
|
151
153
|
|
|
152
|
-
**How it works:**
|
|
153
|
-
```
|
|
154
|
-
Window 1: |----[80 requests]-----|
|
|
155
|
-
Window 2: |--[30 requests]-------|
|
|
156
|
-
^ 50% through window 2
|
|
157
|
-
|
|
158
|
-
Weighted count = 30 + (80 × 0.5) = 70 requests
|
|
159
|
-
```
|
|
160
|
-
|
|
161
154
|
#### Fixed Window
|
|
162
|
-
|
|
163
|
-
Simple counter that resets at fixed intervals. Lower memory usage but allows bursts at boundaries.
|
|
155
|
+
Simple counter that resets at fixed intervals.
|
|
164
156
|
|
|
165
157
|
```typescript
|
|
166
158
|
export const GET = withRateLimit(handler, {
|
|
@@ -170,83 +162,45 @@ export const GET = withRateLimit(handler, {
|
|
|
170
162
|
})
|
|
171
163
|
```
|
|
172
164
|
|
|
173
|
-
**Burst scenario:**
|
|
174
|
-
```
|
|
175
|
-
Window 1: |------------------[100]| <- 100 requests at :59
|
|
176
|
-
Window 2: |[100]------------------| <- 100 requests at :00
|
|
177
|
-
200 requests in 2 seconds!
|
|
178
|
-
```
|
|
179
|
-
|
|
180
165
|
#### Token Bucket
|
|
181
|
-
|
|
182
|
-
Allows controlled bursts while maintaining average rate. Tokens refill continuously.
|
|
166
|
+
Allows controlled bursts while maintaining average rate.
|
|
183
167
|
|
|
184
168
|
```typescript
|
|
185
169
|
export const GET = withRateLimit(handler, {
|
|
186
|
-
limit: 100,
|
|
187
|
-
window: '1m',
|
|
170
|
+
limit: 100,
|
|
171
|
+
window: '1m',
|
|
188
172
|
algorithm: 'token-bucket',
|
|
189
173
|
})
|
|
190
174
|
```
|
|
191
175
|
|
|
192
|
-
**Use case:** APIs where occasional bursts are acceptable but average rate must be controlled.
|
|
193
|
-
|
|
194
176
|
### Storage Backends
|
|
195
177
|
|
|
196
178
|
#### Memory Store (Default)
|
|
197
179
|
|
|
198
|
-
Built-in, zero-config. Perfect for development and single-instance deployments.
|
|
199
|
-
|
|
200
180
|
```typescript
|
|
201
181
|
import { withRateLimit, MemoryStore } from 'nextjs-secure'
|
|
202
182
|
|
|
203
183
|
const store = new MemoryStore({
|
|
204
|
-
cleanupInterval: 60000,
|
|
205
|
-
maxKeys: 10000,
|
|
184
|
+
cleanupInterval: 60000,
|
|
185
|
+
maxKeys: 10000,
|
|
206
186
|
})
|
|
207
187
|
|
|
208
|
-
export const GET = withRateLimit(handler, {
|
|
209
|
-
limit: 100,
|
|
210
|
-
window: '15m',
|
|
211
|
-
store,
|
|
212
|
-
})
|
|
188
|
+
export const GET = withRateLimit(handler, { limit: 100, window: '15m', store })
|
|
213
189
|
```
|
|
214
190
|
|
|
215
|
-
**Limitations:**
|
|
216
|
-
- Data lost on restart
|
|
217
|
-
- Not shared between instances
|
|
218
|
-
- Not suitable for serverless (cold starts)
|
|
219
|
-
|
|
220
191
|
#### Redis Store
|
|
221
192
|
|
|
222
|
-
For distributed deployments. Works with ioredis, node-redis, or any compatible client.
|
|
223
|
-
|
|
224
193
|
```typescript
|
|
225
194
|
import Redis from 'ioredis'
|
|
226
195
|
import { withRateLimit, createRedisStore } from 'nextjs-secure/rate-limit'
|
|
227
196
|
|
|
228
197
|
const redis = new Redis(process.env.REDIS_URL)
|
|
198
|
+
const store = createRedisStore({ client: redis, prefix: 'myapp:rl' })
|
|
229
199
|
|
|
230
|
-
const
|
|
231
|
-
client: redis,
|
|
232
|
-
prefix: 'myapp:rl', // Key prefix
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
export const GET = withRateLimit(handler, {
|
|
236
|
-
limit: 100,
|
|
237
|
-
window: '15m',
|
|
238
|
-
store,
|
|
239
|
-
})
|
|
200
|
+
export const GET = withRateLimit(handler, { limit: 100, window: '15m', store })
|
|
240
201
|
```
|
|
241
202
|
|
|
242
|
-
|
|
243
|
-
- Atomic operations via Lua scripts
|
|
244
|
-
- Automatic key expiration
|
|
245
|
-
- Cluster-ready
|
|
246
|
-
|
|
247
|
-
#### Upstash Store
|
|
248
|
-
|
|
249
|
-
Optimized for serverless and edge. Uses HTTP-based Redis.
|
|
203
|
+
#### Upstash Store (Edge/Serverless)
|
|
250
204
|
|
|
251
205
|
```typescript
|
|
252
206
|
import { withRateLimit, createUpstashStore } from 'nextjs-secure/rate-limit'
|
|
@@ -256,41 +210,20 @@ const store = createUpstashStore({
|
|
|
256
210
|
token: process.env.UPSTASH_REDIS_REST_TOKEN,
|
|
257
211
|
})
|
|
258
212
|
|
|
259
|
-
export const GET = withRateLimit(handler, {
|
|
260
|
-
limit: 100,
|
|
261
|
-
window: '15m',
|
|
262
|
-
store,
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
// Or from environment variables
|
|
266
|
-
import { createUpstashStoreFromEnv } from 'nextjs-secure/rate-limit'
|
|
267
|
-
const store = createUpstashStoreFromEnv()
|
|
213
|
+
export const GET = withRateLimit(handler, { limit: 100, window: '15m', store })
|
|
268
214
|
```
|
|
269
215
|
|
|
270
|
-
**Benefits:**
|
|
271
|
-
- No TCP connections
|
|
272
|
-
- Works on Edge Runtime
|
|
273
|
-
- Global distribution support
|
|
274
|
-
|
|
275
216
|
### Custom Identifiers
|
|
276
217
|
|
|
277
|
-
By default, rate limiting is per-IP. Customize with the `identifier` option:
|
|
278
|
-
|
|
279
|
-
#### By API Key
|
|
280
|
-
|
|
281
218
|
```typescript
|
|
219
|
+
// By API Key
|
|
282
220
|
export const GET = withRateLimit(handler, {
|
|
283
221
|
limit: 1000,
|
|
284
222
|
window: '1h',
|
|
285
|
-
identifier: (req) =>
|
|
286
|
-
return req.headers.get('x-api-key') ?? 'anonymous'
|
|
287
|
-
},
|
|
223
|
+
identifier: (req) => req.headers.get('x-api-key') ?? 'anonymous',
|
|
288
224
|
})
|
|
289
|
-
```
|
|
290
225
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
```typescript
|
|
226
|
+
// By User ID
|
|
294
227
|
export const GET = withRateLimit(handler, {
|
|
295
228
|
limit: 100,
|
|
296
229
|
window: '15m',
|
|
@@ -301,104 +234,20 @@ export const GET = withRateLimit(handler, {
|
|
|
301
234
|
})
|
|
302
235
|
```
|
|
303
236
|
|
|
304
|
-
#### By Route + IP
|
|
305
|
-
|
|
306
|
-
```typescript
|
|
307
|
-
export const GET = withRateLimit(handler, {
|
|
308
|
-
limit: 100,
|
|
309
|
-
window: '15m',
|
|
310
|
-
identifier: (req) => {
|
|
311
|
-
const ip = req.headers.get('x-forwarded-for') ?? '127.0.0.1'
|
|
312
|
-
return `${req.nextUrl.pathname}:${ip}`
|
|
313
|
-
},
|
|
314
|
-
})
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
### Response Customization
|
|
318
|
-
|
|
319
|
-
#### Custom Error Response
|
|
320
|
-
|
|
321
|
-
```typescript
|
|
322
|
-
export const GET = withRateLimit(handler, {
|
|
323
|
-
limit: 100,
|
|
324
|
-
window: '15m',
|
|
325
|
-
onLimit: (req, info) => {
|
|
326
|
-
return Response.json(
|
|
327
|
-
{
|
|
328
|
-
error: 'rate_limit_exceeded',
|
|
329
|
-
message: `Too many requests. Try again in ${info.retryAfter} seconds.`,
|
|
330
|
-
limit: info.limit,
|
|
331
|
-
reset: new Date(info.reset * 1000).toISOString(),
|
|
332
|
-
},
|
|
333
|
-
{ status: 429 }
|
|
334
|
-
)
|
|
335
|
-
},
|
|
336
|
-
})
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
#### Skip Certain Requests
|
|
340
|
-
|
|
341
|
-
```typescript
|
|
342
|
-
export const GET = withRateLimit(handler, {
|
|
343
|
-
limit: 100,
|
|
344
|
-
window: '15m',
|
|
345
|
-
skip: (req) => {
|
|
346
|
-
// Skip for internal services
|
|
347
|
-
const key = req.headers.get('x-internal-key')
|
|
348
|
-
return key === process.env.INTERNAL_API_KEY
|
|
349
|
-
},
|
|
350
|
-
})
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
#### Disable Headers
|
|
354
|
-
|
|
355
|
-
```typescript
|
|
356
|
-
export const GET = withRateLimit(handler, {
|
|
357
|
-
limit: 100,
|
|
358
|
-
window: '15m',
|
|
359
|
-
headers: false, // Don't add X-RateLimit-* headers
|
|
360
|
-
})
|
|
361
|
-
```
|
|
362
|
-
|
|
363
237
|
### Response Headers
|
|
364
238
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
|
368
|
-
|
|
369
|
-
| `X-RateLimit-
|
|
370
|
-
| `
|
|
371
|
-
| `X-RateLimit-Reset` | Unix timestamp when limit resets | `1699999999` |
|
|
372
|
-
| `Retry-After` | Seconds until retry (only on 429) | `60` |
|
|
373
|
-
|
|
374
|
-
### Manual Rate Limit Check
|
|
375
|
-
|
|
376
|
-
For existing handlers or complex logic:
|
|
377
|
-
|
|
378
|
-
```typescript
|
|
379
|
-
import { checkRateLimit } from 'nextjs-secure'
|
|
380
|
-
|
|
381
|
-
export async function GET(request: NextRequest) {
|
|
382
|
-
const { success, info, headers } = await checkRateLimit(request, {
|
|
383
|
-
limit: 100,
|
|
384
|
-
window: '15m',
|
|
385
|
-
})
|
|
386
|
-
|
|
387
|
-
if (!success) {
|
|
388
|
-
return Response.json(
|
|
389
|
-
{ error: 'Rate limited' },
|
|
390
|
-
{ status: 429, headers }
|
|
391
|
-
)
|
|
392
|
-
}
|
|
239
|
+
| Header | Description |
|
|
240
|
+
|--------|-------------|
|
|
241
|
+
| `X-RateLimit-Limit` | Maximum requests allowed |
|
|
242
|
+
| `X-RateLimit-Remaining` | Requests remaining |
|
|
243
|
+
| `X-RateLimit-Reset` | Unix timestamp when limit resets |
|
|
244
|
+
| `Retry-After` | Seconds until retry (only on 429) |
|
|
393
245
|
|
|
394
|
-
|
|
395
|
-
return Response.json({ data: '...' }, { headers })
|
|
396
|
-
}
|
|
397
|
-
```
|
|
246
|
+
---
|
|
398
247
|
|
|
399
248
|
## CSRF Protection
|
|
400
249
|
|
|
401
|
-
Protect
|
|
250
|
+
Protect forms against Cross-Site Request Forgery attacks.
|
|
402
251
|
|
|
403
252
|
### Basic Setup
|
|
404
253
|
|
|
@@ -408,7 +257,6 @@ import { generateCSRF } from 'nextjs-secure/csrf'
|
|
|
408
257
|
|
|
409
258
|
export async function GET() {
|
|
410
259
|
const { token, cookieHeader } = await generateCSRF()
|
|
411
|
-
|
|
412
260
|
return Response.json(
|
|
413
261
|
{ csrfToken: token },
|
|
414
262
|
{ headers: { 'Set-Cookie': cookieHeader } }
|
|
@@ -422,7 +270,6 @@ import { withCSRF } from 'nextjs-secure/csrf'
|
|
|
422
270
|
|
|
423
271
|
export const POST = withCSRF(async (req) => {
|
|
424
272
|
const data = await req.json()
|
|
425
|
-
// Safe to process - CSRF validated
|
|
426
273
|
return Response.json({ success: true })
|
|
427
274
|
})
|
|
428
275
|
```
|
|
@@ -438,111 +285,51 @@ fetch('/api/submit', {
|
|
|
438
285
|
method: 'POST',
|
|
439
286
|
headers: {
|
|
440
287
|
'Content-Type': 'application/json',
|
|
441
|
-
'x-csrf-token': csrfToken
|
|
288
|
+
'x-csrf-token': csrfToken
|
|
442
289
|
},
|
|
443
290
|
body: JSON.stringify({ data: '...' })
|
|
444
291
|
})
|
|
445
292
|
```
|
|
446
293
|
|
|
447
|
-
Or include in form body:
|
|
448
|
-
|
|
449
|
-
```typescript
|
|
450
|
-
fetch('/api/submit', {
|
|
451
|
-
method: 'POST',
|
|
452
|
-
headers: { 'Content-Type': 'application/json' },
|
|
453
|
-
body: JSON.stringify({
|
|
454
|
-
_csrf: csrfToken, // Token in body
|
|
455
|
-
data: '...'
|
|
456
|
-
})
|
|
457
|
-
})
|
|
458
|
-
```
|
|
459
|
-
|
|
460
294
|
### Configuration
|
|
461
295
|
|
|
462
296
|
```typescript
|
|
463
|
-
import { withCSRF } from 'nextjs-secure/csrf'
|
|
464
|
-
|
|
465
297
|
export const POST = withCSRF(handler, {
|
|
466
|
-
// Cookie settings
|
|
467
298
|
cookie: {
|
|
468
|
-
name: '__csrf',
|
|
469
|
-
httpOnly: true,
|
|
470
|
-
secure: true,
|
|
471
|
-
sameSite: 'strict',
|
|
472
|
-
maxAge: 86400
|
|
299
|
+
name: '__csrf',
|
|
300
|
+
httpOnly: true,
|
|
301
|
+
secure: true,
|
|
302
|
+
sameSite: 'strict',
|
|
303
|
+
maxAge: 86400
|
|
473
304
|
},
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
fieldName: '_csrf', // Body field name
|
|
478
|
-
|
|
479
|
-
// Token settings
|
|
480
|
-
secret: process.env.CSRF_SECRET, // Signing secret
|
|
481
|
-
tokenLength: 32, // Token size in bytes
|
|
482
|
-
|
|
483
|
-
// Protected methods (default: POST, PUT, PATCH, DELETE)
|
|
484
|
-
protectedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],
|
|
485
|
-
|
|
486
|
-
// Skip protection conditionally
|
|
305
|
+
headerName: 'x-csrf-token',
|
|
306
|
+
fieldName: '_csrf',
|
|
307
|
+
secret: process.env.CSRF_SECRET,
|
|
487
308
|
skip: (req) => req.headers.get('x-api-key') === 'trusted',
|
|
488
|
-
|
|
489
|
-
// Custom error response
|
|
490
|
-
onError: (req, reason) => {
|
|
491
|
-
return new Response(`CSRF failed: ${reason}`, { status: 403 })
|
|
492
|
-
}
|
|
493
309
|
})
|
|
494
310
|
```
|
|
495
311
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
```typescript
|
|
499
|
-
import { validateCSRF } from 'nextjs-secure/csrf'
|
|
500
|
-
|
|
501
|
-
export async function POST(req) {
|
|
502
|
-
const result = await validateCSRF(req)
|
|
503
|
-
|
|
504
|
-
if (!result.valid) {
|
|
505
|
-
console.log('CSRF failed:', result.reason)
|
|
506
|
-
// reason: 'missing_cookie' | 'invalid_cookie' | 'missing_token' | 'token_mismatch'
|
|
507
|
-
return Response.json({ error: result.reason }, { status: 403 })
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Continue processing
|
|
511
|
-
}
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
### Environment Variable
|
|
515
|
-
|
|
516
|
-
Set `CSRF_SECRET` in your environment:
|
|
517
|
-
|
|
518
|
-
```env
|
|
519
|
-
CSRF_SECRET=your-secret-key-min-32-chars-recommended
|
|
520
|
-
```
|
|
312
|
+
---
|
|
521
313
|
|
|
522
314
|
## Security Headers
|
|
523
315
|
|
|
524
|
-
Add security headers to
|
|
316
|
+
Add security headers to protect against common attacks.
|
|
525
317
|
|
|
526
318
|
### Quick Start
|
|
527
319
|
|
|
528
320
|
```typescript
|
|
529
321
|
import { withSecurityHeaders } from 'nextjs-secure/headers'
|
|
530
322
|
|
|
531
|
-
|
|
532
|
-
export const GET = withSecurityHeaders(async (req) => {
|
|
533
|
-
return Response.json({ data: 'protected' })
|
|
534
|
-
})
|
|
323
|
+
export const GET = withSecurityHeaders(handler)
|
|
535
324
|
```
|
|
536
325
|
|
|
537
326
|
### Presets
|
|
538
327
|
|
|
539
|
-
Three presets available: `strict`, `relaxed`, `api`
|
|
540
|
-
|
|
541
328
|
```typescript
|
|
542
329
|
// Strict: Maximum security (default)
|
|
543
330
|
export const GET = withSecurityHeaders(handler, { preset: 'strict' })
|
|
544
331
|
|
|
545
|
-
// Relaxed: Development-friendly
|
|
332
|
+
// Relaxed: Development-friendly
|
|
546
333
|
export const GET = withSecurityHeaders(handler, { preset: 'relaxed' })
|
|
547
334
|
|
|
548
335
|
// API: Optimized for JSON APIs
|
|
@@ -552,36 +339,22 @@ export const GET = withSecurityHeaders(handler, { preset: 'api' })
|
|
|
552
339
|
### Custom Configuration
|
|
553
340
|
|
|
554
341
|
```typescript
|
|
555
|
-
import { withSecurityHeaders } from 'nextjs-secure/headers'
|
|
556
|
-
|
|
557
342
|
export const GET = withSecurityHeaders(handler, {
|
|
558
343
|
config: {
|
|
559
|
-
// Content-Security-Policy
|
|
560
344
|
contentSecurityPolicy: {
|
|
561
345
|
defaultSrc: ["'self'"],
|
|
562
346
|
scriptSrc: ["'self'", "'unsafe-inline'"],
|
|
563
347
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
564
348
|
imgSrc: ["'self'", 'data:', 'https:'],
|
|
565
349
|
},
|
|
566
|
-
|
|
567
|
-
// Strict-Transport-Security
|
|
568
350
|
strictTransportSecurity: {
|
|
569
351
|
maxAge: 31536000,
|
|
570
352
|
includeSubDomains: true,
|
|
571
353
|
preload: true,
|
|
572
354
|
},
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
xFrameOptions: 'DENY', // or 'SAMEORIGIN'
|
|
576
|
-
xContentTypeOptions: true, // X-Content-Type-Options: nosniff
|
|
355
|
+
xFrameOptions: 'DENY',
|
|
356
|
+
xContentTypeOptions: true,
|
|
577
357
|
referrerPolicy: 'strict-origin-when-cross-origin',
|
|
578
|
-
|
|
579
|
-
// Cross-Origin headers
|
|
580
|
-
crossOriginOpenerPolicy: 'same-origin',
|
|
581
|
-
crossOriginEmbedderPolicy: 'require-corp',
|
|
582
|
-
crossOriginResourcePolicy: 'same-origin',
|
|
583
|
-
|
|
584
|
-
// Permissions-Policy (disable features)
|
|
585
358
|
permissionsPolicy: {
|
|
586
359
|
camera: [],
|
|
587
360
|
microphone: [],
|
|
@@ -591,31 +364,6 @@ export const GET = withSecurityHeaders(handler, {
|
|
|
591
364
|
})
|
|
592
365
|
```
|
|
593
366
|
|
|
594
|
-
### Disable Specific Headers
|
|
595
|
-
|
|
596
|
-
```typescript
|
|
597
|
-
export const GET = withSecurityHeaders(handler, {
|
|
598
|
-
config: {
|
|
599
|
-
contentSecurityPolicy: false, // Disable CSP
|
|
600
|
-
xFrameOptions: false, // Disable X-Frame-Options
|
|
601
|
-
}
|
|
602
|
-
})
|
|
603
|
-
```
|
|
604
|
-
|
|
605
|
-
### Manual Header Creation
|
|
606
|
-
|
|
607
|
-
```typescript
|
|
608
|
-
import { createSecurityHeaders } from 'nextjs-secure/headers'
|
|
609
|
-
|
|
610
|
-
export async function GET() {
|
|
611
|
-
const headers = createSecurityHeaders({ preset: 'api' })
|
|
612
|
-
|
|
613
|
-
return new Response(JSON.stringify({ ok: true }), {
|
|
614
|
-
headers,
|
|
615
|
-
})
|
|
616
|
-
}
|
|
617
|
-
```
|
|
618
|
-
|
|
619
367
|
### Available Headers
|
|
620
368
|
|
|
621
369
|
| Header | Description |
|
|
@@ -630,243 +378,640 @@ export async function GET() {
|
|
|
630
378
|
| Cross-Origin-Embedder-Policy | Controls embedding |
|
|
631
379
|
| Cross-Origin-Resource-Policy | Controls resource sharing |
|
|
632
380
|
|
|
633
|
-
|
|
381
|
+
---
|
|
634
382
|
|
|
635
|
-
|
|
383
|
+
## Authentication
|
|
384
|
+
|
|
385
|
+
Flexible authentication supporting JWT, API keys, sessions, and RBAC.
|
|
386
|
+
|
|
387
|
+
### JWT Authentication
|
|
636
388
|
|
|
637
389
|
```typescript
|
|
638
|
-
import {
|
|
390
|
+
import { withJWT } from 'nextjs-secure/auth'
|
|
639
391
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
392
|
+
export const GET = withJWT(
|
|
393
|
+
async (req, ctx) => {
|
|
394
|
+
return Response.json({ user: ctx.user })
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
secret: process.env.JWT_SECRET,
|
|
398
|
+
algorithms: ['HS256', 'RS256'],
|
|
399
|
+
issuer: 'https://myapp.com',
|
|
400
|
+
audience: 'my-api',
|
|
401
|
+
}
|
|
402
|
+
)
|
|
403
|
+
```
|
|
643
404
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
405
|
+
### API Key Authentication
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
import { withAPIKey } from 'nextjs-secure/auth'
|
|
409
|
+
|
|
410
|
+
export const GET = withAPIKey(
|
|
411
|
+
async (req, ctx) => {
|
|
412
|
+
return Response.json({ user: ctx.user })
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
validate: async (apiKey) => {
|
|
416
|
+
const user = await db.users.findByApiKey(apiKey)
|
|
417
|
+
return user || null
|
|
418
|
+
},
|
|
419
|
+
headerName: 'x-api-key',
|
|
420
|
+
queryParam: 'api_key',
|
|
421
|
+
}
|
|
422
|
+
)
|
|
647
423
|
```
|
|
648
424
|
|
|
649
|
-
###
|
|
425
|
+
### Session Authentication
|
|
650
426
|
|
|
651
427
|
```typescript
|
|
652
|
-
import {
|
|
428
|
+
import { withSession } from 'nextjs-secure/auth'
|
|
653
429
|
|
|
654
|
-
|
|
655
|
-
|
|
430
|
+
export const GET = withSession(
|
|
431
|
+
async (req, ctx) => {
|
|
432
|
+
return Response.json({ user: ctx.user })
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
validate: async (sessionId) => {
|
|
436
|
+
const session = await db.sessions.find(sessionId)
|
|
437
|
+
return session?.user || null
|
|
438
|
+
},
|
|
439
|
+
cookieName: 'session',
|
|
440
|
+
}
|
|
441
|
+
)
|
|
442
|
+
```
|
|
656
443
|
|
|
657
|
-
|
|
658
|
-
const ip = getClientIp(request, {
|
|
659
|
-
trustProxy: true,
|
|
660
|
-
customHeaders: ['x-custom-ip'],
|
|
661
|
-
fallback: '0.0.0.0',
|
|
662
|
-
})
|
|
444
|
+
### Role-Based Access Control
|
|
663
445
|
|
|
664
|
-
|
|
665
|
-
|
|
446
|
+
```typescript
|
|
447
|
+
import { withJWT, withRoles } from 'nextjs-secure/auth'
|
|
666
448
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
449
|
+
export const GET = withJWT(
|
|
450
|
+
withRoles(
|
|
451
|
+
async (req, ctx) => {
|
|
452
|
+
return Response.json({ admin: true })
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
roles: ['admin', 'moderator'],
|
|
456
|
+
permissions: ['users:read', 'users:write'],
|
|
457
|
+
}
|
|
458
|
+
),
|
|
459
|
+
{ secret: process.env.JWT_SECRET }
|
|
460
|
+
)
|
|
670
461
|
```
|
|
671
462
|
|
|
672
|
-
|
|
463
|
+
### Combined Authentication
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
import { withAuth } from 'nextjs-secure/auth'
|
|
467
|
+
|
|
468
|
+
export const GET = withAuth(
|
|
469
|
+
async (req, ctx) => {
|
|
470
|
+
return Response.json({ user: ctx.user })
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
jwt: { secret: process.env.JWT_SECRET },
|
|
474
|
+
apiKey: { validate: (key) => db.apiKeys.findUser(key) },
|
|
475
|
+
session: { validate: (id) => db.sessions.findUser(id) },
|
|
476
|
+
rbac: { roles: ['user', 'admin'] },
|
|
477
|
+
}
|
|
478
|
+
)
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Optional Authentication
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
import { withOptionalAuth } from 'nextjs-secure/auth'
|
|
485
|
+
|
|
486
|
+
export const GET = withOptionalAuth(
|
|
487
|
+
async (req, ctx) => {
|
|
488
|
+
if (ctx.user) {
|
|
489
|
+
return Response.json({ user: ctx.user })
|
|
490
|
+
}
|
|
491
|
+
return Response.json({ guest: true })
|
|
492
|
+
},
|
|
493
|
+
{ jwt: { secret: process.env.JWT_SECRET } }
|
|
494
|
+
)
|
|
495
|
+
```
|
|
673
496
|
|
|
674
|
-
|
|
497
|
+
---
|
|
675
498
|
|
|
676
|
-
|
|
499
|
+
## Input Validation
|
|
500
|
+
|
|
501
|
+
Validate and sanitize user input to prevent attacks.
|
|
502
|
+
|
|
503
|
+
### Schema Validation
|
|
677
504
|
|
|
678
505
|
```typescript
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
onLimit?: (req: NextRequest, info: RateLimitInfo) => Response | Promise<Response>
|
|
688
|
-
prefix?: string
|
|
689
|
-
message?: string
|
|
690
|
-
statusCode?: number
|
|
506
|
+
import { withValidation } from 'nextjs-secure/validation'
|
|
507
|
+
|
|
508
|
+
// Built-in schema
|
|
509
|
+
const schema = {
|
|
510
|
+
email: { type: 'email', required: true },
|
|
511
|
+
password: { type: 'string', minLength: 8, maxLength: 100 },
|
|
512
|
+
age: { type: 'number', min: 18, max: 120 },
|
|
513
|
+
role: { type: 'string', enum: ['user', 'admin'] },
|
|
691
514
|
}
|
|
515
|
+
|
|
516
|
+
export const POST = withValidation(handler, { body: schema })
|
|
517
|
+
|
|
518
|
+
// Or use Zod
|
|
519
|
+
import { z } from 'zod'
|
|
520
|
+
|
|
521
|
+
const zodSchema = z.object({
|
|
522
|
+
email: z.string().email(),
|
|
523
|
+
password: z.string().min(8),
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
export const POST = withValidation(handler, { body: zodSchema })
|
|
692
527
|
```
|
|
693
528
|
|
|
694
|
-
###
|
|
529
|
+
### XSS Protection
|
|
530
|
+
|
|
531
|
+
```typescript
|
|
532
|
+
import { withXSSProtection, withSanitization, sanitize, detectXSS } from 'nextjs-secure/validation'
|
|
533
|
+
|
|
534
|
+
// Block XSS attempts
|
|
535
|
+
export const POST = withXSSProtection(handler)
|
|
695
536
|
|
|
696
|
-
|
|
537
|
+
// Sanitize specific fields
|
|
538
|
+
export const POST = withSanitization(handler, {
|
|
539
|
+
fields: ['content', 'bio'],
|
|
540
|
+
mode: 'escape', // 'escape' | 'strip' | 'allow-safe'
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
// Manual sanitization
|
|
544
|
+
const clean = sanitize(userInput, {
|
|
545
|
+
mode: 'allow-safe',
|
|
546
|
+
allowedTags: ['b', 'i', 'em', 'strong'],
|
|
547
|
+
})
|
|
697
548
|
|
|
698
|
-
|
|
549
|
+
// Detection only
|
|
550
|
+
const { hasXSS, matches } = detectXSS(input)
|
|
551
|
+
```
|
|
699
552
|
|
|
700
|
-
|
|
553
|
+
### SQL Injection Protection
|
|
701
554
|
|
|
702
|
-
Returns:
|
|
703
555
|
```typescript
|
|
704
|
-
{
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
556
|
+
import { withSQLProtection, detectSQLInjection, hasSQLInjection } from 'nextjs-secure/validation'
|
|
557
|
+
|
|
558
|
+
// Block SQL injection
|
|
559
|
+
export const POST = withSQLProtection(handler, {
|
|
560
|
+
mode: 'block', // 'block' | 'detect'
|
|
561
|
+
minSeverity: 'medium', // 'low' | 'medium' | 'high'
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
// Manual detection
|
|
565
|
+
const result = detectSQLInjection(input)
|
|
566
|
+
// { hasSQLi: true, severity: 'high', patterns: ['UNION SELECT'] }
|
|
567
|
+
|
|
568
|
+
// Simple check
|
|
569
|
+
if (hasSQLInjection(input)) {
|
|
570
|
+
// Block request
|
|
709
571
|
}
|
|
710
572
|
```
|
|
711
573
|
|
|
712
|
-
###
|
|
574
|
+
### Path Traversal Prevention
|
|
713
575
|
|
|
714
576
|
```typescript
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
577
|
+
import { validatePath, sanitizePath, sanitizeFilename } from 'nextjs-secure/validation'
|
|
578
|
+
|
|
579
|
+
// Validate path
|
|
580
|
+
const result = validatePath(userPath, {
|
|
581
|
+
basePath: '/uploads',
|
|
582
|
+
allowedExtensions: ['.jpg', '.png'],
|
|
583
|
+
maxDepth: 3,
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
if (!result.valid) {
|
|
587
|
+
console.log(result.reason) // 'traversal_detected', 'invalid_extension', etc.
|
|
721
588
|
}
|
|
589
|
+
|
|
590
|
+
// Sanitize
|
|
591
|
+
const safePath = sanitizePath('../../../etc/passwd') // 'etc/passwd'
|
|
592
|
+
const safeFilename = sanitizeFilename('../../evil.exe') // 'evil.exe'
|
|
722
593
|
```
|
|
723
594
|
|
|
724
|
-
|
|
595
|
+
### File Validation
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
import { withFileValidation, validateFile } from 'nextjs-secure/validation'
|
|
725
599
|
|
|
726
|
-
|
|
600
|
+
export const POST = withFileValidation(handler, {
|
|
601
|
+
maxSize: 5 * 1024 * 1024, // 5MB
|
|
602
|
+
allowedTypes: ['image/jpeg', 'image/png', 'application/pdf'],
|
|
603
|
+
validateMagicNumbers: true, // Check actual file content
|
|
604
|
+
maxFiles: 10,
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
// Manual validation
|
|
608
|
+
const result = await validateFile(file, {
|
|
609
|
+
maxSize: 5 * 1024 * 1024,
|
|
610
|
+
allowedTypes: ['image/jpeg'],
|
|
611
|
+
})
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### Combined Security Validation
|
|
727
615
|
|
|
728
616
|
```typescript
|
|
729
|
-
|
|
730
|
-
import { withRateLimit } from 'nextjs-secure'
|
|
617
|
+
import { withSecureValidation } from 'nextjs-secure/validation'
|
|
731
618
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
619
|
+
export const POST = withSecureValidation(handler, {
|
|
620
|
+
xss: true,
|
|
621
|
+
sql: { minSeverity: 'medium' },
|
|
622
|
+
contentType: ['application/json'],
|
|
623
|
+
})
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
## Audit Logging
|
|
629
|
+
|
|
630
|
+
Track requests and security events for monitoring and compliance.
|
|
631
|
+
|
|
632
|
+
### Request Logging
|
|
633
|
+
|
|
634
|
+
```typescript
|
|
635
|
+
import { withAuditLog, MemoryStore, ConsoleStore } from 'nextjs-secure/audit'
|
|
636
|
+
|
|
637
|
+
const store = new MemoryStore({ maxEntries: 1000 })
|
|
638
|
+
|
|
639
|
+
export const POST = withAuditLog(handler, {
|
|
640
|
+
store,
|
|
641
|
+
include: {
|
|
642
|
+
ip: true,
|
|
643
|
+
userAgent: true,
|
|
644
|
+
headers: false,
|
|
645
|
+
query: true,
|
|
646
|
+
response: true,
|
|
647
|
+
duration: true,
|
|
648
|
+
},
|
|
649
|
+
exclude: {
|
|
650
|
+
paths: ['/health', '/metrics'],
|
|
651
|
+
methods: ['OPTIONS'],
|
|
652
|
+
statusCodes: [304],
|
|
653
|
+
},
|
|
654
|
+
pii: {
|
|
655
|
+
fields: ['password', 'token', 'ssn', 'creditCard'],
|
|
656
|
+
mode: 'mask', // 'mask' | 'hash' | 'remove'
|
|
657
|
+
},
|
|
658
|
+
})
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### Storage Backends
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
import { MemoryStore, ConsoleStore, createDatadogStore, MultiStore } from 'nextjs-secure/audit'
|
|
665
|
+
|
|
666
|
+
// Memory (development)
|
|
667
|
+
const memoryStore = new MemoryStore({ maxEntries: 1000, ttl: 3600000 })
|
|
668
|
+
|
|
669
|
+
// Console (development)
|
|
670
|
+
const consoleStore = new ConsoleStore({ colorize: true, level: 'info' })
|
|
671
|
+
|
|
672
|
+
// Datadog (production)
|
|
673
|
+
const datadogStore = createDatadogStore({
|
|
674
|
+
apiKey: process.env.DATADOG_API_KEY,
|
|
675
|
+
service: 'my-api',
|
|
676
|
+
environment: 'production',
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
// Multiple stores
|
|
680
|
+
const multiStore = new MultiStore([consoleStore, datadogStore])
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### Security Event Tracking
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
import { createSecurityTracker, trackSecurityEvent } from 'nextjs-secure/audit'
|
|
687
|
+
|
|
688
|
+
const tracker = createSecurityTracker({ store })
|
|
689
|
+
|
|
690
|
+
// Authentication failures
|
|
691
|
+
await tracker.authFailed({
|
|
692
|
+
ip: '192.168.1.1',
|
|
693
|
+
email: 'user@example.com',
|
|
694
|
+
reason: 'Invalid password',
|
|
736
695
|
})
|
|
737
696
|
|
|
738
|
-
//
|
|
739
|
-
|
|
697
|
+
// Rate limit exceeded
|
|
698
|
+
await tracker.rateLimitExceeded({
|
|
699
|
+
ip: '192.168.1.1',
|
|
700
|
+
endpoint: '/api/login',
|
|
740
701
|
limit: 10,
|
|
741
|
-
window: '
|
|
702
|
+
window: '15m',
|
|
742
703
|
})
|
|
743
704
|
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
705
|
+
// XSS detected
|
|
706
|
+
await tracker.xssDetected({
|
|
707
|
+
ip: '192.168.1.1',
|
|
708
|
+
field: 'comment',
|
|
709
|
+
endpoint: '/api/comments',
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
// SQL injection detected
|
|
713
|
+
await tracker.sqliDetected({
|
|
714
|
+
ip: '192.168.1.1',
|
|
715
|
+
field: 'username',
|
|
716
|
+
pattern: 'UNION SELECT',
|
|
717
|
+
severity: 'high',
|
|
718
|
+
endpoint: '/api/users',
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
// CSRF validation failure
|
|
722
|
+
await tracker.csrfInvalid({
|
|
723
|
+
ip: '192.168.1.1',
|
|
724
|
+
endpoint: '/api/transfer',
|
|
725
|
+
reason: 'Token mismatch',
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
// IP blocked
|
|
729
|
+
await tracker.ipBlocked({
|
|
730
|
+
ip: '192.168.1.1',
|
|
731
|
+
reason: 'Too many failed attempts',
|
|
732
|
+
duration: 3600,
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
// Custom events
|
|
736
|
+
await tracker.custom({
|
|
737
|
+
message: 'Suspicious activity detected',
|
|
738
|
+
severity: 'high',
|
|
739
|
+
details: { pattern: 'automated_scanning' },
|
|
748
740
|
})
|
|
749
741
|
```
|
|
750
742
|
|
|
751
|
-
###
|
|
743
|
+
### PII Redaction
|
|
752
744
|
|
|
753
745
|
```typescript
|
|
754
|
-
|
|
755
|
-
import { createRateLimiter, createUpstashStore } from 'nextjs-secure/rate-limit'
|
|
746
|
+
import { redactObject, redactEmail, redactCreditCard, redactIP, DEFAULT_PII_FIELDS } from 'nextjs-secure/audit'
|
|
756
747
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
748
|
+
// Redact object
|
|
749
|
+
const safeData = redactObject(userData, {
|
|
750
|
+
fields: DEFAULT_PII_FIELDS,
|
|
751
|
+
mode: 'mask',
|
|
760
752
|
})
|
|
761
753
|
|
|
762
|
-
//
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
754
|
+
// Specific redactors
|
|
755
|
+
redactEmail('john@example.com') // '****@example.com'
|
|
756
|
+
redactCreditCard('4111111111111111') // '**** **** **** 1111'
|
|
757
|
+
redactIP('192.168.1.100') // '192.168.*.*'
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
### Request ID & Timing
|
|
761
|
+
|
|
762
|
+
```typescript
|
|
763
|
+
import { withRequestId, withTiming } from 'nextjs-secure/audit'
|
|
764
|
+
|
|
765
|
+
// Add request ID to responses
|
|
766
|
+
export const GET = withRequestId(handler, {
|
|
767
|
+
headerName: 'x-request-id',
|
|
768
|
+
generateId: () => `req_${Date.now()}`,
|
|
771
769
|
})
|
|
772
770
|
|
|
773
|
-
//
|
|
774
|
-
export const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
store,
|
|
778
|
-
identifier: async (req) => {
|
|
779
|
-
const apiKey = req.headers.get('x-api-key')
|
|
780
|
-
return `pro:${apiKey}`
|
|
781
|
-
},
|
|
771
|
+
// Add response timing
|
|
772
|
+
export const GET = withTiming(handler, {
|
|
773
|
+
headerName: 'x-response-time',
|
|
774
|
+
log: true,
|
|
782
775
|
})
|
|
783
776
|
```
|
|
784
777
|
|
|
785
|
-
###
|
|
778
|
+
### Log Formatters
|
|
786
779
|
|
|
787
780
|
```typescript
|
|
788
|
-
import {
|
|
789
|
-
import { getServerSession } from 'next-auth'
|
|
781
|
+
import { JSONFormatter, TextFormatter, CLFFormatter, StructuredFormatter } from 'nextjs-secure/audit'
|
|
790
782
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
const session = await getServerSession()
|
|
783
|
+
// JSON (default)
|
|
784
|
+
const jsonFormatter = new JSONFormatter({ pretty: true })
|
|
794
785
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
786
|
+
// Human-readable text
|
|
787
|
+
const textFormatter = new TextFormatter({
|
|
788
|
+
template: '{timestamp} [{level}] {message}',
|
|
789
|
+
})
|
|
798
790
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
},
|
|
808
|
-
}
|
|
809
|
-
)
|
|
791
|
+
// Apache/Nginx Common Log Format
|
|
792
|
+
const clfFormatter = new CLFFormatter()
|
|
793
|
+
|
|
794
|
+
// Key=value (ELK/Splunk)
|
|
795
|
+
const structuredFormatter = new StructuredFormatter({
|
|
796
|
+
delimiter: ' ',
|
|
797
|
+
kvSeparator: '=',
|
|
798
|
+
})
|
|
810
799
|
```
|
|
811
800
|
|
|
812
|
-
|
|
801
|
+
---
|
|
802
|
+
|
|
803
|
+
## Utilities
|
|
804
|
+
|
|
805
|
+
### Duration Parsing
|
|
813
806
|
|
|
814
807
|
```typescript
|
|
815
|
-
import {
|
|
816
|
-
import { headers } from 'next/headers'
|
|
817
|
-
import crypto from 'crypto'
|
|
808
|
+
import { parseDuration, formatDuration } from 'nextjs-secure'
|
|
818
809
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
.createHmac('sha256', process.env.WEBHOOK_SECRET!)
|
|
827
|
-
.update(body)
|
|
828
|
-
.digest('hex')
|
|
829
|
-
|
|
830
|
-
if (signature !== expected) {
|
|
831
|
-
return Response.json({ error: 'Invalid signature' }, { status: 401 })
|
|
832
|
-
}
|
|
810
|
+
parseDuration('15m') // 900000
|
|
811
|
+
parseDuration('1h 30m') // 5400000
|
|
812
|
+
parseDuration('2d') // 172800000
|
|
813
|
+
|
|
814
|
+
formatDuration(900000) // '15m'
|
|
815
|
+
formatDuration(5400000) // '1h 30m'
|
|
816
|
+
```
|
|
833
817
|
|
|
834
|
-
|
|
835
|
-
const data = JSON.parse(body)
|
|
836
|
-
await processWebhook(data)
|
|
818
|
+
### IP Utilities
|
|
837
819
|
|
|
838
|
-
|
|
839
|
-
|
|
820
|
+
```typescript
|
|
821
|
+
import { getClientIp, anonymizeIp, isPrivateIp, isLocalhost } from 'nextjs-secure'
|
|
822
|
+
|
|
823
|
+
// Extract client IP
|
|
824
|
+
const ip = getClientIp(request, {
|
|
825
|
+
trustProxy: true,
|
|
826
|
+
customHeaders: ['x-custom-ip'],
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
// Anonymize for GDPR
|
|
830
|
+
anonymizeIp('192.168.1.100') // '192.168.1.xxx'
|
|
831
|
+
|
|
832
|
+
// Check IP type
|
|
833
|
+
isPrivateIp('192.168.1.1') // true
|
|
834
|
+
isPrivateIp('8.8.8.8') // false
|
|
835
|
+
isLocalhost('127.0.0.1') // true
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
---
|
|
839
|
+
|
|
840
|
+
## API Reference
|
|
841
|
+
|
|
842
|
+
### Rate Limiting
|
|
843
|
+
|
|
844
|
+
| Function | Description |
|
|
845
|
+
|----------|-------------|
|
|
846
|
+
| `withRateLimit(handler, config)` | Wrap handler with rate limiting |
|
|
847
|
+
| `createRateLimiter(config)` | Create reusable rate limiter |
|
|
848
|
+
| `checkRateLimit(request, config)` | Manual rate limit check |
|
|
849
|
+
| `getRateLimitStatus(key, config)` | Get current status without incrementing |
|
|
850
|
+
| `resetRateLimit(key, config)` | Reset rate limit for key |
|
|
851
|
+
|
|
852
|
+
### CSRF
|
|
853
|
+
|
|
854
|
+
| Function | Description |
|
|
855
|
+
|----------|-------------|
|
|
856
|
+
| `withCSRF(handler, config)` | Wrap handler with CSRF protection |
|
|
857
|
+
| `generateCSRF(config)` | Generate CSRF token and cookie |
|
|
858
|
+
| `validateCSRF(request, config)` | Manual CSRF validation |
|
|
859
|
+
|
|
860
|
+
### Security Headers
|
|
861
|
+
|
|
862
|
+
| Function | Description |
|
|
863
|
+
|----------|-------------|
|
|
864
|
+
| `withSecurityHeaders(handler, config)` | Add security headers |
|
|
865
|
+
| `createSecurityHeaders(config)` | Create headers object |
|
|
866
|
+
| `buildCSP(config)` | Build CSP header string |
|
|
867
|
+
| `getPreset(name)` | Get preset configuration |
|
|
868
|
+
|
|
869
|
+
### Authentication
|
|
870
|
+
|
|
871
|
+
| Function | Description |
|
|
872
|
+
|----------|-------------|
|
|
873
|
+
| `withJWT(handler, config)` | JWT authentication |
|
|
874
|
+
| `withAPIKey(handler, config)` | API key authentication |
|
|
875
|
+
| `withSession(handler, config)` | Session authentication |
|
|
876
|
+
| `withAuth(handler, config)` | Combined authentication |
|
|
877
|
+
| `withRoles(handler, config)` | Role-based access control |
|
|
878
|
+
| `withOptionalAuth(handler, config)` | Optional authentication |
|
|
879
|
+
| `verifyJWT(token, config)` | Verify JWT token |
|
|
880
|
+
| `decodeJWT(token)` | Decode JWT without verification |
|
|
881
|
+
|
|
882
|
+
### Validation
|
|
883
|
+
|
|
884
|
+
| Function | Description |
|
|
885
|
+
|----------|-------------|
|
|
886
|
+
| `withValidation(handler, config)` | Schema validation |
|
|
887
|
+
| `withXSSProtection(handler)` | Block XSS attempts |
|
|
888
|
+
| `withSanitization(handler, config)` | Sanitize input |
|
|
889
|
+
| `withSQLProtection(handler, config)` | Block SQL injection |
|
|
890
|
+
| `withFileValidation(handler, config)` | File upload validation |
|
|
891
|
+
| `sanitize(input, config)` | Manual sanitization |
|
|
892
|
+
| `detectXSS(input)` | Detect XSS patterns |
|
|
893
|
+
| `detectSQLInjection(input)` | Detect SQL injection |
|
|
894
|
+
| `validatePath(path, config)` | Validate file path |
|
|
895
|
+
|
|
896
|
+
### Audit Logging
|
|
897
|
+
|
|
898
|
+
| Function | Description |
|
|
899
|
+
|----------|-------------|
|
|
900
|
+
| `withAuditLog(handler, config)` | Request logging |
|
|
901
|
+
| `withRequestId(handler, config)` | Add request ID |
|
|
902
|
+
| `withTiming(handler, config)` | Add response timing |
|
|
903
|
+
| `createSecurityTracker(config)` | Create event tracker |
|
|
904
|
+
| `trackSecurityEvent(store, event)` | Track single event |
|
|
905
|
+
| `redactObject(obj, config)` | Redact PII from object |
|
|
906
|
+
|
|
907
|
+
---
|
|
908
|
+
|
|
909
|
+
## Examples
|
|
910
|
+
|
|
911
|
+
### Complete API with All Security Features
|
|
912
|
+
|
|
913
|
+
```typescript
|
|
914
|
+
// lib/security.ts
|
|
915
|
+
import { createRateLimiter, MemoryStore } from 'nextjs-secure/rate-limit'
|
|
916
|
+
import { createSecurityTracker, MemoryStore as AuditStore } from 'nextjs-secure/audit'
|
|
917
|
+
|
|
918
|
+
export const apiLimiter = createRateLimiter({
|
|
919
|
+
limit: 100,
|
|
920
|
+
window: '15m',
|
|
921
|
+
store: new MemoryStore(),
|
|
922
|
+
})
|
|
923
|
+
|
|
924
|
+
export const strictLimiter = createRateLimiter({
|
|
925
|
+
limit: 5,
|
|
926
|
+
window: '1m',
|
|
927
|
+
})
|
|
928
|
+
|
|
929
|
+
export const auditStore = new AuditStore({ maxEntries: 10000 })
|
|
930
|
+
export const securityTracker = createSecurityTracker({ store: auditStore })
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
```typescript
|
|
934
|
+
// app/api/users/route.ts
|
|
935
|
+
import { withJWT, withRoles } from 'nextjs-secure/auth'
|
|
936
|
+
import { withValidation } from 'nextjs-secure/validation'
|
|
937
|
+
import { withAuditLog } from 'nextjs-secure/audit'
|
|
938
|
+
import { apiLimiter, auditStore, securityTracker } from '@/lib/security'
|
|
939
|
+
|
|
940
|
+
const createUserSchema = {
|
|
941
|
+
email: { type: 'email', required: true },
|
|
942
|
+
name: { type: 'string', minLength: 2, maxLength: 100 },
|
|
943
|
+
role: { type: 'string', enum: ['user', 'admin'] },
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
async function createUser(req, ctx) {
|
|
947
|
+
const { email, name, role } = ctx.validated
|
|
948
|
+
const user = await db.users.create({ email, name, role })
|
|
949
|
+
return Response.json(user, { status: 201 })
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
export const POST = withAuditLog(
|
|
953
|
+
apiLimiter(
|
|
954
|
+
withJWT(
|
|
955
|
+
withRoles(
|
|
956
|
+
withValidation(createUser, { body: createUserSchema }),
|
|
957
|
+
{ roles: ['admin'] }
|
|
958
|
+
),
|
|
959
|
+
{ secret: process.env.JWT_SECRET }
|
|
960
|
+
)
|
|
961
|
+
),
|
|
840
962
|
{
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
// Rate limit by webhook source
|
|
845
|
-
return req.headers.get('x-webhook-source') ?? 'unknown'
|
|
846
|
-
},
|
|
963
|
+
store: auditStore,
|
|
964
|
+
include: { ip: true, userAgent: true },
|
|
965
|
+
pii: { fields: ['password'], mode: 'remove' },
|
|
847
966
|
}
|
|
848
967
|
)
|
|
849
968
|
```
|
|
850
969
|
|
|
970
|
+
### Tiered Rate Limiting
|
|
971
|
+
|
|
972
|
+
```typescript
|
|
973
|
+
import { createRateLimiter, createUpstashStore } from 'nextjs-secure/rate-limit'
|
|
974
|
+
|
|
975
|
+
const store = createUpstashStore({
|
|
976
|
+
url: process.env.UPSTASH_REDIS_REST_URL,
|
|
977
|
+
token: process.env.UPSTASH_REDIS_REST_TOKEN,
|
|
978
|
+
})
|
|
979
|
+
|
|
980
|
+
const freeLimiter = createRateLimiter({
|
|
981
|
+
limit: 100,
|
|
982
|
+
window: '1d',
|
|
983
|
+
store,
|
|
984
|
+
identifier: (req) => `free:${req.headers.get('x-api-key')}`,
|
|
985
|
+
})
|
|
986
|
+
|
|
987
|
+
const proLimiter = createRateLimiter({
|
|
988
|
+
limit: 10000,
|
|
989
|
+
window: '1d',
|
|
990
|
+
store,
|
|
991
|
+
identifier: (req) => `pro:${req.headers.get('x-api-key')}`,
|
|
992
|
+
})
|
|
993
|
+
|
|
994
|
+
export async function GET(req) {
|
|
995
|
+
const tier = await getUserTier(req)
|
|
996
|
+
const limiter = tier === 'pro' ? proLimiter : freeLimiter
|
|
997
|
+
return limiter(handler)(req)
|
|
998
|
+
}
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
---
|
|
1002
|
+
|
|
851
1003
|
## Roadmap
|
|
852
1004
|
|
|
853
|
-
- [x]
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
- [ ] NextAuth provider
|
|
864
|
-
- [ ] Clerk provider
|
|
865
|
-
- [ ] RBAC support
|
|
866
|
-
- [ ] CSRF Protection (v0.3.0)
|
|
867
|
-
- [ ] Security Headers (v0.4.0)
|
|
868
|
-
- [ ] Input Validation (v0.5.0)
|
|
869
|
-
- [ ] Audit Logging (v0.6.0)
|
|
1005
|
+
- [x] **v0.1.x** - Rate Limiting
|
|
1006
|
+
- [x] **v0.2.0** - CSRF Protection
|
|
1007
|
+
- [x] **v0.3.0** - Security Headers
|
|
1008
|
+
- [x] **v0.4.0** - Authentication
|
|
1009
|
+
- [x] **v0.5.0** - Input Validation
|
|
1010
|
+
- [x] **v0.6.0** - Audit Logging
|
|
1011
|
+
|
|
1012
|
+
See [ROADMAP.md](ROADMAP.md) for detailed progress and future plans.
|
|
1013
|
+
|
|
1014
|
+
---
|
|
870
1015
|
|
|
871
1016
|
## Contributing
|
|
872
1017
|
|
|
@@ -887,10 +1032,20 @@ npm test
|
|
|
887
1032
|
npm run build
|
|
888
1033
|
```
|
|
889
1034
|
|
|
1035
|
+
### Running Tests
|
|
1036
|
+
|
|
1037
|
+
```bash
|
|
1038
|
+
npm run test # Watch mode
|
|
1039
|
+
npm run test:run # Single run
|
|
1040
|
+
npm run test:coverage # With coverage
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
---
|
|
1044
|
+
|
|
890
1045
|
## License
|
|
891
1046
|
|
|
892
1047
|
MIT License - see [LICENSE](LICENSE) for details.
|
|
893
1048
|
|
|
894
1049
|
---
|
|
895
1050
|
|
|
896
|
-
**
|
|
1051
|
+
**Made with security in mind for the Next.js community.**
|