-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
Summary
WebSocket notification subscriptions do not check ACL permissions. Any client can subscribe to any URL and receive pub notifications when resources change, including private resources they cannot read.
The Problem
Current behavior:
1. Attacker connects to wss://victim.example/.notifications
2. Sends: sub https://victim.example/private/secret.ttl
3. Server sends: ack https://victim.example/private/secret.ttl
4. Attacker receives: pub https://victim.example/private/secret.ttl (when it changes)
Impact:
- Information leak: attacker learns private resource exists
- Activity monitoring: attacker knows when private resources change
- Enumeration: attacker can probe for resource existence
Historical Context
This is a known issue in the Solid ecosystem:
- nodeSolidServer/node-solid-server#143 (2015): "Websockets should be authenticated and respect ACL"
- solid/notifications#3: Tim Berners-Lee called it "a serious bug"
- The Solid Notifications Protocol states: "Resource-based Access controls MUST be enforced for every subscription. A client MUST be permitted to read the resource to which it subscribes."
The Fix
Simple: check WAC read permission on every sub command.
Step 1: Pass WebID from HTTP upgrade to WebSocket
The WebSocket upgrade IS an HTTP request. We already run auth middleware on it.
// src/notifications/index.js
fastify.get('/.notifications', { websocket: true }, async (connection, request) => {
// Attach authenticated WebID (or null for anonymous)
connection.socket.webId = request.webId || null;
handleWebSocket(connection.socket, request);
});Step 2: Check ACL on subscribe
// src/notifications/websocket.js
import { checkAccess } from '../wac/checker.js';
import { AccessMode } from '../wac/parser.js';
// In handleWebSocket(), modify the sub handler:
if (msg.startsWith('sub ')) {
const url = msg.slice(4).trim();
// Security: Check WAC read permission
const { authorized } = await checkAccess(url, socket.webId, AccessMode.READ);
if (!authorized) {
socket.send(`err ${url} forbidden`);
return;
}
subscribe(socket, url);
socket.send(`ack ${url}`);
}Behavior After Fix
| Scenario | Result |
|---|---|
| Anonymous subscribes to public resource | ✅ Allowed |
| Anonymous subscribes to private resource | ❌ err forbidden |
| Authenticated user subscribes to own resource | ✅ Allowed |
| Authenticated user subscribes to resource they can read | ✅ Allowed |
| Authenticated user subscribes to resource they can't read | ❌ err forbidden |
Edge Cases to Handle
- Resource doesn't exist: Return
err forbidden(don't leak existence) - URL outside server: Return
err forbidden(or just ignore?) - Malformed URL: Return
err invalid - ACL check fails (error): Return
err forbidden(fail secure)
Not Included (Keep It Simple)
- ❌ New auth protocols (auth command, tickets, etc.)
- ❌ Token passing over WebSocket
- ❌ Breaking protocol changes
Just use what we have: HTTP auth on upgrade + WAC check on subscribe.
Test Plan
- Anonymous cannot subscribe to private resource
- Anonymous can subscribe to public resource
- Authenticated user can subscribe to readable resource
- Authenticated user cannot subscribe to unreadable resource
- Subscribing to non-existent resource returns forbidden (not 404)
- Existing behavior unchanged for public resources
Metadata
Metadata
Assignees
Labels
No labels