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.
- package/SECURITY-AUDIT-2026-01-03.md +8 -3
- package/package.json +2 -1
- package/src/auth/token.js +23 -2
- package/src/idp/index.js +40 -6
- package/src/server.js +26 -2
|
@@ -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 |
|
|
197
|
-
| Default token secret | High |
|
|
198
|
-
| No rate limiting | Medium |
|
|
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.
|
|
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
|
|
15
|
-
|
|
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',
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|