Skip to content

fix(mobile): validate external URL schemes before opening (port #2494)#2498

Open
Gilbert09 wants to merge 2 commits into
mainfrom
posthog-code/mobile-validate-external-url-schemes
Open

fix(mobile): validate external URL schemes before opening (port #2494)#2498
Gilbert09 wants to merge 2 commits into
mainfrom
posthog-code/mobile-validate-external-url-schemes

Conversation

@Gilbert09

Copy link
Copy Markdown
Member

Summary

Ports the desktop hardening from #2494 to the mobile app. Before handing a URL to Linking.openURL / WebBrowser.openBrowserAsync, the scheme is validated against the shared allowlist (http:/https:/mailto: via @posthog/shared's isSafeExternalUrl), so a tampered or attacker-supplied URL — from a markdown link, chat content, signal report, or MCP app — can't trigger an unsafe scheme (file:, smb:, data:, javascript:, ms-msdt:, custom app deep-links, etc.). Unsafe URLs are silently dropped, preserving the existing no-op UX.

Changes

  • New helper apps/mobile/src/lib/openExternalUrl.ts — gates Linking.openURL behind isSafeExternalUrl and keeps the .catch(() => {}) no-op on failure.
  • Routed the untrusted call sites through it:
    • chat/MarkdownText.tsx (arbitrary markdown links)
    • chat/MarkdownImage.tsx, chat/GithubRefChip.tsx, chat/PostHogRefChip.tsx
    • inbox/SignalCard.tsx (externalUrl), inbox/SuggestedReviewers.tsx
    • tasks/PrStatusBadge.tsx
    • mcp-servers/template/[id].tsx (docs_url)
  • MCP app bridge mcp/McpAppHost.tsx — gates WebBrowser.openBrowserAsync behind isSafeExternalUrl (the URL comes from the sandboxed WebView).

OAuth flows (openAuthSessionAsync) and the fixed-domain settings link open trusted PostHog/GitHub URLs and are intentionally left unchanged.

RN URL note

The task flagged that React Native's URL polyfill can parse differently. Confirmed against RN 0.81's Libraries/Blob/URL.js: its protocol getter extracts the scheme via /^([a-zA-Z][a-zA-Z\d+\-.]*):/, so isSafeExternalUrl classifies schemes correctly on Hermes — no fallback scheme check is needed.

Tests

apps/mobile/src/lib/openExternalUrl.test.ts:

  • http/https/mailto allowed; javascript:/file:/data:/smb:/custom-scheme and unparseable input rejected.
  • A rejected URL never calls Linking.openURL; a safe URL does.
  • Exercises isSafeExternalUrl inside the mobile package to confirm it resolves and runs in this bundle.

Full mobile suite: 215 passing. Lint clean; typecheck adds no new errors. No desktop or packages/shared code was touched.

Port the desktop hardening from #2494 to the mobile app: before handing a
URL to `Linking.openURL` / `WebBrowser.openBrowserAsync`, validate its scheme
against the shared allowlist (`http:`/`https:`/`mailto:`) so tampered or
attacker-supplied URLs from markdown, chat, signal reports, or MCP app content
can't trigger unsafe schemes (`file:`, `data:`, `javascript:`, custom
deep-links, etc.).

- Add `openExternalUrl(url)` helper in `apps/mobile/src/lib` that gates
  `Linking.openURL` behind `@posthog/shared`'s `isSafeExternalUrl` and keeps
  the existing silent no-op on failure.
- Route the untrusted Linking call sites through it: MarkdownText, MarkdownImage,
  GithubRefChip, PostHogRefChip, SignalCard, SuggestedReviewers, PrStatusBadge,
  and the MCP template docs link.
- Gate the MCP app bridge `WebBrowser.openBrowserAsync` behind `isSafeExternalUrl`.
- Add unit tests covering the allow/deny matrix and that a rejected URL never
  reaches `Linking.openURL`. The test also exercises `isSafeExternalUrl` inside
  the mobile package to confirm it resolves and runs there. RN 0.81's `URL`
  extracts schemes via regex in its `protocol` getter, so no polyfill fallback
  is needed.

Generated-By: PostHog Code
Task-Id: 4fe18724-3034-4b84-8924-5b52a4b933fe
@Gilbert09 Gilbert09 requested a review from a team June 5, 2026 10:47
@greptile-apps

greptile-apps Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/mobile/src/features/mcp/components/McpAppHost.tsx:111-114
**Silent drop on blocked WebView URL**

`handleOpenLink` returns early without logging when the URL fails `isSafeExternalUrl`, whereas `openExternalUrl` always emits a `log.warn`. The MCP WebView bridge is the highest-risk source of attacker-controlled URLs, so a blocked attempt here would go completely undetected in logs. Consider adding a `log.warn` before the early return to match the helper's behaviour.

### Issue 2 of 2
apps/mobile/src/lib/openExternalUrl.test.ts:43-54
**Non-parameterised tests in `openExternalUrl` block**

The `isSafeExternalUrl` tests above correctly use `it.each`, but the two `openExternalUrl` tests are written as standalone cases. Given the team's preference for parameterised tests, both cases could be expressed as a single `it.each` table with columns `[url, expectedCall]` — checking `toHaveBeenCalledWith(url)` for safe URLs and `not.toHaveBeenCalled()` for unsafe ones (using a sentinel like `null` to distinguish). With only two cases the gain is small, but it would also make it easier to add future URL examples.

Reviews (1): Last reviewed commit: "fix(mobile): validate external URL schem..." | Re-trigger Greptile

Comment thread apps/mobile/src/features/mcp/components/McpAppHost.tsx
Comment thread apps/mobile/src/lib/openExternalUrl.test.ts
Address Greptile review feedback:

- McpAppHost.handleOpenLink now logs a warning when it drops an unsafe URL,
  matching openExternalUrl's behaviour. The MCP WebView bridge is the
  highest-risk URL source, so blocked attempts shouldn't be silent.
- Collapse the two openExternalUrl test cases into a single it.each table.

Generated-By: PostHog Code
Task-Id: 4fe18724-3034-4b84-8924-5b52a4b933fe
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