From 8f5ce30aa7b13de8b4285fece4aa3d7d0bf2655f Mon Sep 17 00:00:00 2001 From: Kuba Adamczyk Date: Thu, 22 Jan 2026 16:03:30 +0100 Subject: [PATCH 1/4] allow passing active:boolean native prop to webview that disables possiblity of showing alerts if a webview is inactive --- .../webview/RNCWebChromeClient.java | 32 +++++++++++++++++++ .../webview/RNCWebView.java | 9 ++++++ .../webview/RNCWebViewManagerImpl.kt | 4 +++ .../webview/RNCWebViewManager.java | 6 ++++ .../webview/RNCWebViewManager.java | 5 +++ apple/RNCWebViewImpl.h | 1 + apple/RNCWebViewImpl.m | 16 ++++++++++ apple/RNCWebViewManager.mm | 3 ++ src/RNCWebViewNativeComponent.ts | 1 + src/WebViewTypes.ts | 11 +++++++ 10 files changed, 88 insertions(+) diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java index bd2efa5c41..3df50f1b18 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java @@ -16,6 +16,8 @@ import android.view.ViewGroup; import android.webkit.ConsoleMessage; import android.webkit.GeolocationPermissions; +import android.webkit.JsPromptResult; +import android.webkit.JsResult; import android.webkit.PermissionRequest; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; @@ -479,4 +481,34 @@ public void setAllowsProtectedMedia(boolean enabled) { public void setHasOnOpenWindowEvent(boolean hasEvent) { mHasOnOpenWindowEvent = hasEvent; } + + /** + * Security: Suppress dialogs from non-active tabs to prevent cross-tab spoofing + */ + @Override + public boolean onJsAlert(WebView view, String url, String message, JsResult result) { + if (!mWebView.isActive()) { + result.cancel(); + return true; // Consumed - don't show default dialog + } + return false; // Show default system dialog + } + + @Override + public boolean onJsConfirm(WebView view, String url, String message, JsResult result) { + if (!mWebView.isActive()) { + result.cancel(); + return true; + } + return false; + } + + @Override + public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { + if (!mWebView.isActive()) { + result.cancel(); + return true; + } + return false; + } } \ No newline at end of file diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java index 486c77a060..7a2cc3c663 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java @@ -78,6 +78,7 @@ public class RNCWebView extends WebView implements LifecycleEventListener { protected boolean hasScrollEvent = false; protected boolean nestedScrollEnabled = false; protected ProgressChangedFilter progressChangedFilter; + protected boolean mActive = true; /** * WebView must be created with an context of the current activity @@ -107,6 +108,14 @@ public void setNestedScrollEnabled(boolean nestedScrollEnabled) { this.nestedScrollEnabled = nestedScrollEnabled; } + public void setActive(boolean active) { + this.mActive = active; + } + + public boolean isActive() { + return this.mActive; + } + @Override public void onHostResume() { // do nothing diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt index 3e18463562..ebfd70763c 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt @@ -681,4 +681,8 @@ class RNCWebViewManagerImpl(private val newArch: Boolean = false) { WebSettingsCompat.setPaymentRequestEnabled(view.settings, enabled) } } + + fun setActive(viewWrapper: RNCWebViewWrapper, value: Boolean) { + viewWrapper.webView.setActive(value) + } } diff --git a/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java index 4709e28224..53c40af53f 100644 --- a/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -430,6 +430,12 @@ public void setMediaCapturePermissionGrantType(RNCWebViewWrapper view, @Nullable public void setFraudulentWebsiteWarningEnabled(RNCWebViewWrapper view, boolean value) {} /* !iOS PROPS - no implemented here */ + @Override + @ReactProp(name = "active") + public void setActive(RNCWebViewWrapper view, boolean value) { + mRNCWebViewManagerImpl.setActive(view, value); + } + @Override @ReactProp(name = "userAgent") public void setUserAgent(RNCWebViewWrapper view, @Nullable String value) { diff --git a/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java index 974337f41c..27b7a03d3a 100644 --- a/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -279,6 +279,11 @@ public void setPaymentRequestEnabled(RNCWebViewWrapper view, boolean value) { mRNCWebViewManagerImpl.setPaymentRequestEnabled(view, value); } + @ReactProp(name = "active") + public void setActive(RNCWebViewWrapper view, boolean value) { + mRNCWebViewManagerImpl.setActive(view, value); + } + @Override protected void addEventEmitters(@NonNull ThemedReactContext reactContext, RNCWebViewWrapper viewWrapper) { // Do not register default touch emitter and let WebView implementation handle touches diff --git a/apple/RNCWebViewImpl.h b/apple/RNCWebViewImpl.h index 1f6bbfd697..01198d5b12 100644 --- a/apple/RNCWebViewImpl.h +++ b/apple/RNCWebViewImpl.h @@ -113,6 +113,7 @@ shouldStartLoadForRequest:(NSMutableDictionary *)request @property (nonatomic, copy) NSArray * _Nullable menuItems; @property (nonatomic, copy) NSArray * _Nullable suppressMenuItems; @property (nonatomic, copy) RCTDirectEventBlock onCustomMenuSelection; +@property (nonatomic, assign) BOOL active; #if !TARGET_OS_OSX @property (nonatomic, assign) WKDataDetectorTypes dataDetectorTypes; @property (nonatomic, weak) UIRefreshControl * _Nullable refreshControl; diff --git a/apple/RNCWebViewImpl.m b/apple/RNCWebViewImpl.m index 7733e1b149..5fbe05b7ba 100644 --- a/apple/RNCWebViewImpl.m +++ b/apple/RNCWebViewImpl.m @@ -176,6 +176,7 @@ - (instancetype)initWithFrame:(CGRect)frame _autoManageStatusBarEnabled = YES; _contentInset = UIEdgeInsetsZero; _savedKeyboardDisplayRequiresUserAction = YES; + _active = YES; _injectedJavaScript = nil; _injectedJavaScriptForMainFrameOnly = YES; _injectedJavaScriptBeforeContentLoaded = nil; @@ -1203,6 +1204,11 @@ - (void) webView:(WKWebView *)webView */ - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler { + // Security: Suppress dialogs from non-active tabs to prevent cross-tab spoofing + if (!_active) { + completionHandler(); + return; + } #if !TARGET_OS_OSX UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *action) { @@ -1222,6 +1228,11 @@ - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSStrin * confirm */ - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{ + // Security: Suppress dialogs from non-active tabs to prevent cross-tab spoofing + if (!_active) { + completionHandler(NO); + return; + } #if !TARGET_OS_OSX UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *action) { @@ -1247,6 +1258,11 @@ - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSStr * prompt */ - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler{ + // Security: Suppress dialogs from non-active tabs to prevent cross-tab spoofing + if (!_active) { + completionHandler(nil); + return; + } #if !TARGET_OS_OSX UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:prompt preferredStyle:UIAlertControllerStyleAlert]; [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) { diff --git a/apple/RNCWebViewManager.mm b/apple/RNCWebViewManager.mm index f8f375f23b..0ae8e24f54 100644 --- a/apple/RNCWebViewManager.mm +++ b/apple/RNCWebViewManager.mm @@ -125,6 +125,9 @@ - (RNCView *)view RCT_CUSTOM_VIEW_PROPERTY(pullToRefreshEnabled, BOOL, RNCWebViewImpl) { view.pullToRefreshEnabled = json == nil ? false : [RCTConvert BOOL: json]; } +RCT_CUSTOM_VIEW_PROPERTY(active, BOOL, RNCWebViewImpl) { + view.active = json == nil ? YES : [RCTConvert BOOL: json]; +} RCT_CUSTOM_VIEW_PROPERTY(refreshControlLightMode, BOOL, RNCWebViewImpl) { view.refreshControlLightMode = json == nil ? false : [RCTConvert BOOL: json]; } diff --git a/src/RNCWebViewNativeComponent.ts b/src/RNCWebViewNativeComponent.ts index cb9a9ceffd..ff9ee501ff 100644 --- a/src/RNCWebViewNativeComponent.ts +++ b/src/RNCWebViewNativeComponent.ts @@ -296,6 +296,7 @@ export interface NativeProps extends ViewProps { userAgent?: string; injectedJavaScriptObject?: string; paymentRequestEnabled?: boolean; + active?: WithDefault; } export interface NativeCommands { diff --git a/src/WebViewTypes.ts b/src/WebViewTypes.ts index c5cf7ff749..d0dd0da267 100644 --- a/src/WebViewTypes.ts +++ b/src/WebViewTypes.ts @@ -1358,4 +1358,15 @@ export interface WebViewSharedProps extends ViewProps { * Enables support for the Payment Request API for the WebView */ paymentRequestEnabled?: boolean; + + /** + * When used in a multi-tab browser context, indicates whether this WebView + * is the currently active/visible tab. When false, JavaScript dialogs + * (alert, prompt, confirm) will be automatically dismissed to prevent + * cross-tab spoofing attacks. + * @platform android + * @platform ios + * @default true + */ + active?: boolean; } From 5b5cb5b8b66265bbe2fb7b42a4af5f8c6504b0e1 Mon Sep 17 00:00:00 2001 From: Kuba Adamczyk Date: Thu, 22 Jan 2026 16:09:03 +0100 Subject: [PATCH 2/4] style --- .../reactnativecommunity/webview/RNCWebChromeClient.java | 4 ++-- src/WebViewTypes.ts | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java index 3df50f1b18..755da0a104 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java @@ -483,7 +483,7 @@ public void setHasOnOpenWindowEvent(boolean hasEvent) { } /** - * Security: Suppress dialogs from non-active tabs to prevent cross-tab spoofing + * Security: Prevent dialogs from being presented when webiew is inactive. */ @Override public boolean onJsAlert(WebView view, String url, String message, JsResult result) { @@ -511,4 +511,4 @@ public boolean onJsPrompt(WebView view, String url, String message, String defau } return false; } -} \ No newline at end of file +} diff --git a/src/WebViewTypes.ts b/src/WebViewTypes.ts index d0dd0da267..1e2dadd0d0 100644 --- a/src/WebViewTypes.ts +++ b/src/WebViewTypes.ts @@ -1360,10 +1360,8 @@ export interface WebViewSharedProps extends ViewProps { paymentRequestEnabled?: boolean; /** - * When used in a multi-tab browser context, indicates whether this WebView - * is the currently active/visible tab. When false, JavaScript dialogs - * (alert, prompt, confirm) will be automatically dismissed to prevent - * cross-tab spoofing attacks. + * Allows marking the WebView as active or inactive. + * When a WebView is marked inactive, JS dialogs and alerts will be blocked and won't be shown to the user * @platform android * @platform ios * @default true From 176afdb160f38ebe75238e6074eb4524bbe04a25 Mon Sep 17 00:00:00 2001 From: Kuba Adamczyk Date: Thu, 22 Jan 2026 16:11:33 +0100 Subject: [PATCH 3/4] changeset --- .changeset/huge-onions-lie.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/huge-onions-lie.md diff --git a/.changeset/huge-onions-lie.md b/.changeset/huge-onions-lie.md new file mode 100644 index 0000000000..7ae9cdbadc --- /dev/null +++ b/.changeset/huge-onions-lie.md @@ -0,0 +1,5 @@ +--- +'@phantom/react-native-webview': minor +--- + +Added a new prop that allows marking webviews as active or inactive. From eaabb30d76b7e2a07ab0b471a82bcc4484521600 Mon Sep 17 00:00:00 2001 From: Kuba Adamczyk Date: Thu, 22 Jan 2026 16:14:03 +0100 Subject: [PATCH 4/4] typo --- .../com/reactnativecommunity/webview/RNCWebChromeClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java index 755da0a104..7c0131811c 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java @@ -483,7 +483,7 @@ public void setHasOnOpenWindowEvent(boolean hasEvent) { } /** - * Security: Prevent dialogs from being presented when webiew is inactive. + * Security: Prevent dialogs from being presented when webview is inactive. */ @Override public boolean onJsAlert(WebView view, String url, String message, JsResult result) {