From a24709bac03e94150bb0c93784b3becc0ac270b6 Mon Sep 17 00:00:00 2001 From: feruzm Date: Tue, 23 Jun 2026 05:31:32 +0000 Subject: [PATCH 1/2] fix(register): allow all WebView frame schemes so the Turnstile challenge completes on iOS #3284 widened the Turnstile WebView's originWhitelist to ['https://*', 'about:'] and added an onShouldStartLoadWithRequest that returned true only for https/about. But react-native- webview gates every frame against originWhitelist, and Cloudflare's Managed challenge also spins up blob:/data: compute sub-frames. Those were cancelled, so the challenge never issued a token and the widget stayed blank with a dead "Register Free". about: was necessary but not sufficient on its own. This WebView only ever loads our own embed page plus the CF challenge it pulls in, so allow all schemes (originWhitelist ['*']) and drop the custom handler instead of enumerating the schemes Cloudflare uses. Add dev-only webviewDebuggingEnabled for on-device Safari Web Inspector diagnosis. --- .../register/children/turnstileWebView.tsx | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/screens/register/children/turnstileWebView.tsx b/src/screens/register/children/turnstileWebView.tsx index 715a464630..5bb4daa790 100644 --- a/src/screens/register/children/turnstileWebView.tsx +++ b/src/screens/register/children/turnstileWebView.tsx @@ -19,14 +19,16 @@ interface Props { height?: number; } -// Cloudflare's Managed challenge runs its verification compute in about:srcdoc / about:blank -// child frames (nested inside the challenges.cloudflare.com iframe). iOS WKWebView gates -// SUB-FRAME navigations by originWhitelist / onShouldStartLoadWithRequest too, so without the -// about: scheme those frames are silently dropped and the widget spins on "Verifying…" forever -// (the outer widget still renders, since it loaded over https). Allow https + about: only. -const _allowTurnstileFrame = (req: { url: string }) => - req.url.startsWith('https://') || req.url.startsWith('about:'); - +// Cloudflare's Managed challenge renders in a challenges.cloudflare.com iframe and runs its +// verification compute in nested sub-frames across several schemes: about:srcdoc / about:blank +// and, on iOS, often blob: / data:. react-native-webview gates EVERY frame navigation against +// originWhitelist. The previous list (['https://*', 'about:']) plus a custom +// onShouldStartLoadWithRequest that returned true only for https/about therefore CANCELLED the +// challenge's blob:/data: compute frames: about: was allowed, but that alone was not enough, so +// the challenge never issued a token and the widget stayed blank. +// This widget only ever loads our own first-party embed page plus that CF challenge, so rather +// than enumerate every scheme Cloudflare may use, allow them all (originWhitelist ['*']) and +// drop the custom handler (with ['*'] a handler could only re-add the same restriction). const TurnstileWebView = ({ onVerify, onExpire, onError, height = 76 }: Props) => { const intl = useIntl(); // A failed load of the hosted page (network error, or a 404 while the web @@ -73,14 +75,17 @@ const TurnstileWebView = ({ onVerify, onExpire, onError, height = 76 }: Props) = setLoadFailed(true)} From 988bc61de7ee512e36495356ad8cbc388829a58d Mon Sep 17 00:00:00 2001 From: feruzm Date: Tue, 23 Jun 2026 05:57:50 +0000 Subject: [PATCH 2/2] fix(register): guard only the top frame, keep sub-frames open (review) Address Greptile review: originWhitelist ['*'] with no handler left top-level navigation unguarded, so a compromised embed page or open redirect could steer the WebView to file:/javascript:/another origin. Restore onShouldStartLoadWithRequest but gate ONLY the top-level document to our own origin; every sub-frame is still allowed, so Cloudflare's about:/blob:/data: compute frames are never re-blocked (a sub-frame is never the top frame). --- .../register/children/turnstileWebView.tsx | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/screens/register/children/turnstileWebView.tsx b/src/screens/register/children/turnstileWebView.tsx index 5bb4daa790..7289996a80 100644 --- a/src/screens/register/children/turnstileWebView.tsx +++ b/src/screens/register/children/turnstileWebView.tsx @@ -20,15 +20,22 @@ interface Props { } // Cloudflare's Managed challenge renders in a challenges.cloudflare.com iframe and runs its -// verification compute in nested sub-frames across several schemes: about:srcdoc / about:blank -// and, on iOS, often blob: / data:. react-native-webview gates EVERY frame navigation against -// originWhitelist. The previous list (['https://*', 'about:']) plus a custom -// onShouldStartLoadWithRequest that returned true only for https/about therefore CANCELLED the -// challenge's blob:/data: compute frames: about: was allowed, but that alone was not enough, so -// the challenge never issued a token and the widget stayed blank. -// This widget only ever loads our own first-party embed page plus that CF challenge, so rather -// than enumerate every scheme Cloudflare may use, allow them all (originWhitelist ['*']) and -// drop the custom handler (with ['*'] a handler could only re-add the same restriction). +// verification compute in nested SUB-frames across several schemes: about:srcdoc / about:blank +// and, on iOS, often blob: / data:. react-native-webview gates EVERY frame against +// originWhitelist, and the previous narrow list (['https://*', 'about:']) plus a handler that +// returned true only for https/about CANCELLED the challenge's blob:/data: frames, so it never +// issued a token and the widget stayed blank. about: alone was not enough. +// +// Fix: pass every frame to the handler (originWhitelist ['*']) and gate only the TOP-LEVEL +// document. Sub-frames are always allowed, whatever scheme Cloudflare uses, so the challenge +// completes; the top frame must stay on our own first-party origin, which keeps a compromised +// page or open redirect from steering the WebView to file:/javascript:/another origin. +const TURNSTILE_ORIGIN = 'https://ecency.com/'; +const _shouldStartLoad = (req: { url: string; isTopFrame?: boolean }) => + // 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); + const TurnstileWebView = ({ onVerify, onExpire, onError, height = 76 }: Props) => { const intl = useIntl(); // A failed load of the hosted page (network error, or a 404 while the web @@ -76,6 +83,7 @@ const TurnstileWebView = ({ onVerify, onExpire, onError, height = 76 }: Props) =