javascript-solid-server 0.0.47 → 0.0.49

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.
@@ -145,7 +145,62 @@
145
145
  "Bash(if [ ! -d \"node-solid-server\" ])",
146
146
  "Bash(then git clone --depth 1 https://github.com/nodeSolidServer/node-solid-server.git)",
147
147
  "Bash(node test-local-nss2.js:*)",
148
- "Bash(npm test)"
148
+ "Bash(npm test)",
149
+ "Bash(repos.json)",
150
+ "Bash(*.log)",
151
+ "Bash(node --check:*)",
152
+ "Bash(gh repo view:*)",
153
+ "Bash(noskey --help:*)",
154
+ "Bash(npx noskey --help:*)",
155
+ "Bash(noskey:*)",
156
+ "Bash(node -e:*)",
157
+ "Bash(node src/publish.js:*)",
158
+ "Bash(git remote add:*)",
159
+ "Bash(git fetch:*)",
160
+ "Bash(git rev-parse:*)",
161
+ "Bash(f502f06c1d7553f4b7159e8d57a1e14819dc3053b59399e080882cc8e6bb62ad )",
162
+ "Bash(798715377357003683b979b41c5d99c0312e6e788d789f0d5df710465483aa3e )",
163
+ "Bash(f810e7491da3390109ddc13a74a1fff985ba3a4735024f2b714c12d213f5ea11 )",
164
+ "Bash(1 )",
165
+ "Bash(911912000 )",
166
+ "Bash(4ccef8c68cf18f8f156a0bb017dfd6e0cc7ebf1672fa2d769e02e2efc700328b 1000000 )",
167
+ "Bash(798715377357003683b979b41c5d99c0312e6e788d789f0d5df710465483aa3e 910911000 )",
168
+ "Bash(~/.gitmark/faucet.txt)",
169
+ "Bash(blocktrails --version:*)",
170
+ "Bash(blocktrails --help:*)",
171
+ "Bash(blocktrails show:*)",
172
+ "Bash(git restore:*)",
173
+ "Bash(npm show:*)",
174
+ "WebFetch(domain:gitlab.com)",
175
+ "Bash(gh repo edit:*)",
176
+ "WebFetch(domain:blocktrails.github.io)",
177
+ "Bash(jq:*)",
178
+ "Bash(SOLID_SYNC=true timeout 45 node:*)",
179
+ "Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm status)",
180
+ "Bash(SOLID_SYNC=true ANCHOR=true timeout 8 node:*)",
181
+ "Bash(SOLID_SYNC=true ANCHOR=true node:*)",
182
+ "Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm diff src/watcher.js)",
183
+ "Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm add src/watcher.js)",
184
+ "Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm commit -m \"$\\(cat <<''EOF''\nAdd transfer API and HTTP 402 middleware\n\n- Add POST /transfer endpoint for user-to-user token transfers\n- Add verify402Payment middleware for token-gated APIs\n- Add GET /api/quote demo endpoint \\(costs 1 GSAT\\)\n- Add GET /balance/:did and GET /state endpoints\n- Fix anchor function to use encodeBech32m for address derivation\n- Remove OP_RETURN from anchor tx \\(state hash stored in state.json\\)\nEOF\n\\)\")",
185
+ "Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm push)",
186
+ "Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm add demo.html src/watcher.js debug.html paywall.html transfer.html)",
187
+ "Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm commit -m \"$\\(cat <<''EOF''\nAdd NIP-98 paywall, transfer, withdraw, and debug pages\n\n- Implement NIP-98 \\(kind 27235\\) for HTTP 402 authentication\n- Add paywall.html demo page showing NIP-98 flow\n- Add transfer.html for user-to-user GSAT transfers\n- Add debug.html with anchors, state, verify, withdraw, and users tabs\n- Add POST /withdraw endpoint for sats → Bitcoin address\n- Add navigation to demo.html linking all pages\nEOF\n\\)\")",
188
+ "Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm add test-amm.mjs package.json)",
189
+ "Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm commit -m \"$\\(cat <<''EOF''\nAdd AMM tests for math, signatures, and NIP-98\n\n- AMM math tests \\(calculateGsatOut, calculateSatsOut, slippage, k invariant\\)\n- Signature verification tests \\(sell, transfer, withdraw requests\\)\n- NIP-98 event creation, verification, and encoding tests\n- Update package.json with test script\nEOF\n\\)\")",
190
+ "Bash(SOLID_SYNC=true node src/watcher.js:*)",
191
+ "Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm add demo.html src/watcher.js)",
192
+ "Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm commit -m \"$\\(cat <<''EOF''\nAdd smart polling with manual deposit check\n\n- Change poll interval from 30s to 10 minutes\n- Add POST /check endpoint for manual deposit scan\n- Add 10-second rate limit between manual checks\n- Add \"Check Deposits\" button to demo.html\nEOF\n\\)\")",
193
+ "Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm add:*)",
194
+ "Bash(git -C /home/melvin/remote/github.com/blocktrails/gitmark-amm commit -m \"Use blocktrails npm package instead of local path\")",
195
+ "Bash(for addr in tb1pdypd4k38q4x0qz5x7hqavjhfpgt2n4tm0egggx587aafqn3wsnds8gm3yf tb1pqxmrkvuyea9v7vv323tmptjfle5tj9y6cpe5g8wqvlz6d5xmfhlqctx7py tb1p0fv2683x2j5htf9n7fkpmxsy4h7yuxmetelq2c6vp8u2zw9rhp2s5kha7v)",
196
+ "Bash(do echo -n \"$addr: \" curl -s \"https://mempool.space/testnet4/api/address/$addr\")",
197
+ "WebFetch(domain:webledgers.org)",
198
+ "Bash(npm pack:*)",
199
+ "Bash(npm info:*)",
200
+ "Bash(tar:*)",
201
+ "Bash(TEST_API=1 API_URL=https://api.solid.social node:*)",
202
+ "Bash(webledgers show:*)",
203
+ "Bash(webledgers set-balance:*)"
149
204
  ]
150
205
  }
151
206
  }
package/README.md CHANGED
@@ -528,6 +528,51 @@ curl -X POST https://example.com/.pods \
528
528
  | [CSS](https://github.com/CommunitySolidServer/CommunitySolidServer) | 5.8 MB | 70 | Modular, configurable |
529
529
  | [Pivot](https://github.com/solid-contrib/pivot) | ~6 MB | 70+ | Built on CSS |
530
530
 
531
+ ## Security
532
+
533
+ ### Root ACL Required
534
+
535
+ JSS uses **restrictive mode** by default: if no ACL file exists for a resource, access is denied. This prevents unauthorized writes to unprotected containers.
536
+
537
+ **You must create a root `.acl` file** in your data directory. Example (JSON-LD format):
538
+
539
+ ```json
540
+ {
541
+ "@context": {
542
+ "acl": "http://www.w3.org/ns/auth/acl#",
543
+ "foaf": "http://xmlns.com/foaf/0.1/"
544
+ },
545
+ "@graph": [
546
+ {
547
+ "@id": "#owner",
548
+ "@type": "acl:Authorization",
549
+ "acl:agent": { "@id": "https://your-domain.com/profile/card#me" },
550
+ "acl:accessTo": { "@id": "https://your-domain.com/" },
551
+ "acl:default": { "@id": "https://your-domain.com/" },
552
+ "acl:mode": [
553
+ { "@id": "acl:Read" },
554
+ { "@id": "acl:Write" },
555
+ { "@id": "acl:Control" }
556
+ ]
557
+ },
558
+ {
559
+ "@id": "#public",
560
+ "@type": "acl:Authorization",
561
+ "acl:agentClass": { "@id": "foaf:Agent" },
562
+ "acl:accessTo": { "@id": "https://your-domain.com/" },
563
+ "acl:default": { "@id": "https://your-domain.com/" },
564
+ "acl:mode": [
565
+ { "@id": "acl:Read" }
566
+ ]
567
+ }
568
+ ]
569
+ }
570
+ ```
571
+
572
+ Save this as `data/.acl` (replacing `your-domain.com` with your actual domain).
573
+
574
+ See [Issue #32](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/32) for background.
575
+
531
576
  ## Performance
532
577
 
533
578
  This server is designed for speed. Benchmark results on a typical development machine:
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.47",
3
+ "version": "0.0.49",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { getWebIdFromRequestAsync } from './token.js';
8
8
  import { checkAccess, getRequiredMode } from '../wac/checker.js';
9
+ import { AccessMode } from '../wac/parser.js';
9
10
  import * as storage from '../storage/filesystem.js';
10
11
  import { getEffectiveUrlPath } from '../utils/url.js';
11
12
 
@@ -21,15 +22,19 @@ export async function authorize(request, reply, options = {}) {
21
22
  const urlPath = request.url.split('?')[0];
22
23
  const method = request.method;
23
24
 
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') {
27
27
  return { authorized: true, webId: null, wacAllow: 'user="read write append control", public="read write append"', authError: null };
28
28
  }
29
29
 
30
30
  // Get WebID from token (supports both simple and Solid-OIDC tokens)
31
31
  const { webId, error: authError } = await getWebIdFromRequestAsync(request);
32
32
 
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
+
33
38
  // Log auth failures for debugging
34
39
  if (authError) {
35
40
  request.log.warn({ authError, method, urlPath, hasAuth: !!request.headers.authorization }, 'Auth error');
@@ -114,3 +119,39 @@ export function handleUnauthorized(reply, isAuthenticated, wacAllow, authError =
114
119
  });
115
120
  }
116
121
  }
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
+ }
package/src/auth/token.js CHANGED
@@ -37,7 +37,11 @@ export function createToken(webId, expiresIn = 3600) {
37
37
  }
38
38
 
39
39
  /**
40
- * Verify and decode a token (simple 2-part or JWT 3-part)
40
+ * Verify and decode a simple token (2-part HMAC-signed)
41
+ *
42
+ * SECURITY: Only accepts 2-part simple tokens signed with HMAC.
43
+ * JWT tokens (3-part) require async verification via verifyTokenAsync().
44
+ *
41
45
  * @param {string} token - The token to verify
42
46
  * @returns {{webId: string, iat: number, exp: number} | null} Decoded payload or null
43
47
  */
@@ -48,9 +52,9 @@ export function verifyToken(token) {
48
52
 
49
53
  const parts = token.split('.');
50
54
 
51
- // Handle JWT tokens (3 parts) from credentials endpoint
55
+ // JWT tokens (3 parts) require async verification - reject in sync function
52
56
  if (parts.length === 3) {
53
- return verifyJwtToken(token);
57
+ return null;
54
58
  }
55
59
 
56
60
  if (parts.length !== 2) {
@@ -59,13 +63,19 @@ export function verifyToken(token) {
59
63
 
60
64
  const [data, signature] = parts;
61
65
 
62
- // Verify signature
66
+ // Verify HMAC signature
63
67
  const expectedSig = crypto
64
68
  .createHmac('sha256', SECRET)
65
69
  .update(data)
66
70
  .digest('base64url');
67
71
 
68
- if (signature !== expectedSig) {
72
+ // Constant-time comparison to prevent timing attacks
73
+ try {
74
+ if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSig))) {
75
+ return null;
76
+ }
77
+ } catch {
78
+ // If lengths don't match, timingSafeEqual throws
69
79
  return null;
70
80
  }
71
81
 
@@ -85,38 +95,45 @@ export function verifyToken(token) {
85
95
  }
86
96
 
87
97
  /**
88
- * Verify a JWT token from credentials endpoint
89
- * JWT tokens are self-contained and signed with the IdP's private key
98
+ * Verify a JWT token from the credentials endpoint
99
+ * Properly verifies signature against IdP's JWKS
100
+ *
90
101
  * @param {string} token - JWT token
91
- * @returns {{webId: string, iat: number, exp: number} | null} Decoded payload or null
102
+ * @returns {Promise<{webId: string, iat: number, exp: number} | null>}
92
103
  */
93
- function verifyJwtToken(token) {
104
+ async function verifyJwtFromIdp(token) {
94
105
  try {
95
- const parts = token.split('.');
96
- if (parts.length !== 3) {
97
- return null;
98
- }
99
-
100
- // Decode the payload (middle part)
101
- const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
106
+ // Dynamically import to avoid circular dependencies
107
+ const { getPublicJwks } = await import('../idp/keys.js');
108
+ const jose = await import('jose');
102
109
 
103
- // Check expiration
104
- if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
110
+ const jwks = await getPublicJwks();
111
+ if (!jwks || !jwks.keys || jwks.keys.length === 0) {
105
112
  return null;
106
113
  }
107
114
 
108
- // JWT from credentials endpoint uses 'webid' claim (lowercase)
109
- if (payload.webid) {
110
- return { webId: payload.webid, iat: payload.iat, exp: payload.exp };
111
- }
115
+ // Create JWKS for verification
116
+ const keySet = jose.createLocalJWKSet(jwks);
112
117
 
113
- // Also check uppercase WebId for compatibility
114
- if (payload.webId) {
115
- return payload;
118
+ // Verify the token
119
+ const { payload } = await jose.jwtVerify(token, keySet, {
120
+ // Allow some clock skew
121
+ clockTolerance: 60,
122
+ });
123
+
124
+ // Extract webid claim
125
+ const webId = payload.webid || payload.webId || payload.sub;
126
+ if (!webId) {
127
+ return null;
116
128
  }
117
129
 
118
- return null;
119
- } catch {
130
+ return {
131
+ webId,
132
+ iat: payload.iat,
133
+ exp: payload.exp
134
+ };
135
+ } catch (err) {
136
+ // Verification failed - invalid signature, expired, etc.
120
137
  return null;
121
138
  }
122
139
  }
@@ -190,16 +207,27 @@ export async function getWebIdFromRequestAsync(request) {
190
207
  return verifyNostrAuth(request);
191
208
  }
192
209
 
193
- // Fall back to simple Bearer tokens
210
+ // Fall back to Bearer tokens
194
211
  const token = extractToken(authHeader);
195
212
  if (!token) {
196
213
  return { webId: null, error: null };
197
214
  }
198
215
 
216
+ // Try simple 2-part token first
199
217
  const payload = verifyToken(token);
200
218
  if (payload?.webId) {
201
219
  return { webId: payload.webId, error: null };
202
220
  }
203
221
 
222
+ // If 3-part JWT, verify against IdP's JWKS
223
+ const parts = token.split('.');
224
+ if (parts.length === 3) {
225
+ const jwtPayload = await verifyJwtFromIdp(token);
226
+ if (jwtPayload?.webId) {
227
+ return { webId: jwtPayload.webId, error: null };
228
+ }
229
+ return { webId: null, error: 'Invalid or unverifiable JWT token' };
230
+ }
231
+
204
232
  return { webId: null, error: 'Invalid token' };
205
233
  }
@@ -70,6 +70,14 @@ function findGitDir(repoPath) {
70
70
  * @param {FastifyReply} reply
71
71
  */
72
72
  export async function handleGit(request, reply) {
73
+ // Handle CORS preflight
74
+ if (request.method === 'OPTIONS') {
75
+ reply.header('Access-Control-Allow-Origin', '*');
76
+ reply.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
77
+ reply.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
78
+ return reply.code(200).send();
79
+ }
80
+
73
81
  const urlPath = decodeURIComponent(request.url.split('?')[0]);
74
82
  const queryString = request.url.split('?')[1] || '';
75
83
 
@@ -178,6 +186,11 @@ export async function handleGit(request, reply) {
178
186
  }
179
187
  }
180
188
 
189
+ // Add CORS headers for browser git clients
190
+ reply.raw.setHeader('Access-Control-Allow-Origin', '*');
191
+ reply.raw.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
192
+ reply.raw.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
193
+
181
194
  reply.raw.writeHead(statusCode);
182
195
  headersSent = true;
183
196
  reply.raw.write(bodySection);
package/src/server.js CHANGED
@@ -74,6 +74,14 @@ export function createServer(options = {}) {
74
74
  done(null, body);
75
75
  });
76
76
 
77
+ // Git content types need explicit handling (binary data)
78
+ fastify.addContentTypeParser('application/x-git-receive-pack-request', { parseAs: 'buffer' }, (req, body, done) => {
79
+ done(null, body);
80
+ });
81
+ fastify.addContentTypeParser('application/x-git-upload-pack-request', { parseAs: 'buffer' }, (req, body, done) => {
82
+ done(null, body);
83
+ });
84
+
77
85
  // Attach server config to requests
78
86
  fastify.decorateRequest('connegEnabled', null);
79
87
  fastify.decorateRequest('notificationsEnabled', null);
@@ -28,9 +28,9 @@ export async function checkAccess({
28
28
  const aclResult = await findApplicableAcl(resourceUrl, resourcePath, isContainer);
29
29
 
30
30
  if (!aclResult) {
31
- // No ACL found - allow by default (permissive mode)
32
- // This allows resources without ACLs to be publicly accessible
33
- return { allowed: true, wacAllow: 'user="read write append control", public="read write append"' };
31
+ // No ACL found - deny by default (restrictive mode)
32
+ // Security: Require explicit ACL for any access
33
+ return { allowed: false, wacAllow: 'user="", public=""' };
34
34
  }
35
35
 
36
36
  const { authorizations, isDefault, targetUrl: aclContainerUrl } = aclResult;
package/test/wac.test.js CHANGED
@@ -226,15 +226,22 @@ describe('WAC Integration', () => {
226
226
 
227
227
  describe('ACL Files', () => {
228
228
  it('should create root .acl on pod creation', async () => {
229
- const res = await request('/wactest/.acl');
229
+ // ACL files require Control permission - must be authenticated as pod owner
230
+ const res = await request('/wactest/.acl', { auth: 'wactest' });
230
231
 
231
232
  assertStatus(res, 200);
232
233
  const content = await res.json();
233
234
  assert.ok(content['@graph'], 'Should be JSON-LD');
234
235
  });
235
236
 
237
+ it('should deny unauthenticated access to .acl files', async () => {
238
+ // Security: ACL files must require authentication
239
+ const res = await request('/wactest/.acl');
240
+ assertStatus(res, 401);
241
+ });
242
+
236
243
  it('should create private folder .acl', async () => {
237
- const res = await request('/wactest/private/.acl');
244
+ const res = await request('/wactest/private/.acl', { auth: 'wactest' });
238
245
 
239
246
  assertStatus(res, 200);
240
247
  const content = await res.json();
@@ -248,7 +255,7 @@ describe('WAC Integration', () => {
248
255
  });
249
256
 
250
257
  it('should create inbox .acl with public append', async () => {
251
- const res = await request('/wactest/inbox/.acl');
258
+ const res = await request('/wactest/inbox/.acl', { auth: 'wactest' });
252
259
 
253
260
  assertStatus(res, 200);
254
261
  const content = await res.json();