Skip to content

Commit 0442126

Browse files
aarondailsafaiyeh
andcommitted
fix(whitelisted origins): Prevent handling of un-whitelisted URLs
* Preventing an unhandled promise rejection when: a URL is loaded by the WebView, but the URL isn't in the origin whitelist, so it is handed off to the OS to handle by calling Linking.openURL(), but Linking.openURL has an error. The code wasn't catching the error, so this would result in an unhandled promise rejection. Now the error is being caught. * Fixing a problem where a URL is handled to the OS to deal with, via Linking.openURL, and also loaded in the WebView by making those cases mutually exclusive (they weren't previously). In more detail: when a URL is loaded by the WebView that isn't in the origin whitelist it is handled off to the OS to handle by calling Linking.openURL. But, if the onShouldStartLoadWithRequest prop is set, then that function would also be called, and then that would determine whether the URL should be loaded. This can result in a situation where the URL is passed to Linking.openURL and onShouldStartLoadWithRequest returns true so it is also loaded in the WebView. The client can fix this by duplicating the origin whitelist logic in their onShouldStartLoadWithRequest of course, but this change makes it so they don't have to. Co-authored-by: Jason Safaiyeh <safaiyeh@protonmail.com>
1 parent 13ae8c9 commit 0442126

2 files changed

Lines changed: 90 additions & 12 deletions

File tree

src/WebViewShared.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,17 @@ const createOnShouldStartLoadWithRequest = (
4444
const { url, lockIdentifier } = nativeEvent;
4545

4646
if (!passesWhitelist(compileWhitelist(originWhitelist), url)) {
47-
Linking.openURL(url);
47+
Linking.canOpenURL(url).then((supported) => {
48+
if (supported) {
49+
return Linking.openURL(url);
50+
}
51+
console.warn(`Can't open url: ${url}`);
52+
return undefined;
53+
}).catch(e => {
54+
console.warn('Error opening URL: ', e);
55+
});
4856
shouldStart = false;
49-
}
50-
51-
if (onShouldStartLoadWithRequest) {
57+
} else if (onShouldStartLoadWithRequest) {
5258
shouldStart = onShouldStartLoadWithRequest(nativeEvent);
5359
}
5460

src/__tests__/WebViewShared-test.js

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,31 @@ import {
55
createOnShouldStartLoadWithRequest,
66
} from '../WebViewShared';
77

8+
Linking.openURL.mockResolvedValue(undefined);
9+
Linking.canOpenURL.mockResolvedValue(true);
10+
11+
// The tests that call createOnShouldStartLoadWithRequest will cause a promise
12+
// to get kicked off (by calling the mocked `Linking.canOpenURL`) that the tests
13+
// _need_ to get run to completion _before_ doing any `expect`ing. The reason
14+
// is: once that promise is resolved another function should get run which will
15+
// call `Linking.openURL`, and we want to test that.
16+
//
17+
// Normally we would probably do something like `await
18+
// createShouldStartLoadWithRequest(...)` in the tests, but that doesn't work
19+
// here because the promise that gets kicked off is not returned (because
20+
// non-test code doesn't need to know about it).
21+
//
22+
// The tests thus need a way to "flush any pending promises" (to make sure
23+
// pending promises run to completion) before doing any `expect`ing. `jest`
24+
// doesn't provide a way to do this out of the box, but we can use this function
25+
// to do it.
26+
//
27+
// See this issue for more discussion: https://github.com/facebook/jest/issues/2157
28+
function flushPromises() {
29+
return new Promise(resolve => setImmediate(resolve));
30+
}
31+
32+
833
describe('WebViewShared', () => {
934
test('exports defaultOriginWhitelist', () => {
1035
expect(defaultOriginWhitelist).toMatchSnapshot();
@@ -21,114 +46,161 @@ describe('WebViewShared', () => {
2146

2247
const loadRequest = jest.fn();
2348

24-
test('loadRequest is called without onShouldStartLoadWithRequest override', () => {
49+
test('loadRequest is called without onShouldStartLoadWithRequest override', async () => {
2550
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
2651
loadRequest,
2752
defaultOriginWhitelist,
2853
);
2954

3055
onShouldStartLoadWithRequest({ nativeEvent: { url: 'https://www.example.com/', lockIdentifier: 1 } });
56+
57+
await flushPromises();
58+
3159
expect(Linking.openURL).toHaveBeenCalledTimes(0);
3260
expect(loadRequest).toHaveBeenCalledWith(true, 'https://www.example.com/', 1);
3361
});
3462

35-
test('Linking.openURL is called without onShouldStartLoadWithRequest override', () => {
63+
test('Linking.openURL is called without onShouldStartLoadWithRequest override', async () => {
3664
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
3765
loadRequest,
3866
defaultOriginWhitelist,
3967
);
4068

4169
onShouldStartLoadWithRequest({ nativeEvent: { url: 'invalid://example.com/', lockIdentifier: 2 } });
70+
71+
await flushPromises();
72+
4273
expect(Linking.openURL).toHaveBeenCalledWith('invalid://example.com/');
4374
expect(loadRequest).toHaveBeenCalledWith(false, 'invalid://example.com/', 2);
4475
});
4576

46-
test('loadRequest with true onShouldStartLoadWithRequest override is called', () => {
77+
test('loadRequest with true onShouldStartLoadWithRequest override is called', async () => {
4778
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
4879
loadRequest,
4980
defaultOriginWhitelist,
5081
alwaysTrueOnShouldStartLoadWithRequest,
5182
);
5283

5384
onShouldStartLoadWithRequest({ nativeEvent: { url: 'https://www.example.com/', lockIdentifier: 1 } });
85+
86+
await flushPromises();
87+
5488
expect(Linking.openURL).toHaveBeenCalledTimes(0);
5589
expect(loadRequest).toHaveBeenLastCalledWith(true, 'https://www.example.com/', 1);
5690
});
5791

58-
test('Linking.openURL with true onShouldStartLoadWithRequest override is called for links not passing the whitelist', () => {
92+
test('Linking.openURL with true onShouldStartLoadWithRequest override is called for links not passing the whitelist', async () => {
5993
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
6094
loadRequest,
6195
defaultOriginWhitelist,
6296
alwaysTrueOnShouldStartLoadWithRequest,
6397
);
6498

99+
var a = 10;
65100
onShouldStartLoadWithRequest({ nativeEvent: { url: 'invalid://example.com/', lockIdentifier: 1 } });
101+
102+
await flushPromises();
103+
66104
expect(Linking.openURL).toHaveBeenLastCalledWith('invalid://example.com/');
67-
expect(loadRequest).toHaveBeenLastCalledWith(true, 'invalid://example.com/', 1);
105+
// We don't expect the URL to have been loaded in the WebView because it
106+
// is not in the origin whitelist
107+
expect(loadRequest).toHaveBeenLastCalledWith(false, 'invalid://example.com/', 1);
68108
});
69109

70-
test('loadRequest with false onShouldStartLoadWithRequest override is called', () => {
110+
test('loadRequest with false onShouldStartLoadWithRequest override is called', async () => {
71111
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
72112
loadRequest,
73113
defaultOriginWhitelist,
74114
alwaysFalseOnShouldStartLoadWithRequest,
75115
);
76116

77117
onShouldStartLoadWithRequest({ nativeEvent: { url: 'https://www.example.com/', lockIdentifier: 1 } });
118+
119+
await flushPromises();
120+
78121
expect(Linking.openURL).toHaveBeenCalledTimes(0);
79122
expect(loadRequest).toHaveBeenLastCalledWith(false, 'https://www.example.com/', 1);
80123
});
81124

82-
test('loadRequest with limited whitelist', () => {
125+
test('loadRequest with limited whitelist', async () => {
83126
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
84127
loadRequest,
85128
['https://*'],
86129
);
87130

88131
onShouldStartLoadWithRequest({ nativeEvent: { url: 'https://www.example.com/', lockIdentifier: 1 } });
132+
133+
await flushPromises();
134+
89135
expect(Linking.openURL).toHaveBeenCalledTimes(0);
90136
expect(loadRequest).toHaveBeenLastCalledWith(true, 'https://www.example.com/', 1);
91137

92138
onShouldStartLoadWithRequest({ nativeEvent: { url: 'http://insecure.com/', lockIdentifier: 2 } });
139+
140+
await flushPromises();
141+
93142
expect(Linking.openURL).toHaveBeenLastCalledWith('http://insecure.com/');
94143
expect(loadRequest).toHaveBeenLastCalledWith(false, 'http://insecure.com/', 2);
95144

96145
onShouldStartLoadWithRequest({ nativeEvent: { url: 'git+https://insecure.com/', lockIdentifier: 3 } });
146+
147+
await flushPromises();
148+
97149
expect(Linking.openURL).toHaveBeenLastCalledWith('git+https://insecure.com/');
98150
expect(loadRequest).toHaveBeenLastCalledWith(false, 'git+https://insecure.com/', 3);
99151

100152
onShouldStartLoadWithRequest({ nativeEvent: { url: 'fakehttps://insecure.com/', lockIdentifier: 4 } });
153+
154+
await flushPromises();
155+
101156
expect(Linking.openURL).toHaveBeenLastCalledWith('fakehttps://insecure.com/');
102157
expect(loadRequest).toHaveBeenLastCalledWith(false, 'fakehttps://insecure.com/', 4);
103158
});
104159

105-
test('loadRequest allows for valid URIs', () => {
160+
test('loadRequest allows for valid URIs', async () => {
106161
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
107162
loadRequest,
108163
['plus+https://*', 'DOT.https://*', 'dash-https://*', '0invalid://*', '+invalid://*'],
109164
);
110165

111166
onShouldStartLoadWithRequest({ nativeEvent: { url: 'plus+https://www.example.com/', lockIdentifier: 1 } });
167+
168+
await flushPromises();
112169
expect(Linking.openURL).toHaveBeenCalledTimes(0);
113170
expect(loadRequest).toHaveBeenLastCalledWith(true, 'plus+https://www.example.com/', 1);
114171

115172
onShouldStartLoadWithRequest({ nativeEvent: { url: 'DOT.https://www.example.com/', lockIdentifier: 2 } });
173+
174+
await flushPromises();
175+
116176
expect(Linking.openURL).toHaveBeenCalledTimes(0);
117177
expect(loadRequest).toHaveBeenLastCalledWith(true, 'DOT.https://www.example.com/', 2);
118178

119179
onShouldStartLoadWithRequest({ nativeEvent: { url: 'dash-https://www.example.com/', lockIdentifier: 3 } });
180+
181+
await flushPromises();
182+
120183
expect(Linking.openURL).toHaveBeenCalledTimes(0);
121184
expect(loadRequest).toHaveBeenLastCalledWith(true, 'dash-https://www.example.com/', 3);
122185

123186
onShouldStartLoadWithRequest({ nativeEvent: { url: '0invalid://www.example.com/', lockIdentifier: 4 } });
187+
188+
await flushPromises();
189+
124190
expect(Linking.openURL).toHaveBeenLastCalledWith('0invalid://www.example.com/');
125191
expect(loadRequest).toHaveBeenLastCalledWith(false, '0invalid://www.example.com/', 4);
126192

127193
onShouldStartLoadWithRequest({ nativeEvent: { url: '+invalid://www.example.com/', lockIdentifier: 5 } });
194+
195+
await flushPromises();
196+
128197
expect(Linking.openURL).toHaveBeenLastCalledWith('+invalid://www.example.com/');
129198
expect(loadRequest).toHaveBeenLastCalledWith(false, '+invalid://www.example.com/', 5);
130199

131200
onShouldStartLoadWithRequest({ nativeEvent: { url: 'FAKE+plus+https://www.example.com/', lockIdentifier: 6 } });
201+
202+
await flushPromises();
203+
132204
expect(Linking.openURL).toHaveBeenLastCalledWith('FAKE+plus+https://www.example.com/');
133205
expect(loadRequest).toHaveBeenLastCalledWith(false, 'FAKE+plus+https://www.example.com/', 6);
134206
});

0 commit comments

Comments
 (0)