Skip to content

Commit

Permalink
iOS only: Breaking Change: Restrict WebView to only http(s) URLs
Browse files Browse the repository at this point in the history
Summary:
To prevent people from linking file:// or other URLs inside RN WebViews, default <WebView> to not allowing those types of URLs.
This adds the originWhitelist to specify other schemes or domains to be allowed.

If the url is not allowed, it will be opened in Safari/by the OS instead.

Reviewed By: yungsters

Differential Revision: D7833203

fbshipit-source-id: 6881acd3b434d17910240e4edd585c0a10b5df8c
  • Loading branch information
Mehdi Mulani authored and facebook-github-bot committed May 4, 2018
1 parent cd48a61 commit 634e7e1
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 5 deletions.
1 change: 1 addition & 0 deletions IntegrationTests/WebViewTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class WebViewTest extends React.Component {
<WebView
source={source}
onMessage = {processMessage}
originWhitelist={['about:blank']}
/>
);
}
Expand Down
32 changes: 27 additions & 5 deletions Libraries/Components/WebView/WebView.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@

const ActivityIndicator = require('ActivityIndicator');
const EdgeInsetsPropType = require('EdgeInsetsPropType');
const React = require('React');
const Linking = require('Linking');
const PropTypes = require('prop-types');
const React = require('React');
const ReactNative = require('ReactNative');
const ScrollView = require('ScrollView');
const StyleSheet = require('StyleSheet');
const Text = require('Text');
const UIManager = require('UIManager');
const View = require('View');
const ViewPropTypes = require('ViewPropTypes');
const ScrollView = require('ScrollView');
const WebViewShared = require('WebViewShared');

const deprecatedPropType = require('deprecatedPropType');
const invariant = require('fbjs/lib/invariant');
Expand Down Expand Up @@ -353,6 +355,15 @@ class WebView extends React.Component {
*/
mediaPlaybackRequiresUserAction: 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, we will open the URL in Safari.
* 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.
Expand Down Expand Up @@ -398,6 +409,7 @@ class WebView extends React.Component {
};

static defaultProps = {
originWhitelist: WebViewShared.defaultOriginWhitelist,
scalesPageToFit: true,
};

Expand Down Expand Up @@ -446,9 +458,19 @@ class WebView extends React.Component {

const viewManager = nativeConfig.viewManager || RCTWebViewManager;

const onShouldStartLoadWithRequest = this.props.onShouldStartLoadWithRequest && ((event: Event) => {
const shouldStart = this.props.onShouldStartLoadWithRequest &&
this.props.onShouldStartLoadWithRequest(event.nativeEvent);
const compiledWhitelist = (this.props.originWhitelist || []).map(WebViewShared.originWhitelistToRegex);
const onShouldStartLoadWithRequest = ((event: Event) => {
let shouldStart = true;
const {url} = event.nativeEvent;
const origin = WebViewShared.extractOrigin(url);
const passesWhitelist = compiledWhitelist.some(x => new RegExp(x).test(origin));
shouldStart = shouldStart && passesWhitelist;
if (!passesWhitelist) {
Linking.openURL(url);
}
if (this.props.onShouldStartLoadWithRequest) {
shouldStart = shouldStart && this.props.onShouldStartLoadWithRequest(event.nativeEvent);
}
viewManager.startLoadWithResult(!!shouldStart, event.nativeEvent.lockIdentifier);
});

Expand Down
24 changes: 24 additions & 0 deletions Libraries/Components/WebView/WebViewShared.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
'use strict';

const escapeStringRegexp = require('escape-string-regexp');

const WebViewShared = {
defaultOriginWhitelist: ['http://*', 'https://*'],
extractOrigin: (url: string): ?string => {
const result = /^[A-Za-z0-9]+:(\/\/)?[^/]*/.exec(url);
return result === null ? null : result[0];
},
originWhitelistToRegex: (originWhitelist: string): string => {
return escapeStringRegexp(originWhitelist).replace(/\\\*/g, '.*');
},
};

module.exports = WebViewShared;
41 changes: 41 additions & 0 deletions Libraries/Components/WebView/__tests__/WebViewShared-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails oncall+react_native
*/

'use strict';

const WebViewShared = require('WebViewShared');

describe('WebViewShared', () => {
it('extracts the origin correctly', () => {
expect(WebViewShared.extractOrigin('http://facebook.com')).toBe('http://facebook.com');
expect(WebViewShared.extractOrigin('https://facebook.com')).toBe('https://facebook.com');
expect(WebViewShared.extractOrigin('http://facebook.com:8081')).toBe('http://facebook.com:8081');
expect(WebViewShared.extractOrigin('ftp://facebook.com')).toBe('ftp://facebook.com');
expect(WebViewShared.extractOrigin('myweirdscheme://')).toBe('myweirdscheme://');
expect(WebViewShared.extractOrigin('http://facebook.com/')).toBe('http://facebook.com');
expect(WebViewShared.extractOrigin('http://facebook.com/longerurl')).toBe('http://facebook.com');
expect(WebViewShared.extractOrigin('http://facebook.com/http://facebook.com')).toBe('http://facebook.com');
expect(WebViewShared.extractOrigin('http://facebook.com//http://facebook.com')).toBe('http://facebook.com');
expect(WebViewShared.extractOrigin('http://facebook.com//http://facebook.com//')).toBe('http://facebook.com');
expect(WebViewShared.extractOrigin('about:blank')).toBe('about:blank');
});

it('rejects bad urls', () => {
expect(WebViewShared.extractOrigin('a/b')).toBeNull();
expect(WebViewShared.extractOrigin('a//b')).toBeNull();
});

it('creates a whitelist regex correctly', () => {
expect(WebViewShared.originWhitelistToRegex('http://*')).toBe('http://.*');
expect(WebViewShared.originWhitelistToRegex('*')).toBe('.*');
expect(WebViewShared.originWhitelistToRegex('*//test')).toBe('.*//test');
expect(WebViewShared.originWhitelistToRegex('*/*')).toBe('.*/.*');
expect(WebViewShared.originWhitelistToRegex('*.com')).toBe('.*\\.com');
});
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@
"denodeify": "^1.2.1",
"envinfo": "^3.0.0",
"errorhandler": "^1.5.0",
"escape-string-regexp": "^1.0.5",
"eslint-plugin-react-native": "^3.2.1",
"event-target-shim": "^1.0.5",
"fbjs": "^0.8.14",
Expand Down

1 comment on commit 634e7e1

@MichalKrakow
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, what's the policy behind this limitations? What's offered in exchange? This change intend to force users to download assets used by webview at component initiation - what about larger files that can be downloaded in advance or even shipped with application? Can you ellaborate?

Please sign in to comment.