iOS only: Breaking Change: Restrict WebView to only http(s) URLs
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
This commit is contained in:
parent
cd48a6130b
commit
634e7e11e3
|
@ -48,6 +48,7 @@ class WebViewTest extends React.Component {
|
||||||
<WebView
|
<WebView
|
||||||
source={source}
|
source={source}
|
||||||
onMessage = {processMessage}
|
onMessage = {processMessage}
|
||||||
|
originWhitelist={['about:blank']}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,15 +10,17 @@
|
||||||
|
|
||||||
const ActivityIndicator = require('ActivityIndicator');
|
const ActivityIndicator = require('ActivityIndicator');
|
||||||
const EdgeInsetsPropType = require('EdgeInsetsPropType');
|
const EdgeInsetsPropType = require('EdgeInsetsPropType');
|
||||||
const React = require('React');
|
const Linking = require('Linking');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
|
const React = require('React');
|
||||||
const ReactNative = require('ReactNative');
|
const ReactNative = require('ReactNative');
|
||||||
|
const ScrollView = require('ScrollView');
|
||||||
const StyleSheet = require('StyleSheet');
|
const StyleSheet = require('StyleSheet');
|
||||||
const Text = require('Text');
|
const Text = require('Text');
|
||||||
const UIManager = require('UIManager');
|
const UIManager = require('UIManager');
|
||||||
const View = require('View');
|
const View = require('View');
|
||||||
const ViewPropTypes = require('ViewPropTypes');
|
const ViewPropTypes = require('ViewPropTypes');
|
||||||
const ScrollView = require('ScrollView');
|
const WebViewShared = require('WebViewShared');
|
||||||
|
|
||||||
const deprecatedPropType = require('deprecatedPropType');
|
const deprecatedPropType = require('deprecatedPropType');
|
||||||
const invariant = require('fbjs/lib/invariant');
|
const invariant = require('fbjs/lib/invariant');
|
||||||
|
@ -353,6 +355,15 @@ class WebView extends React.Component {
|
||||||
*/
|
*/
|
||||||
mediaPlaybackRequiresUserAction: PropTypes.bool,
|
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
|
* Function that accepts a string that will be passed to the WebView and
|
||||||
* executed immediately as JavaScript.
|
* executed immediately as JavaScript.
|
||||||
|
@ -398,6 +409,7 @@ class WebView extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
originWhitelist: WebViewShared.defaultOriginWhitelist,
|
||||||
scalesPageToFit: true,
|
scalesPageToFit: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -446,9 +458,19 @@ class WebView extends React.Component {
|
||||||
|
|
||||||
const viewManager = nativeConfig.viewManager || RCTWebViewManager;
|
const viewManager = nativeConfig.viewManager || RCTWebViewManager;
|
||||||
|
|
||||||
const onShouldStartLoadWithRequest = this.props.onShouldStartLoadWithRequest && ((event: Event) => {
|
const compiledWhitelist = (this.props.originWhitelist || []).map(WebViewShared.originWhitelistToRegex);
|
||||||
const shouldStart = this.props.onShouldStartLoadWithRequest &&
|
const onShouldStartLoadWithRequest = ((event: Event) => {
|
||||||
this.props.onShouldStartLoadWithRequest(event.nativeEvent);
|
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);
|
viewManager.startLoadWithResult(!!shouldStart, event.nativeEvent.lockIdentifier);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -158,6 +158,7 @@
|
||||||
"denodeify": "^1.2.1",
|
"denodeify": "^1.2.1",
|
||||||
"envinfo": "^3.0.0",
|
"envinfo": "^3.0.0",
|
||||||
"errorhandler": "^1.5.0",
|
"errorhandler": "^1.5.0",
|
||||||
|
"escape-string-regexp": "^1.0.5",
|
||||||
"eslint-plugin-react-native": "^3.2.1",
|
"eslint-plugin-react-native": "^3.2.1",
|
||||||
"event-target-shim": "^1.0.5",
|
"event-target-shim": "^1.0.5",
|
||||||
"fbjs": "^0.8.14",
|
"fbjs": "^0.8.14",
|
||||||
|
|
Loading…
Reference in New Issue