Repository navigation
Conversation
The challenge opens its `sms:` link with `target="_blank"`. On Android `setSupportMultipleWindows` is always on in react-native-webview, so that navigation goes to `WebChromeClient.onCreateWindow` and never reaches `onShouldStartLoadWithRequest`. Measured on a Pixel 7: with no `onOpenWindow` prop set, no JS callback fires at all and the URL is handed to the system, which shows an app chooser listing WhatsApp - the app the challenge tells users not to use. The same applies to the `https://www.hcaptcha.com` policy links, which left the app the same way. Route both callbacks through one handler and add `onOpenWindow`, which recovers every `target="_blank"` case in JS. Parse the link rather than passing it through. The recipient can carry formatting a messaging app rejects, and the challenge emits an empty leading query parameter (`?&body=`). Splitting on `?` before `;` keeps the body of an RFC 5724 link with subscriber params, and percent-decoding is done without form semantics so a literal `+` survives. The body is re-encoded unchanged so the one-time code reaches the composer byte-exact. The rejection message from `Linking.openURL` embeds the URL, and the body carries the one-time code, so it is no longer forwarded to `onMessage`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
ควย
เมื่อ จันทร์ 5 ต.ค. 2569 เวลา 01:28 Alex Babrykovich <
***@***.***> เขียนว่า:
… The problem
The MFA inbound-SMS challenge opens its sms: link with target="_blank".
On Android that
navigation never reaches onShouldStartLoadWithRequest, so the handler
added for it has been
dead code on the live path.
Mechanism, from the installed react-native-webview source:
- RNCWebViewManagerImpl.kt:79 calls
settings.setSupportMultipleWindows(true) unconditionally,
so target="_blank" goes to WebChromeClient.onCreateWindow.
- RNCWebChromeClient.onCreateWindow only dispatches TopOpenWindowEvent
when the onOpenWindow
prop is set. We never set it. Otherwise it creates a bare WebView with
no WebViewClient,
which hands the URL to ActivityManager.
Identical in the 13.13.4 we pin and the 13.16.1 the e2e host uses, so this
is not version-specific.
Measured, not assumed
Run on a physical Pixel 7 with a page reproducing the challenge's link
shapes. "Current" = the
onOpenWindow prop unset, i.e. master.
Link Callback fired Result
sms: plain onShouldStartLoadWithRequest stays in app
*sms: target="_blank"* *none* *app chooser, new task*
sms: via window.open() *none* app chooser, new task
https://www.hcaptcha.com plain onShouldStartLoadWithRequest stays in app
*https://www.hcaptcha.com <https://www.hcaptcha.com> target="_blank"*
*none* *external app, new task*
Two things worth flagging:
1. The chooser lists WhatsApp — the app the challenge explicitly tells
users not to use.
2. The same bug hits the hCaptcha policy links, which had not been
reported.
With onOpenWindow set, all four _blank cases are handled in JS and the
app stays foregrounded.
*iOS is almost certainly unaffected today*, but this is a source reading,
not a device run:
RNCWebViewImpl.m:1329 guards the createWebViewWith short-circuit with
if (_onOpenWindow && !hasTargetFrame), so with the prop unset _blank
falls through to
onShouldStartLoadWithRequest.
Changes
- *hcaptchaSmsLink.js* — parser for sms:/smsto: URLs. Behaviour ported
from
HCaptchaSmsLink.java in hcaptcha-android-sdk#279 (the behaviour and
its test table, not the
Java). Splits on ? before ; so an RFC 5724 link with subscriber params
keeps its body;
skips empty query pairs, since the challenge emits ?&body=;
percent-decodes without form
semantics so a literal + survives; keeps malformed escapes as literal
text; reduces the
recipient to + and digits.
- *Hcaptcha.js* — one handleExternalUrl shared by
onShouldStartLoadWithRequest and the new
onOpenWindow. The link is rebuilt from the parsed parts, so the body
is re-encoded unchanged
and reaches the composer byte-exact.
- *Security fix:* Linking.openURL's rejection message embeds the URL,
and the body carries the
one-time code, so that message is no longer forwarded through onMessage
— host apps log it.
This is the one intentional behaviour change to an existing assertion.
Claims from the original research that did not hold here
- *smsto: does not avoid the chooser.* cmd package query-activities
shows com.whatsapp
claiming sms: and smsto: identically, for both VIEW and SENDTO. Both
open the same
chooser. sms: is therefore kept, and no Android-only scheme switch is
included.
Not verified — please do not read this as a finished investigation
- *The live MFA challenge was never run.* Everything above uses a
synthetic page
reproducing the link shapes, not an enterprise MFA sitekey. The
routing finding is solid;
the assumption that the live challenge uses target="_blank" is
inherited from the Android
SDK work, not re-confirmed here.
- *Body prefill is unverified.* Google Messages rejected the dummy
recipient and opened the
conversation list for both ?body= and ?&body=, so the two shapes could
not be told apart.
- *One deliberate iOS behaviour change is untested on hardware.*
Adding onOpenWindow makes
iOS route _blank there rather than loading in the WebView; unhandled
URLs now open
externally. Low risk, as the WebView only hosts the challenge, but it
is a change.
Test plan
- npm test — 76 passing, 5 suites (15 new parser cases + onOpenWindow
coverage).
- npm run lint — clean.
Note: both require __e2e__/host to be absent, which is its normal state.
When that generated
directory is present, jest picks up a second React via the haste map and
32 tests fail
*on master too* — pre-existing, unrelated to this PR, and worth a
separate fix
(modulePathIgnorePatterns, plus gitignoring dist/ and coverage/ so eslint
. stops reading
build output).
🤖 Generated with Claude Code <https://claude.com/claude-code>
------------------------------
You can view, comment on, or merge this pull request online at:
#119
Commit Summary
- 33cfc04
<33cfc04>
fix: handle the MFA sms link on the callback the challenge actually uses
File Changes
(6 files
<https://github.com/hCaptcha/react-native-hcaptcha/pull/119/files>)
- *M* Hcaptcha.js
<https://github.com/hCaptcha/react-native-hcaptcha/pull/119/files#diff-b821a110bb6fd53e34ac91b43e78d02a2f6b845a8e0de758d0be3c2904866567>
(72)
- *M* __tests__/Hcaptcha.test.js
<https://github.com/hCaptcha/react-native-hcaptcha/pull/119/files#diff-f6de0e9176ef94c7038bace1a5c693e150314e5dcb1b3d5723e299ad76011a47>
(43)
- *M* __tests__/__snapshots__/ConfirmHcaptcha.test.js.snap
<https://github.com/hCaptcha/react-native-hcaptcha/pull/119/files#diff-9ac995edbfbc7a4c6fd1ec104105413d493631066d5a09603ec705e81cee5f77>
(1)
- *M* __tests__/__snapshots__/Hcaptcha.test.js.snap
<https://github.com/hCaptcha/react-native-hcaptcha/pull/119/files#diff-650d3a2dcb892a6c6d79645a68dc0e0b3e1188b0fa3455c070b5f95f5368138d>
(1)
- *A* __tests__/hcaptchaSmsLink.test.js
<https://github.com/hCaptcha/react-native-hcaptcha/pull/119/files#diff-c845ec5d10ecda03821d91d86c575a9c009040faf49788b8ddba72ce73df8e7e>
(115)
- *A* hcaptchaSmsLink.js
<https://github.com/hCaptcha/react-native-hcaptcha/pull/119/files#diff-8d385f810248d04af77130e7c7c8f168e074fc88e89c7afc3d6242f189220d84>
(175)
Patch Links:
- https://github.com/hCaptcha/react-native-hcaptcha/pull/119.patch
- https://github.com/hCaptcha/react-native-hcaptcha/pull/119.diff
—
Reply to this email directly, view it on GitHub
<#119?email_source=notifications&email_token=B2YLXCU6FLBHVR66WSVG4ML5SKJF5A5CNFSNUABEM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UF42DOMZWGYYDMMBUGOTHEZLBONXW5KTTOVRHGY3SNFRGKZFFMV3GK3TUVRTG633UMVZF6Y3MNFRWW>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/B2YLXCWU3URA3IKR7NADSMT5SKJF5AVCNFSNUABFKJSXA33TNF2G64TZHMZTGMZYGAZDQOJZHNEXG43VMU5TKNZQGIYDKOJUGM42C5QC>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/B2YLXCSMGSS5WPD25US6H4T5SKJF5A5CNFSNUABEM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UF42DOMZWGYYDMMBUGOTHEZLBONXW5KTTOVRHGY3SNFRGKZFFMV3GK3TUVJTG633UMVZF62LPOM>
and Android
<https://github.com/notifications/mobile/android/B2YLXCROYVEYXQCA266MIDD5SKJF5A5CNFSNUABEM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UF42DOMZWGYYDMMBUGOTHEZLBONXW5KTTOVRHGY3SNFRGKZFFMV3GK3TUVZTG633UMVZF6YLOMRZG62LE>.
Download it today!
You are receiving this because you are subscribed to this thread.Message
ID: ***@***.***>
|
| while (start < url.length && url[start] === '/') { | ||
| start++; | ||
| } | ||
| const payload = url.slice(start); |
There was a problem hiding this comment.
Similar as the one in IOS and Android.
Copied and paste from the other PR comment
"I left a similar message to the IOS PR, we strip literal fragments before parsing and preserve %23. So sms:+15551234567#123?body=code changes the outtput recipient to +15551234567123."
| } | ||
|
|
||
| if (candidate.slice(0, 24) === 'https://www.hcaptcha.com') { | ||
| Linking.openURL(candidate); |
There was a problem hiding this comment.
_blank privacy/terms links return through this branch before the fallback catch, leaving Linking.openURL() failures unhandled.
| return true; | ||
| } | ||
|
|
||
| if (candidate.toLowerCase().startsWith('sms:')) { |
There was a problem hiding this comment.
Sorry I am a bit confused with this, if the smsto is to be supported can it use openSmslink() too or no? I was reading it yesterday and the parser accepts it but this branch bypasses it normalization and sms failure reporting.
| it('handles target="_blank" links through onOpenWindow', () => { | ||
| // The challenge opens its sms link with target="_blank", which never reaches | ||
| // onShouldStartLoadWithRequest on Android. | ||
| const openURL = jest.spyOn(Linking, 'openURL').mockResolvedValue(true); |
There was a problem hiding this comment.
How can we verify the body prefill and the returned challenege on mobile for both the IOS and android? I see it checking the outgoing URL but I don't see the messaging app test.
Handles the
sms:link from the MFA inbound-SMS challenge: parses it and opens the messaging appwith the recipient and body prefilled.
Changes
hcaptchaSmsLink.js— parser forsms:/smsto:links. Behaviour ported fromHCaptchaSmsLink.javain hcaptcha-android-sdk#279 (behaviour and test table, not the Java).Splits on
?before;so subscriber params don't eat the body, skips empty query pairs(
?&body=), decodes without form semantics so a literal+survives.Hcaptcha.js— addsonOpenWindowalongsideonShouldStartLoadWithRequest, both goingthrough one handler. The challenge opens the link with
target="_blank", which on Android goesto
onCreateWindowand only surfaces asonOpenWindow. Verified on a Pixel 7: without it no JScallback fires and the URL leaves the app. Same applies to the hCaptcha policy links.
Linking.openURL's rejection message embeds the URL, so it's no longer forwarded throughonMessage— the body carries the one-time code and host apps log that event.Note:
smsto:is not a useful alternative — WhatsApp claimssms:andsmsto:identically, soboth show the same chooser.
Not verified
enterprise sitekey.
onOpenWindowmakes iOS route_blankthere instead of loadingin the WebView; unhandled URLs now open externally.
Test plan
npm test(76 passing) andnpm run lintare green. Both need__e2e__/hostabsent — when thatgenerated dir is present, jest picks up a second React and 32 tests fail on
mastertoo.🤖 Generated with Claude Code