Skip to content

Commit a052003

Browse files
v0.0.49 - Critical security fixes
- Fix ACL bypass: ACL files now require acl:Control on protected resource - Fix JWT signature bypass: JWTs verified against IdP JWKS before accepting - Add test for unauthenticated ACL access denial - Update security audit report with remediation status
1 parent 4380ec8 commit a052003

5 files changed

Lines changed: 319 additions & 35 deletions

File tree

SECURITY-AUDIT-2026-01-03.md

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
# JSS Security Audit Report
2+
3+
**Date:** 2026-01-03
4+
**Auditor:** Security Review
5+
**Version Audited:** 0.0.48
6+
7+
---
8+
9+
## Executive Summary
10+
11+
A security audit of JavaScriptSolidServer revealed **2 critical**, **2 high**, and **2 medium** severity vulnerabilities. The most severe allows unauthenticated users to read and write ACL (Access Control List) files, effectively bypassing all authorization.
12+
13+
---
14+
15+
## Critical Vulnerabilities
16+
17+
### 1. ACL Files Bypass Authorization (CRITICAL) ⚠️
18+
19+
**Location:** `src/auth/middleware.js:24-28`
20+
21+
```javascript
22+
if (urlPath.endsWith('.acl') || method === 'OPTIONS') {
23+
return { authorized: true, webId: null, wacAllow: '...', authError: null };
24+
}
25+
```
26+
27+
**Description:** The authorization middleware explicitly skips authentication and authorization checks for all requests to `.acl` files. This allows any unauthenticated user to:
28+
29+
1. **Read any ACL file** - Discover permission structures
30+
2. **Write/Create any ACL file** - Grant themselves access to any resource
31+
3. **Modify existing ACL files** - Lock out legitimate owners
32+
33+
**Proof of Concept:**
34+
```bash
35+
# Read root ACL without authentication
36+
curl https://example.com/.acl
37+
38+
# Create malicious ACL without authentication
39+
curl -X PUT https://example.com/victim/.acl \
40+
-H "Content-Type: application/ld+json" \
41+
-d '{"@graph":[{"@id":"#attacker","@type":"acl:Authorization","acl:agent":{"@id":"https://attacker.com/card#me"},"acl:accessTo":{"@id":"https://example.com/victim/"},"acl:mode":[{"@id":"acl:Read"},{"@id":"acl:Write"},{"@id":"acl:Control"}]}]}'
42+
```
43+
44+
**Impact:** Complete authorization bypass. Attacker can gain full control of any resource.
45+
46+
**CVSS Score:** 9.8 (Critical)
47+
48+
**Fix Required:** ACL files should require `acl:Control` permission on the resource they protect.
49+
50+
---
51+
52+
### 2. JWT Token Signature Not Verified (CRITICAL) ⚠️
53+
54+
**Location:** `src/auth/token.js:93-122`
55+
56+
```javascript
57+
function verifyJwtToken(token) {
58+
const parts = token.split('.');
59+
if (parts.length !== 3) return null;
60+
61+
// Decode the payload (middle part) - NO SIGNATURE VERIFICATION!
62+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
63+
64+
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
65+
return null;
66+
}
67+
68+
if (payload.webid) {
69+
return { webId: payload.webid, iat: payload.iat, exp: payload.exp };
70+
}
71+
// ...
72+
}
73+
```
74+
75+
**Description:** The `verifyJwtToken` function decodes JWT tokens but **never verifies the cryptographic signature**. An attacker can craft arbitrary JWT tokens with any WebID.
76+
77+
**Proof of Concept:**
78+
```bash
79+
# Forge a JWT token with attacker's WebID (signature is ignored)
80+
# Header: {"alg":"RS256","typ":"JWT"}
81+
# Payload: {"webid":"https://attacker.com/card#me","exp":9999999999}
82+
curl https://example.com/private/ \
83+
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJ3ZWJpZCI6Imh0dHBzOi8vYXR0YWNrZXIuY29tL2NhcmQjbWUiLCJleHAiOjk5OTk5OTk5OTl9.fakesig"
84+
```
85+
86+
**Impact:** Complete authentication bypass. Attacker can impersonate any user.
87+
88+
**CVSS Score:** 9.8 (Critical)
89+
90+
**Fix Required:** Verify JWT signatures against the issuer's JWKS before accepting tokens.
91+
92+
---
93+
94+
## High Severity Vulnerabilities
95+
96+
### 3. Pod Creation Without Authentication (HIGH)
97+
98+
**Location:** `src/server.js:203,228`
99+
100+
```javascript
101+
// Auth bypass list includes /.pods
102+
if (request.url === '/.pods' || ...) {
103+
return; // Skip auth
104+
}
105+
106+
// Anyone can create pods
107+
fastify.post('/.pods', handleCreatePod);
108+
```
109+
110+
**Description:** The `/.pods` endpoint allows anyone to create new pods without authentication.
111+
112+
**Impact:**
113+
- Resource exhaustion (DoS)
114+
- Username/namespace squatting
115+
- Disk space exhaustion
116+
117+
**CVSS Score:** 7.5 (High)
118+
119+
**Fix Required:** Require authentication or implement rate limiting and CAPTCHA.
120+
121+
---
122+
123+
### 4. Default Token Secret in Production (HIGH)
124+
125+
**Location:** `src/auth/token.js:15`
126+
127+
```javascript
128+
const SECRET = process.env.TOKEN_SECRET || 'dev-secret-change-in-production';
129+
```
130+
131+
**Description:** If `TOKEN_SECRET` environment variable is not set, a hardcoded default secret is used.
132+
133+
**Impact:** Tokens can be forged by anyone who knows the default secret.
134+
135+
**CVSS Score:** 8.1 (High)
136+
137+
**Fix Required:** Fail to start if TOKEN_SECRET is not set in production, or generate a random secret on first run.
138+
139+
---
140+
141+
## Medium Severity Vulnerabilities
142+
143+
### 5. No Rate Limiting on Authentication Endpoints (MEDIUM)
144+
145+
**Location:** `src/idp/interactions.js`, `src/idp/credentials.js`
146+
147+
**Description:** Login, registration, and credential endpoints have no rate limiting, allowing brute force attacks.
148+
149+
**Impact:** Account takeover through credential stuffing or brute force.
150+
151+
**CVSS Score:** 5.3 (Medium)
152+
153+
**Fix Required:** Implement rate limiting (e.g., 5 attempts per minute per IP).
154+
155+
---
156+
157+
### 6. Information Disclosure via Error Messages (MEDIUM)
158+
159+
**Location:** Various handlers
160+
161+
**Description:** Error messages may reveal internal paths or stack traces.
162+
163+
**Impact:** Information leakage useful for further attacks.
164+
165+
**CVSS Score:** 4.3 (Medium)
166+
167+
---
168+
169+
## Recommendations
170+
171+
### Immediate Actions (Critical)
172+
173+
1. **Fix ACL bypass** - Require `acl:Control` permission to modify ACL files
174+
2. **Verify JWT signatures** - Use `jose` library to verify against issuer JWKS
175+
176+
### Short-term Actions (High)
177+
178+
3. **Protect pod creation** - Add authentication or rate limiting
179+
4. **Enforce TOKEN_SECRET** - Fail startup if not configured
180+
181+
### Medium-term Actions
182+
183+
5. **Add rate limiting** - Use `@fastify/rate-limit` plugin
184+
6. **Sanitize error messages** - Remove internal details from user-facing errors
185+
186+
---
187+
188+
## Remediation Status
189+
190+
| Issue | Severity | Status | Fixed In |
191+
|-------|----------|--------|----------|
192+
| ACL bypass | Critical | 🟢 Fixed | v0.0.49 |
193+
| JWT signature bypass | Critical | 🟢 Fixed | v0.0.49 |
194+
| Unauthenticated pod creation | High | 🔴 Open | - |
195+
| Default token secret | High | 🔴 Open | - |
196+
| No rate limiting | Medium | 🔴 Open | - |
197+
| Information disclosure | Medium | 🔴 Open | - |
198+
199+
---
200+
201+
## Changelog
202+
203+
### v0.0.49 (2026-01-03)
204+
- **Fixed ACL bypass**: ACL files now require `acl:Control` permission on the protected resource
205+
- **Fixed JWT signature bypass**: JWTs are now verified against the IdP's JWKS before accepting
206+
207+
*Report generated: 2026-01-03*
208+
*Last updated: 2026-01-03*

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "javascript-solid-server",
3-
"version": "0.0.48",
3+
"version": "0.0.49",
44
"description": "A minimal, fast Solid server",
55
"main": "src/index.js",
66
"type": "module",

src/auth/middleware.js

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import { getWebIdFromRequestAsync } from './token.js';
88
import { checkAccess, getRequiredMode } from '../wac/checker.js';
9+
import { AccessMode } from '../wac/parser.js';
910
import * as storage from '../storage/filesystem.js';
1011
import { getEffectiveUrlPath } from '../utils/url.js';
1112

@@ -21,15 +22,19 @@ export async function authorize(request, reply, options = {}) {
2122
const urlPath = request.url.split('?')[0];
2223
const method = request.method;
2324

24-
// Skip auth for .acl files (they need special handling)
25-
// and for OPTIONS (CORS preflight)
26-
if (urlPath.endsWith('.acl') || method === 'OPTIONS') {
25+
// OPTIONS is always allowed (CORS preflight)
26+
if (method === 'OPTIONS') {
2727
return { authorized: true, webId: null, wacAllow: 'user="read write append control", public="read write append"', authError: null };
2828
}
2929

3030
// Get WebID from token (supports both simple and Solid-OIDC tokens)
3131
const { webId, error: authError } = await getWebIdFromRequestAsync(request);
3232

33+
// ACL files require special handling - check Control permission on protected resource
34+
if (urlPath.endsWith('.acl')) {
35+
return authorizeAclAccess(request, urlPath, method, webId, authError);
36+
}
37+
3338
// Log auth failures for debugging
3439
if (authError) {
3540
request.log.warn({ authError, method, urlPath, hasAuth: !!request.headers.authorization }, 'Auth error');
@@ -114,3 +119,39 @@ export function handleUnauthorized(reply, isAuthenticated, wacAllow, authError =
114119
});
115120
}
116121
}
122+
123+
/**
124+
* Authorize access to ACL files
125+
* ACL files require acl:Control permission on the resource they protect
126+
*
127+
* @param {object} request - Fastify request
128+
* @param {string} urlPath - URL path to the ACL file
129+
* @param {string} method - HTTP method
130+
* @param {string|null} webId - Authenticated user's WebID
131+
* @param {string|null} authError - Authentication error if any
132+
* @returns {Promise<{authorized: boolean, webId: string|null, wacAllow: string, authError: string|null}>}
133+
*/
134+
async function authorizeAclAccess(request, urlPath, method, webId, authError) {
135+
// Determine the protected resource URL
136+
// /foo/.acl protects /foo/ (container)
137+
// /foo/bar.acl protects /foo/bar (resource)
138+
const protectedPath = urlPath.replace(/\.acl$/, '');
139+
const isProtectedContainer = protectedPath.endsWith('/');
140+
const protectedUrl = `${request.protocol}://${request.hostname}${protectedPath}`;
141+
142+
// Get storage path for the protected resource
143+
const storagePath = getEffectiveUrlPath(request).replace(/\.acl$/, '');
144+
145+
// All ACL operations require Control permission on the protected resource
146+
// This is stricter than the Solid spec (which allows Read for reading ACLs)
147+
// but simpler and more secure
148+
const { allowed, wacAllow } = await checkAccess({
149+
resourceUrl: protectedUrl,
150+
resourcePath: storagePath,
151+
isContainer: isProtectedContainer,
152+
agentWebId: webId,
153+
requiredMode: AccessMode.CONTROL
154+
});
155+
156+
return { authorized: allowed, webId, wacAllow, authError };
157+
}

0 commit comments

Comments
 (0)