Skip to content

Security: WebSocket subscriptions should check ACL permissions #4

@melvincarvalho

Description

@melvincarvalho

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

  1. Resource doesn't exist: Return err forbidden (don't leak existence)
  2. URL outside server: Return err forbidden (or just ignore?)
  3. Malformed URL: Return err invalid
  4. 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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions