Skip to content

fix(captcha): fall back to external browser when system WebView is disabled (refs #303)#736

Open
jim-daf wants to merge 1 commit intomollyim:mainfrom
jim-daf:fix/security-issue-303-captcha-browser-fallback
Open

fix(captcha): fall back to external browser when system WebView is disabled (refs #303)#736
jim-daf wants to merge 1 commit intomollyim:mainfrom
jim-daf:fix/security-issue-303-captcha-browser-fallback

Conversation

@jim-daf
Copy link
Copy Markdown

@jim-daf jim-daf commented Apr 22, 2026

Open captcha in the default browser when WebView is disabled (refs #303)

Problem

Issue #303 reports that disabling / removing the system
WebView (a common configuration on low-storage devices and
some hardened ROMs) bricks registration: CaptchaFragment
unconditionally calls into the WebView and either silently
fails to render or crashes when the WebView constructor
throws AndroidRuntimeException("WebView ... not available").

Fix

Wrap the WebView access in a try/catch. If accessing the
WebView (binding.registrationCaptchaWebView and its
settings) throws, or if loadUrl throws, fall back to
launching BuildConfig.SIGNAL_CAPTCHA_URL via
Intent.ACTION_VIEW. The existing signalcaptcha:// URI
scheme handler (the same one the share-sheet uses today)
brings the user back into the app with the token and
handleCaptchaToken runs as normal.

If no browser is available either we just navigateUp() so
the user can back out and retry, no worse than the current
blank-screen behaviour.

Why this is in scope of mollyim-android

Upstream Signal does not have to care because it does not
ship a browser-light user base. The mollyim user base has
explicitly asked for it (#303) and the change is
self-contained inside the fork's existing CaptchaFragment
(no AndroidManifest or shared-component edits).

Notes

  • The intent filter for signalcaptcha:// is already
    declared (it is what makes the share-sheet workflow
    function today), so the round-trip through the browser
    works without any manifest changes.
  • clearCache(true) is preserved on the WebView path; the
    browser path does not need it because the user's browser
    manages its own cache.

…ollyim#303)

Refs mollyim#303

Users on low-storage devices and some hardened ROMs disable
or remove the system WebView. Today that bricks registration
because `CaptchaFragment` unconditionally tries to load the
captcha into a WebView and either silently fails to render
or crashes when the WebView constructor / inflate throws.

Wrap the WebView setup in a try/catch. If the WebView is
not available, fall back to opening
`BuildConfig.SIGNAL_CAPTCHA_URL` in the user's default
browser. The existing `signalcaptcha://` scheme handler
(used by the share-sheet today) brings the user back into
the app with the token and registration completes normally.
If no browser is available either we just navigate up so
the user can retry rather than hanging on a blank screen.
@jim-daf jim-daf marked this pull request as ready for review April 22, 2026 14:09
Copilot AI review requested due to automatic review settings April 22, 2026 14:09
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to prevent registration from breaking on devices where the system WebView provider is disabled/uninstalled by falling back to opening the CAPTCHA page in an external browser.

Changes:

  • Wrap WebView access/configuration and loadUrl() in try/catch to detect WebView failures.
  • Add a fallback path that launches BuildConfig.SIGNAL_CAPTCHA_URL via Intent.ACTION_VIEW when WebView is unavailable.
  • Navigate up from the CAPTCHA screen after triggering the fallback behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +71 to 80
private fun openCaptchaInBrowser() {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(BuildConfig.SIGNAL_CAPTCHA_URL))
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
// No browser available; let the user back out.
}
binding.registrationCaptchaWebView.loadUrl(BuildConfig.SIGNAL_CAPTCHA_URL)
findNavController().navigateUp()
}
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

openCaptchaInBrowser() always calls findNavController().navigateUp() even when the browser intent launches successfully. This doesn’t match the PR description (“If no browser is available either we just navigateUp()”) and can also make the UI jump back immediately after launching the browser. Consider only navigating up in the ActivityNotFoundException case, or making this behavior explicit in the PR description and UX (e.g., show a message / button while waiting for the token).

Copilot uses AI. Check for mistakes.

private fun openCaptchaInBrowser() {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(BuildConfig.SIGNAL_CAPTCHA_URL))
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

Intent.FLAG_ACTIVITY_NEW_TASK is typically only needed when starting an activity from a non-Activity context. Here you’re calling Fragment.startActivity(), so the flag is unnecessary and may change task/back-stack behavior. Consider removing it, or only adding it conditionally (similar to CommunicationActions.openBrowserLink(), which only sets NEW_TASK when the Context is not an Activity).

Suggested change
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +68
try {
webView.settings.javaScriptEnabled = true
webView.clearCache(true)

webView.webViewClient = object : WebViewClient() {
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
if (url.startsWith(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME)) {
val token = url.substring(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME.length)
handleCaptchaToken(token)
findNavController().navigateUp()
return true
}
return false
}
return false
}
webView.loadUrl(BuildConfig.SIGNAL_CAPTCHA_URL)
} catch (e: Throwable) {
// Catches AndroidRuntimeException("WebView ... not available") thrown
// by the WebView constructor / settings when the WebView package is
// disabled at runtime.
openCaptchaInBrowser()
}
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

Catching Throwable here is overly broad (it will also catch serious Errors like OOM, linkage errors, etc.) and can mask unrelated bugs. Since the intent is to detect “WebView provider not available”, prefer catching the specific runtime exceptions thrown by WebView when the provider is missing (e.g., AndroidRuntimeException / InflateException depending on where handled) or at least Exception/RuntimeException rather than Throwable.

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +44
// Issue #303: users that have disabled / removed the system WebView
// (low-storage devices, hardened ROMs) cannot complete the captcha
// because the WebView either fails to inflate or refuses to load.
// Detect that case and fall back to opening the captcha URL in the
// user's default browser — the `signalcaptcha://` URI scheme handler
// brings them back into the app with the token via the existing
// intent filter.
val webView = try {
binding.registrationCaptchaWebView
} catch (e: Throwable) {
openCaptchaInBrowser()
return
}
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

Catching errors in onViewCreated won’t prevent the crash reported in #303 when WebView inflation fails: fragment_registration_captcha.xml instantiates directly, so devices without a WebView provider can throw an InflateException before onViewCreated runs. To make this fallback effective, the WebView needs to be inflated/created inside a try/catch (e.g., override onCreateView and fall back to a non-WebView layout / ViewStub / programmatically-added WebView) so the fragment can render and open the browser instead of crashing.

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +38
// Detect that case and fall back to opening the captcha URL in the
// user's default browser — the `signalcaptcha://` URI scheme handler
// brings them back into the app with the token via the existing
// intent filter.
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

The browser fallback assumes the captcha page can redirect back into the app via signalcaptcha://…, but this repo doesn’t appear to declare any manifest intent-filter for the signalcaptcha scheme (app/src/main/AndroidManifest.xml only registers sgnl, tsdevice, and https). Without a scheme handler, completing the captcha in an external browser won’t be able to return the token to the app. Either add an exported activity intent-filter for signalcaptcha:// that forwards the token into the registration flow, or change the captcha URL/return scheme to one that is already handled (e.g., existing sgnl:// deep link handling).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants