Permalink
Browse files

Breaking Change: Restrict WebView to only manage navigation of whitel…

…isted URLs: http(s) by default

Reviewed By: achen1

Differential Revision: D7834050

fbshipit-source-id: 80f7fd3cd20979590b75804819e154afc14a3c64
  • Loading branch information...
mdvacca authored and facebook-github-bot committed May 5, 2018
1 parent 1e68ca7 commit 23f8f7aecb1f21f4f5e44fb9e4a7456ea97935c9
@@ -16,6 +16,7 @@ const StyleSheet = require('StyleSheet');
const UIManager = require('UIManager');
const View = require('View');
const ViewPropTypes = require('ViewPropTypes');
const WebViewShared = require('WebViewShared');
const deprecatedPropType = require('deprecatedPropType');
const keyMirror = require('fbjs/lib/keyMirror');
@@ -179,6 +180,15 @@ class WebView extends React.Component {
*/
allowUniversalAccessFromFileURLs: PropTypes.bool,
/**
* List of origin strings to allow being navigated to. The strings allow
* wildcards and get matched against *just* the origin (not the full URL).
* If the user taps to navigate to a new page but the new page is not in
* this whitelist, the URL will be oppened by the Android OS.
* The default whitelisted origins are "http://*" and "https://*".
*/
originWhitelist: PropTypes.arrayOf(PropTypes.string),
/**
* Function that accepts a string that will be passed to the WebView and
* executed immediately as JavaScript.
@@ -241,7 +251,8 @@ class WebView extends React.Component {
javaScriptEnabled : true,
thirdPartyCookiesEnabled: true,
scalesPageToFit: true,
saveFormDataDisabled: false
saveFormDataDisabled: false,
originWhitelist: WebViewShared.defaultOriginWhitelist,
};
state = {
@@ -293,6 +304,8 @@ class WebView extends React.Component {
const nativeConfig = this.props.nativeConfig || {};
const originWhitelist = (this.props.originWhitelist || []).map(WebViewShared.originWhitelistToRegex);
let NativeWebView = nativeConfig.component || RCTWebView;
const webView =
@@ -319,6 +332,7 @@ class WebView extends React.Component {
geolocationEnabled={this.props.geolocationEnabled}
mediaPlaybackRequiresUserAction={this.props.mediaPlaybackRequiresUserAction}
allowUniversalAccessFromFileURLs={this.props.allowUniversalAccessFromFileURLs}
originWhitelist={originWhitelist}
mixedContentMode={this.props.mixedContentMode}
saveFormDataDisabled={this.props.saveFormDataDisabled}
urlPrefixesForDefaultIntent={this.props.urlPrefixesForDefaultIntent}
@@ -7,6 +7,11 @@
package com.facebook.react.views.webview;
import android.annotation.TargetApi;
import android.content.Context;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import java.io.UnsupportedEncodingException;
@@ -110,6 +115,7 @@
protected boolean mLastLoadFailed = false;
protected @Nullable ReadableArray mUrlPrefixesForDefaultIntent;
protected @Nullable List<Pattern> mOriginWhitelist;
@Override
public void onPageFinished(WebView webView, String url) {
@@ -137,32 +143,50 @@ public void onPageStarted(WebView webView, String url, Bitmap favicon) {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
boolean useDefaultIntent = false;
if (mUrlPrefixesForDefaultIntent != null && mUrlPrefixesForDefaultIntent.size() > 0) {
ArrayList<Object> urlPrefixesForDefaultIntent =
mUrlPrefixesForDefaultIntent.toArrayList();
for (Object urlPrefix : urlPrefixesForDefaultIntent) {
if (url.startsWith((String) urlPrefix)) {
useDefaultIntent = true;
break;
}
if (url.equals(BLANK_URL)) return false;
// url blacklisting
if (mUrlPrefixesForDefaultIntent != null && mUrlPrefixesForDefaultIntent.size() > 0) {
ArrayList<Object> urlPrefixesForDefaultIntent =
mUrlPrefixesForDefaultIntent.toArrayList();
for (Object urlPrefix : urlPrefixesForDefaultIntent) {
if (url.startsWith((String) urlPrefix)) {
launchIntent(view.getContext(), url);
return true;
}
}
}
if (!useDefaultIntent &&
(url.startsWith("http://") || url.startsWith("https://") ||
url.startsWith("file://") || url.equals("about:blank"))) {
return false;
} else {
try {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
view.getContext().startActivity(intent);
} catch (ActivityNotFoundException e) {
FLog.w(ReactConstants.TAG, "activity not found to handle uri scheme for: " + url, e);
}
if (mOriginWhitelist != null && shouldHandleURL(mOriginWhitelist, url)) {
return false;
}
launchIntent(view.getContext(), url);
return true;
}
private void launchIntent(Context context, String url) {
try {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
FLog.w(ReactConstants.TAG, "activity not found to handle uri scheme for: " + url, e);
}
}
private boolean shouldHandleURL(List<Pattern> originWhitelist, String url) {
Uri uri = Uri.parse(url);
String scheme = uri.getScheme() != null ? uri.getScheme() : "";
String authority = uri.getAuthority() != null ? uri.getAuthority() : "";
String urlToCheck = scheme + "://" + authority;
for (Pattern pattern : originWhitelist) {
if (pattern.matcher(urlToCheck).matches()) {
return true;
}
}
return false;
}
@Override
@@ -211,6 +235,10 @@ protected WritableMap createWebViewEvent(WebView webView, String url) {
public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) {
mUrlPrefixesForDefaultIntent = specialUrls;
}
public void setOriginWhitelist(List<Pattern> originWhitelist) {
mOriginWhitelist = originWhitelist;
}
}
/**
@@ -356,6 +384,7 @@ protected ReactWebView createReactWebViewInstance(ThemedReactContext reactContex
}
@Override
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
protected WebView createViewInstance(ThemedReactContext reactContext) {
ReactWebView webView = createReactWebViewInstance(reactContext);
webView.setWebChromeClient(new WebChromeClient() {
@@ -375,9 +404,18 @@ public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermiss
});
reactContext.addLifecycleEventListener(webView);
mWebViewConfig.configWebView(webView);
webView.getSettings().setBuiltInZoomControls(true);
webView.getSettings().setDisplayZoomControls(false);
webView.getSettings().setDomStorageEnabled(true);
WebSettings settings = webView.getSettings();
settings.setBuiltInZoomControls(true);
settings.setDisplayZoomControls(false);
settings.setDomStorageEnabled(true);
settings.setAllowFileAccess(false);
settings.setAllowContentAccess(false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
settings.setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(webView, false);
}
setMixedContentMode(webView, "never");
// Fixes broken full-screen modals/galleries due to body height being 0.
webView.setLayoutParams(
@@ -546,6 +584,20 @@ public void setGeolocationEnabled(
view.getSettings().setGeolocationEnabled(isGeolocationEnabled != null && isGeolocationEnabled);
}
@ReactProp(name = "originWhitelist")
public void setOriginWhitelist(
WebView view,
@Nullable ReadableArray originWhitelist) {
ReactWebViewClient client = ((ReactWebView) view).getReactWebViewClient();
if (client != null && originWhitelist != null) {
List<Pattern> whiteList = new LinkedList<>();
for (int i = 0 ; i < originWhitelist.size() ; i++) {
whiteList.add(Pattern.compile(originWhitelist.getString(i)));
}
client.setOriginWhitelist(whiteList);
}
}
@Override
protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
// Do not register default touch emitter and let WebView implementation handle touches

0 comments on commit 23f8f7a

Please sign in to comment.