Skip to content

Close remaining authz gaps from PR #320#350

Open
tlongwell-block wants to merge 2 commits intomainfrom
tlongwell/close-authz-gaps-pr320
Open

Close remaining authz gaps from PR #320#350
tlongwell-block wants to merge 2 commits intomainfrom
tlongwell/close-authz-gaps-pr320

Conversation

@tlongwell-block
Copy link
Copy Markdown
Collaborator

@tlongwell-block tlongwell-block commented Apr 17, 2026

Summary

Cherry-pick of commit 3056641 from jm/morevulns — this was authored during the review of #320 but accidentally excluded from the squash-merge (approval was recorded against the prior HEAD).

All 4 security gaps are still open on main. No equivalent fixes have landed since.

Changes

Security fixes

  1. trigger_workflow: Require workflow ownership for channel workflows. Previously any channel member could trigger any workflow in that channel, executing actions attributed to the workflow owner.

  2. list_run_approvals: Require workflow ownership. Previously any channel member could read approval records (including actionable token hashes) for any workflow in the channel.

  3. REQ handler: Block global subscriptions for channel-restricted tokens. The fan_out() path delivers global events to all global subscriptions without per-connection channel filtering, so restricted tokens must not register global subscriptions.

  4. GET /api/events/:id: Block global event access for channel-restricted tokens. Matches the policy already enforced by the search endpoints.

Cosmetic

  1. list_workflow_runs: Fix misleading error string ("trigger""view runs for").

Crossfire review follow-ups (commit 3)

Dual-model crossfire review (Claude Opus 4.6 + OpenAI Codex) identified the following issues, now addressed:

SSRF hardening

_isPrivateHost() only handled IPv4 dotted-quad notation. Rewrote to use InternetAddress.tryParse() for canonical IP normalisation. Now blocks:

  • IPv6 private/link-local/loopback (fc00::/7, fe80::/10, ::1)
  • IPv4-mapped IPv6 (::ffff:10.0.0.1)
  • Decimal/octal/hex IP encodings (2130706433, 0177.0.0.1)
  • 0.0.0.0/8 unspecified and full 127.0.0.0/8 loopback range
  • DNS rebinding mitigated by HTTPS cert validation in production

Workflow authz consistency

get_workflow used the old else if pattern (channel members could view any workflow), while trigger, list_runs, and list_approvals now require ownership. Aligned get_workflow to also use require_workflow_owner() for consistency.

DM presence widget deduplication

Extracted ~90 lines of identical presence lookup + avatar rendering from _DmAvatar (channels_page) and _DmAppBarTitle (channel_detail_page) into shared dm_presence_avatar.dart.

Presence cache hardening

  • Capped tracked set at 200 entries (oldest-first eviction)
  • Added untrack() method for cleanup
  • Added exponential backoff on fetch failure (30s → 5min max)
  • Reset state on provider rebuild

Files changed

Commit 1 (b106fba): feat(mobile)

  • mobile/lib/features/channels/channel_detail_page.dart
  • mobile/lib/features/channels/channel_management_provider.dart
  • mobile/lib/features/channels/channels_page.dart
  • mobile/lib/features/pairing/pairing_page.dart
  • mobile/lib/features/pairing/pairing_provider.dart
  • mobile/lib/features/profile/presence_cache_provider.dart
  • mobile/lib/shared/relay/relay_session.dart
  • mobile/lib/app.dart
  • mobile/test/features/channels/channel_detail_page_test.dart

Commit 2 (5532e0d): Security fixes

  • crates/sprout-relay/src/api/events.rs
  • crates/sprout-relay/src/api/workflows.rs
  • crates/sprout-relay/src/handlers/req.rs

Commit 3 (f184b63): Crossfire review follow-ups

  • crates/sprout-relay/src/api/workflows.rs — align get_workflow authz
  • mobile/lib/features/channels/dm_presence_avatar.dart — new shared widget
  • mobile/lib/features/channels/channel_detail_page.dart — use shared widget
  • mobile/lib/features/channels/channels_page.dart — use shared widget
  • mobile/lib/features/pairing/pairing_provider.dart — SSRF hardening
  • mobile/lib/features/profile/presence_cache_provider.dart — backoff + cap

Fix authorization holes identified during crossfire security review:

1. trigger_workflow: require workflow ownership for channel workflows.
   Previously any channel member could trigger any workflow in that
   channel, executing actions attributed to the workflow owner.

2. list_run_approvals: require workflow ownership. Previously any
   channel member could read approval records (including actionable
   token hashes) for any workflow in the channel.

3. REQ handler: block global subscriptions for channel-restricted
   tokens. The fan_out() path delivers global events to all global
   subscriptions without per-connection channel filtering, so
   restricted tokens must not register global subscriptions.

4. GET /api/events/:id: block global event access for channel-
   restricted tokens. Matches the policy already enforced by the
   search endpoints (no globals for restricted tokens).

5. list_workflow_runs: fix misleading error string ('trigger' ->
   'view runs for').
1. SSRF hardening: rewrite _isPrivateHost() to use InternetAddress.tryParse()
   for canonical IP normalisation. Now blocks IPv6 private/link-local/loopback,
   IPv4-mapped IPv6 (::ffff:x.x.x.x), decimal/octal/hex IP encodings,
   0.0.0.0/8, and the full 127.0.0.0/8 loopback range. DNS rebinding
   mitigated by HTTPS cert validation in production.

2. Workflow authz consistency: align get_workflow with trigger/list_runs/
   list_approvals — all now require ownership via require_workflow_owner().
   Previously get_workflow allowed any channel member to view, creating an
   inconsistency with the other endpoints.

3. Extract shared DM presence widget: deduplicate ~90 lines of identical
   presence lookup + avatar rendering from _DmAvatar (channels_page) and
   _DmAppBarTitle (channel_detail_page) into dm_presence_avatar.dart.

4. Presence cache hardening: cap tracked set at 200 (oldest-first eviction),
   add untrack() method, add exponential backoff on fetch failure (30s base,
   doubles per failure, 5min max), reset state on provider rebuild.
@tlongwell-block tlongwell-block force-pushed the tlongwell/close-authz-gaps-pr320 branch from 5eaf5bd to f184b63 Compare April 17, 2026 16:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant