Native Content-Feed Link Hardening

Filed 2026-07-15 from the range-bag-suggested-items PR-B review (flagged by the worker lane, verified and ruled by the architect). Content-feed surface — belongs to whoever next works that code, not the range-bag lane.

Summary

The native content-feed detail screens open the feed-supplied actionUrl without the scheme allowlist (and, on Android, without the defensive open) that the PWA and the range-bag store links already use. Low severity today, but the pattern should be consistent everywhere a server-supplied URL is opened.

Requirements

  • Android and iOS content-feed detail screens open actionUrl only when it passes an http/https scheme allowlist (mirror safeStoreLink / the PWA messages.rs check).
  • The Android open is defensive: an unhandlable or malformed URL must not crash the app (try/catch around startActivity).
  • Unit tests pin the allowlist on both platforms (same five-case shape as the range-bag tests: https, http, javascript:, ftp:, null).

Context

PR-B (#238) established the client-side pattern for opening server-supplied URLs: an http/https allowlist plus a safe external open (safe_store_link / safeStoreLink on all three platforms). The PWA’s content-feed counterpart already follows it (apps/pwa/src/pages/messages.rs:283-288 checks the scheme before rendering the anchor). The native content-feed screens predate the pattern.

Details

  • Android — two defects (apps/android/.../contentfeed/ContentFeedDetailScreen.kt:94): Intent(Intent.ACTION_VIEW, Uri.parse(entry.actionUrl)) + startActivity with no scheme allowlist and no try/catch.
    1. A malicious/unexpected URL deep-links users into arbitrary scheme handlers.
    2. An unhandlable or malformed URL throws ActivityNotFoundException and crashes the app on tap — no attacker needed; a content typo is enough.
  • iOS — same family, milder (apps/ios/RangeDayPro/ContentFeed/ContentFeedDetailView.swift:29): Link(destination:) hands any scheme to the OS. No crash path, but no allowlist.
  • Fix precedent: the PWA content-feed check (messages.rs:283-288) and PR-B’s safeStoreLink pattern — roughly 10 lines per platform plus unit tests: allowlist the scheme, open defensively. range-bag-packing-checklist adds the try/catch half to the Android range-bag open (scope addition recorded there), so by the time this item is picked up the range-bag code models both halves.

Severity is honestly low: feed entries are server-authored — there is no end-user injection path, and the dashboard has no actionUrl authoring UI today. Filed so the inconsistency is tracked, not because anything is exploitable right now.

Open Questions

None — scope and precedent are settled; this is a mechanical hardening pass for whoever next touches the content-feed surface.