javascript-solid-server 0.0.50 → 0.0.51

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.
@@ -193,15 +193,20 @@ const SECRET = process.env.TOKEN_SECRET || 'dev-secret-change-in-production';
193
193
  | JWT signature bypass | Critical | 🟢 Fixed | v0.0.49 |
194
194
  | SSRF in OIDC discovery | Critical | 🟢 Fixed | v0.0.50 |
195
195
  | 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 |
199
199
  | Information disclosure | Medium | 🔴 Open | - |
200
200
 
201
201
  ---
202
202
 
203
203
  ## Changelog
204
204
 
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
+
205
210
  ### v0.0.50 (2026-01-03)
206
211
  - **Fixed SSRF in OIDC discovery**: Issuer URLs are now validated before fetching (HTTPS required, private IPs blocked)
207
212
  - **Fixed SSRF in client document fetch**: Client ID URLs are now validated before fetching
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.50",
3
+ "version": "0.0.51",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -24,6 +24,7 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@fastify/middie": "^8.3.3",
27
+ "@fastify/rate-limit": "^9.1.0",
27
28
  "@fastify/websocket": "^8.3.1",
28
29
  "bcrypt": "^6.0.0",
29
30
  "commander": "^14.0.2",
package/src/auth/token.js CHANGED
@@ -11,8 +11,29 @@ import crypto from 'crypto';
11
11
  import { verifySolidOidc, hasSolidOidcAuth } from './solid-oidc.js';
12
12
  import { verifyNostrAuth, hasNostrAuth } from './nostr.js';
13
13
 
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();
16
37
 
17
38
  /**
18
39
  * Create a simple token for a WebID
package/src/idp/index.js CHANGED
@@ -209,8 +209,16 @@ export async function idpPlugin(fastify, options) {
209
209
  return handleCredentialsInfo(request, reply, issuer);
210
210
  });
211
211
 
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) => {
214
222
  return handleCredentials(request, reply, issuer);
215
223
  });
216
224
 
@@ -224,12 +232,29 @@ export async function idpPlugin(fastify, options) {
224
232
 
225
233
  // POST interaction - direct form submission (CTH compatibility)
226
234
  // 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) => {
228
245
  return handleLogin(request, reply, provider);
229
246
  });
230
247
 
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) => {
233
258
  return handleLogin(request, reply, provider);
234
259
  });
235
260
 
@@ -248,7 +273,16 @@ export async function idpPlugin(fastify, options) {
248
273
  return handleRegisterGet(request, reply);
249
274
  });
250
275
 
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) => {
252
286
  return handleRegisterPost(request, reply, issuer);
253
287
  });
254
288
 
package/src/server.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import Fastify from 'fastify';
2
+ import rateLimit from '@fastify/rate-limit';
2
3
  import { readFile } from 'fs/promises';
3
4
  import { join, dirname } from 'path';
4
5
  import { fileURLToPath } from 'url';
@@ -127,6 +128,20 @@ export function createServer(options = {}) {
127
128
  fastify.register(idpPlugin, { issuer: idpIssuer });
128
129
  }
129
130
 
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
+
130
145
  // Global CORS preflight
131
146
  fastify.addHook('onRequest', async (request, reply) => {
132
147
  // Add CORS headers to all responses
@@ -224,8 +239,17 @@ export function createServer(options = {}) {
224
239
  }
225
240
  });
226
241
 
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);
229
253
 
230
254
  // Mashlib static files (served from root like NSS does)
231
255
  if (mashlibEnabled) {