@exodus/react-native-webview 13.16.0-exodus.5 → 13.16.0-exodus.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,5 @@
1
1
  package com.reactnativecommunity.webview;
2
2
 
3
- import android.annotation.SuppressLint;
4
3
  import android.graphics.Rect;
5
4
  import android.net.Uri;
6
5
  import android.text.TextUtils;
@@ -9,7 +8,6 @@ import android.view.Menu;
9
8
  import android.view.MenuItem;
10
9
  import android.view.MotionEvent;
11
10
  import android.view.View;
12
- import android.webkit.JavascriptInterface;
13
11
  import android.webkit.ValueCallback;
14
12
  import android.webkit.WebChromeClient;
15
13
  import android.webkit.WebView;
@@ -55,8 +53,6 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
55
53
  String injectedJSBeforeContentLoaded;
56
54
  protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebView";
57
55
  protected @Nullable
58
- RNCWebViewBridge fallbackBridge;
59
- protected @Nullable
60
56
  WebViewCompat.WebMessageListener bridgeListener = null;
61
57
 
62
58
  protected boolean messagingEnabled = false;
@@ -246,6 +242,15 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
246
242
  this.bridgeListener = new WebViewCompat.WebMessageListener() {
247
243
  @Override
248
244
  public void onPostMessage(@NonNull WebView view, @NonNull WebMessageCompat message, @NonNull Uri sourceOrigin, boolean isMainFrame, @NonNull JavaScriptReplyProxy replyProxy) {
245
+ // Exodus: only accept messages from the top frame. The injected
246
+ // ReactNativeWebView object is available in every frame (subframes
247
+ // included), so without this guard any embedded iframe — even a
248
+ // cross-origin one that happens to pass the origin whitelist — could
249
+ // reach the native onMessage handler. This mirrors the iOS bridge,
250
+ // which is injected with forMainFrameOnly:YES.
251
+ if (!isMainFrame) {
252
+ return;
253
+ }
249
254
  RNCWebView.this.onMessage(message.getData(), sourceOrigin.toString());
250
255
  }
251
256
  };
@@ -257,10 +262,14 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
257
262
  );
258
263
  }
259
264
  } else {
260
- if (fallbackBridge == null) {
261
- fallbackBridge = new RNCWebViewBridge(webView);
262
- addJavascriptInterface(fallbackBridge, JAVASCRIPT_INTERFACE);
263
- }
265
+ // Exodus: the legacy addJavascriptInterface bridge injects ReactNativeWebView
266
+ // into every frame and gives the native side no way to tell which frame a
267
+ // message came from, so it cannot enforce the top-frame restriction above.
268
+ // WEB_MESSAGE_LISTENER is supported on every WebView new enough to clear
269
+ // hardMinimumChromeVersion (100, see src/WebView.android.tsx), so this branch
270
+ // is unreachable in practice. Fail closed rather than install an unhardenable
271
+ // bridge.
272
+ FLog.w("RNCWebView", "WEB_MESSAGE_LISTENER is unsupported on this WebView; ReactNativeWebView messaging bridge not installed.");
264
273
  }
265
274
  injectJavascriptObject();
266
275
  }
@@ -275,7 +284,6 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
275
284
  }
276
285
  }
277
286
 
278
- @SuppressLint("AddJavascriptInterface")
279
287
  public void setMessagingEnabled(boolean enabled) {
280
288
  if (messagingEnabled == enabled) {
281
289
  return;
@@ -423,30 +431,6 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
423
431
  return this.getThemedReactContext().getReactApplicationContext();
424
432
  }
425
433
 
426
- protected class RNCWebViewBridge {
427
- private String TAG = "RNCWebViewBridge";
428
- RNCWebView mWebView;
429
-
430
- RNCWebViewBridge(RNCWebView c) {
431
- mWebView = c;
432
- }
433
-
434
- /**
435
- * This method is called whenever JavaScript running within the web view calls:
436
- * - window[JAVASCRIPT_INTERFACE].postMessage
437
- */
438
- @JavascriptInterface
439
- public void postMessage(String message) {
440
- if (mWebView.getMessagingEnabled()) {
441
- // Post to main thread because `mWebView.getUrl()` requires to be executed on main.
442
- mWebView.post(() -> mWebView.onMessage(message, mWebView.getUrl()));
443
- } else {
444
- FLog.w(TAG, "ReactNativeWebView.postMessage method was called but messaging is disabled. Pass an onMessage handler to the WebView.");
445
- }
446
- }
447
- }
448
-
449
-
450
434
  protected static class ProgressChangedFilter {
451
435
  private boolean waitingForCommandLoadUrl = false;
452
436
 
@@ -3,11 +3,15 @@
3
3
  /**
4
4
  * Exodus: Thread-safe singleton that manages navigation decision handlers.
5
5
  *
6
+ * All public methods use @synchronized for thread safety, and decision
7
+ * handlers are invoked outside the lock to avoid deadlocks
8
+ * (upstream react-native-webview#3916).
9
+ *
6
10
  * Security improvements over upstream:
7
11
  * - Uses NSInteger (64-bit) instead of int to prevent overflow
8
12
  * - Adds collision checking to skip identifiers still in use
9
- * - All public methods use @synchronized for thread safety
10
13
  * - Explicitly copies blocks to heap to prevent use-after-free
14
+ * - Denies navigation by default if JS does not respond within 500ms
11
15
  * - Provides cancelDecisionForLockIdentifier: for cleanup on WebView dealloc
12
16
  */
13
17
  @implementation RNCWebViewDecisionManager
@@ -40,14 +44,17 @@
40
44
  // if JS responded in time.
41
45
  NSInteger capturedIdentifier = lockIdentifier;
42
46
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(500 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
47
+ DecisionBlock pendingHandler;
43
48
  @synchronized (self) {
44
- DecisionBlock pendingHandler = [self.decisionHandlers objectForKey:@(capturedIdentifier)];
45
- if (pendingHandler != nil) {
46
- RCTLogWarn(@"Navigation decision timeout for lock %ld, denying by default", (long)capturedIdentifier);
47
- pendingHandler(NO);
48
- [self.decisionHandlers removeObjectForKey:@(capturedIdentifier)];
49
+ pendingHandler = [self.decisionHandlers objectForKey:@(capturedIdentifier)];
50
+ if (pendingHandler == nil) {
51
+ return;
49
52
  }
53
+ [self.decisionHandlers removeObjectForKey:@(capturedIdentifier)];
50
54
  }
55
+ // Invoke outside the lock, as in setResult:forLockIdentifier:.
56
+ RCTLogWarn(@"Navigation decision timeout for lock %ld, denying by default", (long)capturedIdentifier);
57
+ pendingHandler(NO);
51
58
  });
52
59
 
53
60
  return lockIdentifier;
@@ -55,15 +62,20 @@
55
62
  }
56
63
 
57
64
  - (void)setResult:(BOOL)shouldStart forLockIdentifier:(NSInteger)lockIdentifier {
65
+ // The handler is captured and removed under the lock, then invoked OUTSIDE
66
+ // it (upstream react-native-webview#3916). The handler hops to the main
67
+ // queue and can trigger another navigation that re-enters this class, so
68
+ // holding the lock across its invocation risks deadlock.
69
+ DecisionBlock handler;
58
70
  @synchronized (self) {
59
- DecisionBlock handler = [self.decisionHandlers objectForKey:@(lockIdentifier)];
71
+ handler = [self.decisionHandlers objectForKey:@(lockIdentifier)];
60
72
  if (handler == nil) {
61
73
  RCTLogWarn(@"Lock not found for identifier: %ld", (long)lockIdentifier);
62
74
  return;
63
75
  }
64
- handler(shouldStart);
65
76
  [self.decisionHandlers removeObjectForKey:@(lockIdentifier)];
66
77
  }
78
+ handler(shouldStart);
67
79
  }
68
80
 
69
81
 
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "Thibault Malbranche <malbranche.thibault@gmail.com>"
11
11
  ],
12
12
  "license": "MIT",
13
- "version": "13.16.0-exodus.5",
13
+ "version": "13.16.0-exodus.7",
14
14
  "homepage": "https://github.com/ExodusMovement/react-native-webview#readme",
15
15
  "scripts": {
16
16
  "android": "react-native run-android",