Skip to content

Commit 23f8f7a

Browse files
mdvaccafacebook-github-bot
authored andcommitted
Breaking Change: Restrict WebView to only manage navigation of whitelisted URLs: http(s) by default
Reviewed By: achen1 Differential Revision: D7834050 fbshipit-source-id: 80f7fd3cd20979590b75804819e154afc14a3c64
1 parent 1e68ca7 commit 23f8f7a

File tree

2 files changed

+91
-25
lines changed

2 files changed

+91
-25
lines changed

Libraries/Components/WebView/WebView.android.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const StyleSheet = require('StyleSheet');
1616
const UIManager = require('UIManager');
1717
const View = require('View');
1818
const ViewPropTypes = require('ViewPropTypes');
19+
const WebViewShared = require('WebViewShared');
1920

2021
const deprecatedPropType = require('deprecatedPropType');
2122
const keyMirror = require('fbjs/lib/keyMirror');
@@ -179,6 +180,15 @@ class WebView extends React.Component {
179180
*/
180181
allowUniversalAccessFromFileURLs: PropTypes.bool,
181182

183+
/**
184+
* List of origin strings to allow being navigated to. The strings allow
185+
* wildcards and get matched against *just* the origin (not the full URL).
186+
* If the user taps to navigate to a new page but the new page is not in
187+
* this whitelist, the URL will be oppened by the Android OS.
188+
* The default whitelisted origins are "http://*" and "https://*".
189+
*/
190+
originWhitelist: PropTypes.arrayOf(PropTypes.string),
191+
182192
/**
183193
* Function that accepts a string that will be passed to the WebView and
184194
* executed immediately as JavaScript.
@@ -241,7 +251,8 @@ class WebView extends React.Component {
241251
javaScriptEnabled : true,
242252
thirdPartyCookiesEnabled: true,
243253
scalesPageToFit: true,
244-
saveFormDataDisabled: false
254+
saveFormDataDisabled: false,
255+
originWhitelist: WebViewShared.defaultOriginWhitelist,
245256
};
246257

247258
state = {
@@ -293,6 +304,8 @@ class WebView extends React.Component {
293304

294305
const nativeConfig = this.props.nativeConfig || {};
295306

307+
const originWhitelist = (this.props.originWhitelist || []).map(WebViewShared.originWhitelistToRegex);
308+
296309
let NativeWebView = nativeConfig.component || RCTWebView;
297310

298311
const webView =
@@ -319,6 +332,7 @@ class WebView extends React.Component {
319332
geolocationEnabled={this.props.geolocationEnabled}
320333
mediaPlaybackRequiresUserAction={this.props.mediaPlaybackRequiresUserAction}
321334
allowUniversalAccessFromFileURLs={this.props.allowUniversalAccessFromFileURLs}
335+
originWhitelist={originWhitelist}
322336
mixedContentMode={this.props.mixedContentMode}
323337
saveFormDataDisabled={this.props.saveFormDataDisabled}
324338
urlPrefixesForDefaultIntent={this.props.urlPrefixesForDefaultIntent}

ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77

88
package com.facebook.react.views.webview;
99

10+
import android.annotation.TargetApi;
11+
import android.content.Context;
12+
import java.util.LinkedList;
13+
import java.util.List;
14+
import java.util.regex.Pattern;
1015
import javax.annotation.Nullable;
1116

1217
import java.io.UnsupportedEncodingException;
@@ -110,6 +115,7 @@ protected static class ReactWebViewClient extends WebViewClient {
110115

111116
protected boolean mLastLoadFailed = false;
112117
protected @Nullable ReadableArray mUrlPrefixesForDefaultIntent;
118+
protected @Nullable List<Pattern> mOriginWhitelist;
113119

114120
@Override
115121
public void onPageFinished(WebView webView, String url) {
@@ -137,32 +143,50 @@ public void onPageStarted(WebView webView, String url, Bitmap favicon) {
137143

138144
@Override
139145
public boolean shouldOverrideUrlLoading(WebView view, String url) {
140-
boolean useDefaultIntent = false;
141-
if (mUrlPrefixesForDefaultIntent != null && mUrlPrefixesForDefaultIntent.size() > 0) {
142-
ArrayList<Object> urlPrefixesForDefaultIntent =
143-
mUrlPrefixesForDefaultIntent.toArrayList();
144-
for (Object urlPrefix : urlPrefixesForDefaultIntent) {
145-
if (url.startsWith((String) urlPrefix)) {
146-
useDefaultIntent = true;
147-
break;
148-
}
146+
if (url.equals(BLANK_URL)) return false;
147+
148+
// url blacklisting
149+
if (mUrlPrefixesForDefaultIntent != null && mUrlPrefixesForDefaultIntent.size() > 0) {
150+
ArrayList<Object> urlPrefixesForDefaultIntent =
151+
mUrlPrefixesForDefaultIntent.toArrayList();
152+
for (Object urlPrefix : urlPrefixesForDefaultIntent) {
153+
if (url.startsWith((String) urlPrefix)) {
154+
launchIntent(view.getContext(), url);
155+
return true;
149156
}
150157
}
158+
}
151159

152-
if (!useDefaultIntent &&
153-
(url.startsWith("http://") || url.startsWith("https://") ||
154-
url.startsWith("file://") || url.equals("about:blank"))) {
155-
return false;
156-
} else {
157-
try {
158-
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
159-
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
160-
view.getContext().startActivity(intent);
161-
} catch (ActivityNotFoundException e) {
162-
FLog.w(ReactConstants.TAG, "activity not found to handle uri scheme for: " + url, e);
163-
}
160+
if (mOriginWhitelist != null && shouldHandleURL(mOriginWhitelist, url)) {
161+
return false;
162+
}
163+
164+
launchIntent(view.getContext(), url);
165+
return true;
166+
}
167+
168+
private void launchIntent(Context context, String url) {
169+
try {
170+
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
171+
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
172+
intent.addCategory(Intent.CATEGORY_BROWSABLE);
173+
context.startActivity(intent);
174+
} catch (ActivityNotFoundException e) {
175+
FLog.w(ReactConstants.TAG, "activity not found to handle uri scheme for: " + url, e);
176+
}
177+
}
178+
179+
private boolean shouldHandleURL(List<Pattern> originWhitelist, String url) {
180+
Uri uri = Uri.parse(url);
181+
String scheme = uri.getScheme() != null ? uri.getScheme() : "";
182+
String authority = uri.getAuthority() != null ? uri.getAuthority() : "";
183+
String urlToCheck = scheme + "://" + authority;
184+
for (Pattern pattern : originWhitelist) {
185+
if (pattern.matcher(urlToCheck).matches()) {
164186
return true;
165187
}
188+
}
189+
return false;
166190
}
167191

168192
@Override
@@ -211,6 +235,10 @@ protected WritableMap createWebViewEvent(WebView webView, String url) {
211235
public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) {
212236
mUrlPrefixesForDefaultIntent = specialUrls;
213237
}
238+
239+
public void setOriginWhitelist(List<Pattern> originWhitelist) {
240+
mOriginWhitelist = originWhitelist;
241+
}
214242
}
215243

216244
/**
@@ -356,6 +384,7 @@ protected ReactWebView createReactWebViewInstance(ThemedReactContext reactContex
356384
}
357385

358386
@Override
387+
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
359388
protected WebView createViewInstance(ThemedReactContext reactContext) {
360389
ReactWebView webView = createReactWebViewInstance(reactContext);
361390
webView.setWebChromeClient(new WebChromeClient() {
@@ -375,9 +404,18 @@ public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermiss
375404
});
376405
reactContext.addLifecycleEventListener(webView);
377406
mWebViewConfig.configWebView(webView);
378-
webView.getSettings().setBuiltInZoomControls(true);
379-
webView.getSettings().setDisplayZoomControls(false);
380-
webView.getSettings().setDomStorageEnabled(true);
407+
WebSettings settings = webView.getSettings();
408+
settings.setBuiltInZoomControls(true);
409+
settings.setDisplayZoomControls(false);
410+
settings.setDomStorageEnabled(true);
411+
412+
settings.setAllowFileAccess(false);
413+
settings.setAllowContentAccess(false);
414+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
415+
settings.setAllowFileAccessFromFileURLs(false);
416+
setAllowUniversalAccessFromFileURLs(webView, false);
417+
}
418+
setMixedContentMode(webView, "never");
381419

382420
// Fixes broken full-screen modals/galleries due to body height being 0.
383421
webView.setLayoutParams(
@@ -546,6 +584,20 @@ public void setGeolocationEnabled(
546584
view.getSettings().setGeolocationEnabled(isGeolocationEnabled != null && isGeolocationEnabled);
547585
}
548586

587+
@ReactProp(name = "originWhitelist")
588+
public void setOriginWhitelist(
589+
WebView view,
590+
@Nullable ReadableArray originWhitelist) {
591+
ReactWebViewClient client = ((ReactWebView) view).getReactWebViewClient();
592+
if (client != null && originWhitelist != null) {
593+
List<Pattern> whiteList = new LinkedList<>();
594+
for (int i = 0 ; i < originWhitelist.size() ; i++) {
595+
whiteList.add(Pattern.compile(originWhitelist.getString(i)));
596+
}
597+
client.setOriginWhitelist(whiteList);
598+
}
599+
}
600+
549601
@Override
550602
protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
551603
// Do not register default touch emitter and let WebView implementation handle touches

0 commit comments

Comments
 (0)