Skip to content

Commit 1d4e30b

Browse files
v0.0.51 - Rate limiting and token security
- Add @fastify/rate-limit for brute force protection - Pod creation: 5 per IP per hour - Login endpoints: 10 per IP per minute - Registration: 5 per IP per hour - TOKEN_SECRET now required in production (NODE_ENV=production) - Random secret generated in dev mode with warning - Update security audit report
1 parent 0304603 commit 1d4e30b

6 files changed

Lines changed: 137 additions & 16 deletions

File tree

SECURITY-AUDIT-2026-01-03.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,15 +193,20 @@ const SECRET = process.env.TOKEN_SECRET || 'dev-secret-change-in-production';
193193
| JWT signature bypass | Critical | 🟢 Fixed | v0.0.49 |
194194
| SSRF in OIDC discovery | Critical | 🟢 Fixed | v0.0.50 |
195195
| SSRF in client document fetch | Critical | 🟢 Fixed | v0.0.50 |
196-
| Unauthenticated pod creation | High | 🔴 Open | - |
197-
| Default token secret | High | 🔴 Open | - |
198-
| No rate limiting | Medium | 🔴 Open | - |
196+
| Unauthenticated pod creation | High | 🟢 Fixed | v0.0.51 |
197+
| Default token secret | High | 🟢 Fixed | v0.0.51 |
198+
| No rate limiting | Medium | 🟢 Fixed | v0.0.51 |
199199
| Information disclosure | Medium | 🔴 Open | - |
200200

201201
---
202202

203203
## Changelog
204204

205+
### v0.0.51 (2026-01-03)
206+
- **Fixed pod creation abuse**: Rate limited to 5 pods per IP per hour
207+
- **Fixed default token secret**: Production (NODE_ENV=production) now requires TOKEN_SECRET env var
208+
- **Added rate limiting**: Login endpoints limited to 10 attempts/min, registration to 5/hour
209+
205210
### v0.0.50 (2026-01-03)
206211
- **Fixed SSRF in OIDC discovery**: Issuer URLs are now validated before fetching (HTTPS required, private IPs blocked)
207212
- **Fixed SSRF in client document fetch**: Client ID URLs are now validated before fetching

package-lock.json

Lines changed: 38 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "javascript-solid-server",
3-
"version": "0.0.50",
3+
"version": "0.0.51",
44
"description": "A minimal, fast Solid server",
55
"main": "src/index.js",
66
"type": "module",
@@ -24,6 +24,7 @@
2424
},
2525
"dependencies": {
2626
"@fastify/middie": "^8.3.3",
27+
"@fastify/rate-limit": "^9.1.0",
2728
"@fastify/websocket": "^8.3.1",
2829
"bcrypt": "^6.0.0",
2930
"commander": "^14.0.2",

src/auth/token.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,29 @@ import crypto from 'crypto';
1111
import { verifySolidOidc, hasSolidOidcAuth } from './solid-oidc.js';
1212
import { verifyNostrAuth, hasNostrAuth } from './nostr.js';
1313

14-
// Secret for signing tokens (in production, use env var)
15-
const SECRET = process.env.TOKEN_SECRET || 'dev-secret-change-in-production';
14+
// Secret for signing tokens
15+
// SECURITY: In production, TOKEN_SECRET must be set via environment variable
16+
const getSecret = () => {
17+
if (process.env.TOKEN_SECRET) {
18+
return process.env.TOKEN_SECRET;
19+
}
20+
21+
// In production (NODE_ENV=production), require explicit secret
22+
if (process.env.NODE_ENV === 'production') {
23+
console.error('SECURITY ERROR: TOKEN_SECRET environment variable must be set in production');
24+
console.error('Generate one with: node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"');
25+
process.exit(1);
26+
}
27+
28+
// In development, generate a random secret per process (tokens won't survive restarts)
29+
const devSecret = crypto.randomBytes(32).toString('hex');
30+
console.warn('WARNING: No TOKEN_SECRET set. Using random secret (tokens will not survive restarts).');
31+
console.warn('Set TOKEN_SECRET environment variable for persistent tokens.');
32+
return devSecret;
33+
};
34+
35+
// Initialize secret once at module load
36+
const SECRET = getSecret();
1637

1738
/**
1839
* Create a simple token for a WebID

src/idp/index.js

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,16 @@ export async function idpPlugin(fastify, options) {
209209
return handleCredentialsInfo(request, reply, issuer);
210210
});
211211

212-
// POST credentials - obtain tokens
213-
fastify.post('/idp/credentials', async (request, reply) => {
212+
// POST credentials - obtain tokens (with rate limiting for brute force protection)
213+
fastify.post('/idp/credentials', {
214+
config: {
215+
rateLimit: {
216+
max: 10,
217+
timeWindow: '1 minute',
218+
keyGenerator: (request) => request.ip
219+
}
220+
}
221+
}, async (request, reply) => {
214222
return handleCredentials(request, reply, issuer);
215223
});
216224

@@ -224,12 +232,29 @@ export async function idpPlugin(fastify, options) {
224232

225233
// POST interaction - direct form submission (CTH compatibility)
226234
// This handles form submissions directly to /idp/interaction/:uid
227-
fastify.post('/idp/interaction/:uid', async (request, reply) => {
235+
// Rate limited to prevent brute force attacks
236+
fastify.post('/idp/interaction/:uid', {
237+
config: {
238+
rateLimit: {
239+
max: 10,
240+
timeWindow: '1 minute',
241+
keyGenerator: (request) => request.ip
242+
}
243+
}
244+
}, async (request, reply) => {
228245
return handleLogin(request, reply, provider);
229246
});
230247

231-
// POST login (explicit path)
232-
fastify.post('/idp/interaction/:uid/login', async (request, reply) => {
248+
// POST login (explicit path) - rate limited
249+
fastify.post('/idp/interaction/:uid/login', {
250+
config: {
251+
rateLimit: {
252+
max: 10,
253+
timeWindow: '1 minute',
254+
keyGenerator: (request) => request.ip
255+
}
256+
}
257+
}, async (request, reply) => {
233258
return handleLogin(request, reply, provider);
234259
});
235260

@@ -248,7 +273,16 @@ export async function idpPlugin(fastify, options) {
248273
return handleRegisterGet(request, reply);
249274
});
250275

251-
fastify.post('/idp/register', async (request, reply) => {
276+
// Registration - rate limited to prevent spam accounts
277+
fastify.post('/idp/register', {
278+
config: {
279+
rateLimit: {
280+
max: 5,
281+
timeWindow: '1 hour',
282+
keyGenerator: (request) => request.ip
283+
}
284+
}
285+
}, async (request, reply) => {
252286
return handleRegisterPost(request, reply, issuer);
253287
});
254288

src/server.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Fastify from 'fastify';
2+
import rateLimit from '@fastify/rate-limit';
23
import { readFile } from 'fs/promises';
34
import { join, dirname } from 'path';
45
import { fileURLToPath } from 'url';
@@ -127,6 +128,20 @@ export function createServer(options = {}) {
127128
fastify.register(idpPlugin, { issuer: idpIssuer });
128129
}
129130

131+
// Register rate limiting plugin
132+
// Protects against brute force attacks and resource exhaustion
133+
fastify.register(rateLimit, {
134+
global: false, // Don't apply globally, only to specific routes
135+
max: 100, // Default max requests per window
136+
timeWindow: '1 minute',
137+
// Custom error response
138+
errorResponseBuilder: (request, context) => ({
139+
error: 'Too Many Requests',
140+
message: `Rate limit exceeded. Try again in ${Math.ceil(context.after / 1000)} seconds.`,
141+
retryAfter: Math.ceil(context.after / 1000)
142+
})
143+
});
144+
130145
// Global CORS preflight
131146
fastify.addHook('onRequest', async (request, reply) => {
132147
// Add CORS headers to all responses
@@ -224,8 +239,17 @@ export function createServer(options = {}) {
224239
}
225240
});
226241

227-
// Pod creation endpoint
228-
fastify.post('/.pods', handleCreatePod);
242+
// Pod creation endpoint with rate limiting
243+
// Limit: 5 pods per IP per hour to prevent resource exhaustion and namespace squatting
244+
fastify.post('/.pods', {
245+
config: {
246+
rateLimit: {
247+
max: 5,
248+
timeWindow: '1 hour',
249+
keyGenerator: (request) => request.ip
250+
}
251+
}
252+
}, handleCreatePod);
229253

230254
// Mashlib static files (served from root like NSS does)
231255
if (mashlibEnabled) {

0 commit comments

Comments
 (0)