Skip to content

fix(register): gate Turnstile WebView by https scheme, not origin (fixes Android)#3289

Merged
feruzm merged 2 commits into
developmentfrom
fix/turnstile-webview-android-https-gate
Jun 23, 2026
Merged

fix(register): gate Turnstile WebView by https scheme, not origin (fixes Android)#3289
feruzm merged 2 commits into
developmentfrom
fix/turnstile-webview-android-https-gate

Conversation

@feruzm

@feruzm feruzm commented Jun 23, 2026

Copy link
Copy Markdown
Member

Follow-up to #3287 (iOS Turnstile fix — confirmed working on TestFlight).

Problem

#3287's top-frame guard gates the WebView's top document to our origin:

req.isTopFrame === false || req.url.startsWith('https://ecency.com/')

That works on iOS but breaks Turnstile on Android. Android's react-native-webview shouldStart event (createWebViewEvent, RNCWebViewClient.java) does not include isTopFrame, so on Android req.isTopFrame is always undefined and the handler collapses to:

req.url.startsWith('https://ecency.com/')

Android routes iframe navigations through shouldOverrideUrlLoading too, so the Cloudflare challenge's https://challenges.cloudflare.com/... frame is evaluated by this handler — it doesn't start with ecency.com, so it's cancelled and the challenge never completes.

Fix

Gate by scheme instead of origin:

req.isTopFrame === false || req.url.startsWith('https://')
  • iOS: unchanged behaviour — sub-frames allowed via isTopFrame === false, top frame is our https embed page. (The real flow never navigates the top frame off ecency.com, so dropping the origin check is a no-op for it.)
  • Android: the Cloudflare https iframe now passes, so the challenge completes.
  • Still blocks a top-level file:/javascript: navigation (Greptile's defense-in-depth concern from fix(register): allow all WebView frame schemes so the Turnstile challenge completes on iOS #3287).

Testing

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced WebView navigation in the registration flow by updating navigation gating behavior, allowing non-top frame requests while restricting top-frame navigations to explicitly allowed HTTPS hosts (https://ecency.com/ and https://challenges.cloudflare.com/).

… (Android)

The merged origin gate (url.startsWith('https://ecency.com/')) breaks Turnstile on
Android. Android's react-native-webview shouldStart event carries no isTopFrame field,
so the handler collapses to "allow only our origin" for every navigation Android routes
through it -- and Android routes the challenge's https iframe (challenges.cloudflare.com)
through it too, so that frame gets cancelled and the challenge never completes.

Gate by scheme instead: allow all sub-frames (iOS reports isTopFrame=false) and allow any
https top-level navigation, which keeps Cloudflare's https iframe working on both platforms
while still blocking a top-level file:/javascript: navigation. iOS behaviour is unchanged
(verified working on TestFlight); this only relaxes the Android-affecting origin check.
@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes the Turnstile WebView registration flow on Android by replacing a narrow origin guard (https://ecency.com/) with a host-pinned allowlist that also permits https://challenges.cloudflare.com/. The previous fix (#3287) used isTopFrame === false to pass sub-frames freely, but Android's WebView client never sets isTopFrame, so all navigations fell through to the origin check and the Cloudflare challenge iframe was blocked.

  • The committed code is actually more conservative than the PR title/description suggests: rather than gating on any https:// URL it pins to exactly two hosts (ecency.com and challenges.cloudflare.com), closing the arbitrary-HTTPS-navigation gap that was flagged in the prior review thread.
  • The in-file comments are thorough and explain both the Android isTopFrame-is-undefined caveat and why challenges.cloudflare.com must be explicitly allowed.

Confidence Score: 5/5

Safe to merge — the guard change is tightly scoped, well-commented, and the actual implementation is more conservative than the PR description implies.

The change replaces one URL prefix check with a two-host allowlist. Both hosts are necessary and correct: ecency.com for the embed page and challenges.cloudflare.com for the Android path where the iframe triggers the handler. The _onMessage token-acceptance surface is unchanged, and any accepted token still requires server-side Cloudflare validation. No regressions on the iOS path were introduced.

No files require special attention.

Important Files Changed

Filename Overview
src/screens/register/children/turnstileWebView.tsx Replaces the single-origin top-frame guard with a host-pinned two-host allowlist; well-commented, no logic or security regressions found.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant App as TurnstileWebView
    participant WV as react-native-webview
    participant Guard as _shouldStartLoad
    participant Ecency as ecency.com/embed/turnstile
    participant CF as challenges.cloudflare.com

    App->>WV: load https://ecency.com/embed/turnstile
    WV->>Guard: "req {url: ecency.com, isTopFrame: true|undefined}"
    Guard-->>WV: _isAllowedTopUrl → true
    WV->>Ecency: fetch embed page
    Ecency->>WV: "render Turnstile widget (iframe src=challenges.cloudflare.com)"
    WV->>Guard: "req {url: challenges.cloudflare.com, isTopFrame: false (iOS) | undefined (Android)}"
    Note over Guard: iOS: isTopFrame===false → true
    Note over Guard: Android: _isAllowedTopUrl(challenges.cloudflare.com) → true
    Guard-->>WV: allow
    WV->>CF: load challenge iframe
    CF->>WV: "challenge complete, postMessage {type:verify, token}"
    WV->>App: onMessage → onVerify(token)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant App as TurnstileWebView
    participant WV as react-native-webview
    participant Guard as _shouldStartLoad
    participant Ecency as ecency.com/embed/turnstile
    participant CF as challenges.cloudflare.com

    App->>WV: load https://ecency.com/embed/turnstile
    WV->>Guard: "req {url: ecency.com, isTopFrame: true|undefined}"
    Guard-->>WV: _isAllowedTopUrl → true
    WV->>Ecency: fetch embed page
    Ecency->>WV: "render Turnstile widget (iframe src=challenges.cloudflare.com)"
    WV->>Guard: "req {url: challenges.cloudflare.com, isTopFrame: false (iOS) | undefined (Android)}"
    Note over Guard: iOS: isTopFrame===false → true
    Note over Guard: Android: _isAllowedTopUrl(challenges.cloudflare.com) → true
    Guard-->>WV: allow
    WV->>CF: load challenge iframe
    CF->>WV: "challenge complete, postMessage {type:verify, token}"
    WV->>App: onMessage → onVerify(token)
Loading

Reviews (2): Last reviewed commit: "fix(register): pin Turnstile top-frame t..." | Re-trigger Greptile

Comment thread src/screens/register/children/turnstileWebView.tsx Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e668e00e4d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// isTopFrame is iOS-only; on Android the handler only fires for the main frame, so a missing
// flag is treated as the top frame too. Only the top document is constrained to our origin.
req.isTopFrame === false || req.url.startsWith(TURNSTILE_ORIGIN);
req.isTopFrame === false || req.url.startsWith('https://');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict the Android iframe exception to the challenge host

When Android delivers a request without a reliable frame signal, this line treats every HTTPS URL as allowed. That fixes the Cloudflare iframe case but also permits any top-level HTTPS navigation initiated by the embed page (for example an external link or redirect) into the same WebView; the WebView messaging bridge is enabled for all origins and _onMessage accepts verify from whatever page is loaded, so such a page can enable the free-register button with arbitrary data instead of keeping the challenge confined to Ecency/Cloudflare. Keep the top document on ecency.com and limit the Android no-frame-signal exception to the known Turnstile host(s), rather than whitelisting all HTTPS origins.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 90682d6. Pinned the top document to the two hosts the widget actually loads -- https://ecency.com/ (embed page) and https://challenges.cloudflare.com/ (the challenge) -- instead of allowing any https. So a stray top-level navigation to an arbitrary https page can no longer load and post a fake verify token over the bridge. challenges.cloudflare.com is allow-listed explicitly so the challenge iframe still passes on Android (where isTopFrame is undefined and the iframe is routed through this handler); sub-frames remain unconditionally allowed on iOS via isTopFrame === false.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1234b8a8-b26c-4431-b6ab-9535b5fd1b51

📥 Commits

Reviewing files that changed from the base of the PR and between e668e00 and 90682d6.

📒 Files selected for processing (1)
  • src/screens/register/children/turnstileWebView.tsx

📝 Walkthrough

Walkthrough

The onShouldStartLoadWithRequest handler in turnstileWebView.tsx is updated to unconditionally allow all sub-frame navigations (isTopFrame === false) and restrict top-frame navigations to two explicitly allowed HTTPS hosts: https://ecency.com/ and https://challenges.cloudflare.com/, replacing the prior check that only referenced the https://ecency.com/ origin constant.

Changes

Turnstile WebView Navigation Gating

Layer / File(s) Summary
Navigation gating predicate update
src/screens/register/children/turnstileWebView.tsx
_shouldStartLoad now allows all sub-frames unconditionally and restricts top-frame navigations to URLs starting with https://ecency.com/ or https://challenges.cloudflare.com/, replacing the prior single origin-constant check.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related PRs

  • ecency/ecency-mobile#3225: Directly modifies the same turnstileWebView.tsx file and the same onShouldStartLoadWithRequest navigation control logic that this PR updates.

Poem

🐇 A tweak to the Turnstile's protective gate,
Two hosts approved—no need to debate.
Sub-frames roam free, top-frames stay secure,
From Cloudflare and home, the paths are pure.
Hopping through widgets with confidence anew! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing Turnstile WebView gating by HTTPS scheme instead of origin, specifically addressing the Android regression mentioned in the objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/turnstile-webview-android-https-gate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…review)

Address Codex/Greptile review on #3289: gating the top frame by bare https scheme
is too loose. Because the ReactNativeWebView bridge + _onMessage accept a `verify`
token from whatever page is loaded, a stray top-level navigation to an arbitrary
https page could post a fake token. Pin the top document to the two hosts the widget
actually loads -- our embed page (ecency.com) and Cloudflare's challenge
(challenges.cloudflare.com). Sub-frames stay unconditionally allowed (iOS isTopFrame),
and challenges.cloudflare.com is allow-listed explicitly so the challenge iframe still
passes on Android, where isTopFrame is undefined.
@feruzm feruzm merged commit 6d8ec4b into development Jun 23, 2026
9 of 12 checks passed
@feruzm feruzm deleted the fix/turnstile-webview-android-https-gate branch June 23, 2026 08:22
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