Skip to content

Commit 6238f4e

Browse files
Feat: Implement fixes and improvements from security audit (v2)
This commit implements critical fixes, feature enhancements, and documentation updates based on a security audit of the nuxt-api-shield module. This version corrects previous misplacement of utility functions and ensures proper module configuration. Core Logic & Reliability: - Fixed flawed rate limit window logic in `src/runtime/server/middleware/shield.ts` to ensure accurate request counting and window resets, preventing bypass of limits. - IP data is now reset correctly upon ban expiry and when a new ban is applied. - Created `isActualBanTimestampExpired` utility in `src/runtime/server/utils/` for correctly checking ban expiry. - Removed the old, misleading `isBanExpired.ts` utility. Module Configuration & Exports (`src/module.ts`): - Added new configuration options `ipTTL` (for IP data cleanup TTL) and `security.trustXForwardedFor` (for XFF header trust) to `ModuleOptions` and module defaults. - Ensured `isActualBanTimestampExpired` and `RateLimit` type are exported via `addServerImports` to be available through `#imports` for user-defined tasks. - Corrected `defu` strategy for runtime config to align with Nuxt standards. Cleanup Tasks & Storage Management: - Updated `playground/server/tasks/shield/cleanBans.ts` and README example to use the core `isActualBanTimestampExpired` utility (via `#imports`) and efficiently process only `ban:` keys. - Created `playground/server/tasks/shield/cleanIpData.ts` and corresponding README example to clean old IP tracking entries based on the `ipTTL` config, preventing storage bloat. Security Configuration & Hardening: - Made `X-Forwarded-For` header trust configurable via `security.trustXForwardedFor` in `nuxtApiShield` options. Middleware now respects this. Documentation (README.md): - Extensively updated README: - Added examples for `shield:cleanBans` and `shield:cleanIpData` tasks. - Documented new config options: `ipTTL` and `security.trustXForwardedFor`. - Added "Important Considerations" section: Data Privacy (PII), Storage Security (FS driver, logs), `errorMessage` XSS note. - Added prominent security warning and guidance for `trustXForwardedFor`. - Clarified default configuration values. These changes significantly improve the module's security, reliability, and configurability, with clearer guidance for users.
1 parent 89f9986 commit 6238f4e

File tree

7 files changed

+1200
-75
lines changed

7 files changed

+1200
-75
lines changed

README.md

Lines changed: 145 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,46 @@ export default defineNuxtConfig({
7575
routes: [], // specify routes to apply rate limiting to, default is an empty array meaning all routes are protected.
7676
// Example:
7777
// routes: ["/api/v2/", "/api/v3/"], // /api/v1 will not be protected, /api/v2/ and /api/v3/ will be protected */
78+
ipTTL: 604800, // Optional: Time-to-live in seconds for IP tracking entries (default: 7 days). Set to 0 or negative to disable this specific cleanup.
79+
security: { // Optional: Security-related configurations
80+
trustXForwardedFor: true, // Default: true. Whether to trust X-Forwarded-For headers. See warning below.
81+
}
7882
},
7983
});
8084
```
8185

86+
**Default Configuration Values:**
87+
(These are applied by the module if not specified in your `nuxtApiShield` config)
88+
```js
89+
{
90+
limit: {
91+
max: 12,
92+
duration: 108, // seconds
93+
ban: 3600, // seconds
94+
},
95+
delayOnBan: true,
96+
errorMessage: "Too Many Requests",
97+
retryAfterHeader: false,
98+
log: {
99+
path: "logs", // Logging is disabled if path is empty
100+
attempts: 100, // Logging per IP is disabled if attempts is 0
101+
},
102+
routes: [],
103+
ipTTL: 7 * 24 * 60 * 60, // 7 days in seconds
104+
security: {
105+
trustXForwardedFor: true,
106+
}
107+
}
108+
```
109+
110+
**Security Warning: `trustXForwardedFor`**
111+
112+
The `security.trustXForwardedFor` option (default is `true`, set by the module) determines if the module uses the `X-Forwarded-For` HTTP header to identify the client's IP address.
113+
- If set to `true`: The module will use the IP address provided in the `X-Forwarded-For` header. This is common when your Nuxt application is behind a trusted reverse proxy, load balancer, or CDN (like Nginx, Cloudflare, AWS ELB/ALB) that correctly sets this header with the real client IP.
114+
- **WARNING:** If `trustXForwardedFor` is `true` and your application is directly internet-facing OR your proxy is not configured to strip incoming `X-Forwarded-For` headers from clients, malicious users can spoof their IP address by sending a fake `X-Forwarded-For` header. This would allow them to bypass rate limits or cause other users to be incorrectly rate-limited.
115+
- If set to `false`: The module will use the direct IP address of the incoming connection (i.e., `event.node.req.socket.remoteAddress`). Use this setting if your application is directly internet-facing or if you are unsure about your proxy's configuration.
116+
- **Recommendation:** Only enable `trustXForwardedFor: true` if you are certain your reverse proxy is correctly configured to set this header and strip any client-sent versions of it. Otherwise, set it to `false`.
117+
82118
### 3. Add `nitro/storage` to `nuxt.config.ts`
83119

84120
You can use any storage you want, but you have to use **shield** as the name of the storage.
@@ -112,7 +148,7 @@ If you use for example redis, you can use the following configuration, define th
112148
}
113149
```
114150

115-
### 4. Add `shield:clean` to `nuxt.config.ts`
151+
### 4. Add Cleanup Task(s) to `nuxt.config.ts`
116152

117153
```json
118154
{
@@ -121,37 +157,133 @@ If you use for example redis, you can use the following configuration, define th
121157
"tasks": true
122158
},
123159
"scheduledTasks": {
124-
"*/15 * * * *": ["shield:clean"] // clean the shield storage every 15 minutes
160+
"*/15 * * * *": ["shield:cleanBans"], // Example: clean expired bans every 15 minutes
161+
"0 0 * * *": ["shield:cleanIpData"] // Example: clean old IP data daily at midnight
125162
}
126163
}
127164
}
128165
```
129166

130-
### 5. Create your `clean` task
167+
### 5. Create your Cleanup Task(s)
168+
169+
It's recommended to clean up expired bans and old IP tracking data regularly to prevent storage bloat and ensure good performance.
170+
171+
#### a) Task for Cleaning Expired Bans
172+
173+
This task removes ban entries (`ban:xxx.xxx.xxx.xxx`) from storage once their ban period has passed.
174+
175+
In `server/tasks/shield/cleanBans.ts` (you can name the file and task as you like):
176+
177+
```ts
178+
import { isActualBanTimestampExpired } from '#imports'; // Auto-imported utility from nuxt-api-shield
179+
180+
export default defineTask({
181+
meta: {
182+
name: 'shield:cleanBans', // Match the name in scheduledTasks
183+
description: 'Clean expired bans from nuxt-api-shield storage.',
184+
},
185+
async run() {
186+
const shieldStorage = useStorage('shield'); // Use your configured storage name
187+
188+
// Only fetch keys that start with the 'ban:' prefix
189+
const banKeys = await shieldStorage.getKeys('ban:');
190+
191+
let cleanedCount = 0;
192+
for (const key of banKeys) {
193+
const bannedUntilRaw = await shieldStorage.getItem(key);
194+
if (isActualBanTimestampExpired(bannedUntilRaw)) {
195+
await shieldStorage.removeItem(key);
196+
cleanedCount++;
197+
}
198+
}
199+
console.log(`[nuxt-api-shield] Cleaned ${cleanedCount} expired ban(s).`);
200+
return { result: { cleanedCount } };
201+
},
202+
});
203+
```
204+
The `isActualBanTimestampExpired` utility is provided by `nuxt-api-shield` and should be available via `#imports`.
205+
206+
#### b) Task for Cleaning Old IP Tracking Data
131207

132-
In `server/tasks/shield/clean.ts` you should have something like this.
208+
This task cleans up IP tracking entries (`ip:xxx.xxx.xxx.xxx`) that haven't been active (i.e., their `time` field hasn't been updated) for a certain period. This period is defined by the `ipTTL` configuration option in your `nuxt.config.ts` (under `nuxtApiShield`), which defaults to 7 days. This cleanup helps prevent your storage from growing indefinitely with IPs that make a few requests but are never banned.
209+
210+
In `server/tasks/shield/cleanIpData.ts`:
133211

134212
```ts
135-
import type { RateLimit } from "#imports";
213+
import type { RateLimit } from '#imports'; // Or from 'nuxt-api-shield/types' if made available by the module
214+
import { useRuntimeConfig } from '#imports';
136215

137216
export default defineTask({
138217
meta: {
139-
description: "Clean expired bans",
218+
name: 'shield:cleanIpData', // Match the name in scheduledTasks
219+
description: 'Clean old IP tracking data from nuxt-api-shield storage.',
140220
},
141221
async run() {
142-
const shieldStorage = useStorage("shield");
222+
const shieldStorage = useStorage('shield');
223+
const config = useRuntimeConfig().public.nuxtApiShield;
224+
225+
// ipTTL is expected to be in seconds from config (module applies default if not set by user)
226+
const ipTTLseconds = config.ipTTL;
143227

144-
const keys = await shieldStorage.getKeys();
145-
keys.forEach(async (key) => {
146-
const rateLimit = (await shieldStorage.getItem(key)) as RateLimit;
147-
if (isBanExpired(rateLimit)) {
228+
if (!ipTTLseconds || ipTTLseconds <= 0) {
229+
console.log('[nuxt-api-shield] IP data cleanup (ipTTL) is disabled or invalid.');
230+
return { result: { cleanedCount: 0, status: 'disabled_or_invalid_ttl' } };
231+
}
232+
const ipTTLms = ipTTLseconds * 1000;
233+
234+
const ipKeys = await shieldStorage.getKeys('ip:');
235+
const currentTime = Date.now();
236+
let cleanedCount = 0;
237+
238+
for (const key of ipKeys) {
239+
const entry = await shieldStorage.getItem(key) as RateLimit | null;
240+
241+
// Check if entry exists and has a numeric 'time' property
242+
if (entry && typeof entry.time === 'number') {
243+
if ((currentTime - entry.time) > ipTTLms) {
244+
await shieldStorage.removeItem(key);
245+
cleanedCount++;
246+
}
247+
} else {
248+
// Clean up entries that are null, not an object, or missing a numeric 'time'
148249
await shieldStorage.removeItem(key);
250+
cleanedCount++;
149251
}
150-
});
151-
return { result: keys };
252+
}
253+
254+
console.log(`[nuxt-api-shield] Cleaned ${cleanedCount} old/malformed IP data entries.`);
255+
return { result: { cleanedCount } };
152256
},
153257
});
154258
```
259+
Make sure to configure `ipTTL` in your `nuxt.config.ts` under `nuxtApiShield` if you wish to use a value different from the default (7 days). Setting `ipTTL: 0` (or any non-positive number) in your config will disable this cleanup task. The `RateLimit` type should be available via `#imports` if your module exports it or makes it available to Nuxt's auto-import system.
260+
261+
## Important Considerations
262+
263+
### Data Privacy (IP Address Storage)
264+
265+
`nuxt-api-shield` functions by tracking IP addresses to monitor request rates and apply bans. This means IP addresses, which can be considered Personally Identifiable Information (PII) under regulations like GDPR, are stored by the module.
266+
267+
- **Data Stored:**
268+
- `ip:<IP_ADDRESS>`: Stores `{ count: number, time: number }` for tracking request rates.
269+
- `ban:<IP_ADDRESS>`: Stores a timestamp indicating when a ban on an IP address expires.
270+
- **Compliance:** Ensure your usage complies with any applicable data privacy regulations. This may involve updating your privacy policy to inform users about this data processing.
271+
- **Data Retention:**
272+
- Ban entries are cleaned up by the `shield:cleanBans` task after expiry.
273+
- IP tracking entries are cleaned up by the `shield:cleanIpData` task based on the `ipTTL` setting.
274+
275+
### Storage Security
276+
277+
- **Filesystem Driver (`driver: 'fs'`):** If you use the filesystem driver for `unstorage` (e.g., `driver: 'fs'`, `base: '.shield'`), ensure that the storage directory (and the `logs` directory if logging is enabled via `log.path`) is:
278+
- **Not web-accessible:** Your web server should not be configured to serve files from these directories.
279+
- **Properly permissioned:** The directories should have appropriate server-side file permissions to prevent unauthorized reading or writing.
280+
- **Other Drivers (Redis, etc.):** If using database drivers like Redis, ensure your database server itself is secured (e.g., authentication, network access controls).
281+
282+
### Error Message (`errorMessage`)
283+
284+
The `errorMessage` option in the module configuration is returned in the body of a 429 response.
285+
- It's recommended to use a plain text message.
286+
- If you choose to use HTML in your `errorMessage`, ensure your client-side application correctly sanitizes it or renders it in a way that prevents XSS vulnerabilities. The module itself does not sanitize this user-configured message.
155287

156288
## Development
157289

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { isActualBanTimestampExpired } from '#imports' // Assumes utility is auto-imported after module.ts update
2+
3+
export default defineTask({
4+
meta: {
5+
name: 'shield:cleanBans',
6+
description: 'Clean expired bans from nuxt-api-shield storage.',
7+
},
8+
async run() {
9+
const shieldStorage = useStorage('shield')
10+
// Only fetch keys that start with the 'ban:' prefix
11+
const banKeys = await shieldStorage.getKeys('ban:')
12+
13+
let cleanedCount = 0
14+
for (const key of banKeys) {
15+
const bannedUntilRaw = await shieldStorage.getItem(key)
16+
if (isActualBanTimestampExpired(bannedUntilRaw)) {
17+
await shieldStorage.removeItem(key)
18+
cleanedCount++
19+
}
20+
}
21+
console.log(`[nuxt-api-shield] Cleaned ${cleanedCount} expired ban(s).`)
22+
return { result: { cleanedCount } }
23+
},
24+
})
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { RateLimit } from '#imports'; // Assuming RateLimit type is made available via #imports
2+
import { useRuntimeConfig } from '#imports';
3+
4+
export default defineTask({
5+
meta: {
6+
name: 'shield:cleanIpData',
7+
description: 'Clean old IP tracking data from nuxt-api-shield storage.',
8+
},
9+
async run() {
10+
const shieldStorage = useStorage('shield');
11+
const config = useRuntimeConfig().public.nuxtApiShield;
12+
13+
// ipTTL is expected to be in seconds from config, module applies default if not set by user
14+
const ipTTLseconds = config.ipTTL;
15+
16+
if (!ipTTLseconds || ipTTLseconds <= 0) {
17+
console.log('[nuxt-api-shield] IP data cleanup (ipTTL) is disabled or invalid.');
18+
return { result: { cleanedCount: 0, status: 'disabled_or_invalid_ttl' } };
19+
}
20+
const ipTTLms = ipTTLseconds * 1000;
21+
22+
const ipKeys = await shieldStorage.getKeys('ip:');
23+
const currentTime = Date.now();
24+
let cleanedCount = 0;
25+
26+
for (const key of ipKeys) {
27+
const entry = await shieldStorage.getItem(key) as RateLimit | null;
28+
29+
if (entry && typeof entry.time === 'number') {
30+
if ((currentTime - entry.time) > ipTTLms) {
31+
await shieldStorage.removeItem(key);
32+
cleanedCount++;
33+
}
34+
} else {
35+
// Clean up potentially malformed (not RateLimit object), null, or missing 'time' property entries
36+
await shieldStorage.removeItem(key);
37+
cleanedCount++;
38+
}
39+
}
40+
41+
console.log(`[nuxt-api-shield] Cleaned ${cleanedCount} old/malformed IP data entries.`);
42+
return { result: { cleanedCount } };
43+
},
44+
});

0 commit comments

Comments
 (0)