fix(security): scope channel-permission node filter by sourceId (#3745)#3749
Conversation
filterNodesByChannelPermission already accepts a sourceId, but several call sites fetched source-scoped nodes then filtered with no sourceId — so getUserPermissionSetAsync fell back to a cross-source UNION. A guest with channel-0 read on source A could thus see source B's channel-0 nodes. Forward the request's source id at every leaky call site: - /api/nodes (nodesSourceId), /api/nodes/active (activeNodesSourceId) - consolidated poll: memory nodes + telemetry availability (pollSourceId) - unread-DM visible-node gate (unreadSourceId) - /telemetry/available/nodes (telAvailSourceId) (v1/nodes and sourceRoutes already passed the source id.) Adds a nodeEnhancer regression test asserting the sourceId is forwarded to the permission lookup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBLhGGNh35oMwTL53va1Y5
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
|
Claude finished @Yeraze's task —— View job PR Review: fix(security): scope channel-permission node filter by sourceId (#3745)Tasks:
SummaryThis PR correctly identifies and fixes a real security bug: The fix is conceptually correct and the approach (forwarding Issues Found🔴 Incomplete Fix:
|
|
Thanks for the review — both points addressed in the next push: 1. `checkNodeChannelAccess` not source-scoped (valid — same leak class). The function already accepted/forwarded `sourceId`, but 7 call sites omitted it, so node-specific endpoints (telemetry rates/history, position-history/positions/position-override) had the same cross-source union fallback. Fixed all 7 to forward `req.query.sourceId` (server.ts ×3, telemetryRoutes.ts ×4). The v1 routes already passed it. Added a `checkNodeChannelAccess` regression test asserting the permission lookup receives the exact sourceId. 2. `getChannelDatabasePermissionsForUserAsSetAsync` not source-scoped — intentional. The `channel_database` (server-side decryption PSKs) is global-by-design: `channelDecryptionService` tries every enabled row regardless of source, and migration 063 dropped its dead `sourceId` column. So virtual-channel (>= CHANNEL_DB_OFFSET) permissions are global too. Added an explanatory comment at the call sites to prevent future confusion. |
…review) Addresses the review-bot finding on PR #3749: filterNodesByChannelPermission was fixed, but its sibling checkNodeChannelAccess (single-node access gate) had the same cross-source permission union at 7 call sites that omitted sourceId — node-specific endpoints (telemetry rates/history, position-history/positions/ position-override) could leak across sources for a guest. The function already forwarded sourceId; only the callers dropped it. - server.ts ×3 + telemetryRoutes.ts ×4 now forward req.query.sourceId. (The v1 routes already passed it.) - checkNodeChannelAccess regression test asserts the permission lookup gets the exact sourceId. - Documented that getChannelDatabasePermissionsForUserAsSetAsync is global by design (channel_database PSKs are tried across all sources; migration 063 dropped its sourceId), so virtual-channel permissions are intentionally global. Full suite 7550 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBLhGGNh35oMwTL53va1Y5
|
Addressed in ca4b3da: forwarded sourceId at all 7 |
|
Claude finished @Yeraze's task —— View job PR Review: fix(security): scope channel-permission node filter by sourceId (#3745)Tasks:
SummaryThe follow-up push ( What Changed
Remaining Observation (non-blocking)
// nodeEnhancer.ts:378
const node = await databaseService.nodes.getNode(nodeNum, sourceId);
// ^^^^^^^^
VerdictThe core security fixes are complete and correct. The regression tests are tight and targeted. The The PR is ready to merge. The |
) Completes the per-source permission check flagged in PR #3749 review: the node lookup that resolves the channel driving the access decision was unscoped (getNode(nodeNum)), so in a multi-source deployment the same nodeNum in another source — with a different channel assignment — could be fetched and the wrong channel used to gate access. getNode already accepts an optional sourceId; now forwarded. Regression test asserts getNode is called with the request's sourceId. Full suite 7550 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBLhGGNh35oMwTL53va1Y5
|
Addressed the remaining observation in 108f2d8: |
|
Claude finished @Yeraze's task —— View job PR Review: fix(security): scope channel-permission node filter by sourceId (#3745)Tasks:
SummaryThe latest push (108f2d8) addresses the final remaining observation from the previous review: What Was Fixed
VerificationSpot-checked the three endpoints in
All four in
The No Remaining IssuesThe three-push fix series fully closes the cross-source permission leak:
The The PR is ready to merge. |
Fixes #3745 — a cross-source permission leak for guests.
Bug
filterNodesByChannelPermissionaccepts asourceId, but several call sites fetched source-scoped nodes and then called the filter without the source id. WithsourceIdundefined,getUserPermissionSetAsyncfalls back to a union of permissions across all sources — so a guest holding channel-0 read on source A passes the filter for source B's channel-0 nodes, despite having zero permission on B.Fix
Forward the request's source id at every leaky call site (the function already scopes correctly when given one):
/api/nodes→nodesSourceId/api/nodes/active→activeNodesSourceId/poll: memory nodes and telemetry-availability →pollSourceIdunreadSourceId/telemetry/available/nodes→telAvailSourceIdroutes/v1/nodes.tsandsourceRoutes.tsalready passed the source id, so they were correct. Same bug class as #3712 (the fetch path was scoped but the permission-filter path wasn't).Test
Adds a
nodeEnhancer.test.tsregression assertingfilterNodesByChannelPermission(..., sourceId)forwards thesourceIdtogetUserPermissionSetAsync(i.e. no global union when a source is requested). Full suite 7549 passed, 0 failures; servertscclean.🤖 Generated with Claude Code