Skip to content

Commit c2ad41d

Browse files
mzndakosergii-bo
authored andcommitted
feat: validate download file extension and origin
1 parent ae0e797 commit c2ad41d

File tree

4 files changed

+51
-30
lines changed

4 files changed

+51
-30
lines changed

src/WebView.android.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ const WebViewComponent = forwardRef<{}, AndroidWebViewProps>((props, ref) => {
7171
androidLayerType = "none",
7272
originWhitelist = defaultOriginWhitelist,
7373
deeplinkWhitelist = defaultDeeplinkWhitelist,
74-
downloadOriginWhitelist = [],
74+
downloadWhitelist = [],
7575
setBuiltInZoomControls = true,
7676
setDisplayZoomControls = false,
7777
nestedScrollEnabled = false,
@@ -109,7 +109,7 @@ const WebViewComponent = forwardRef<{}, AndroidWebViewProps>((props, ref) => {
109109
}, []);
110110

111111
const { onLoadingStart, onShouldStartLoadWithRequest, onMessage, viewState, setViewState, lastErrorEvent, onLoadingError, onLoadingFinish, onLoadingProgress, onOpenWindow, passesWhitelist } = useWebWiewLogic({
112-
downloadOriginWhitelist,
112+
downloadWhitelist,
113113
onLoad,
114114
onError,
115115
onLoadEnd,

src/WebView.ios.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ const WebViewComponent = forwardRef<{}, IOSWebViewProps>((props, ref) => {
7474
cacheEnabled = true,
7575
originWhitelist = defaultOriginWhitelist,
7676
deeplinkWhitelist = defaultDeeplinkWhitelist,
77-
downloadOriginWhitelist = [],
77+
downloadWhitelist = [],
7878
textInteractionEnabled= true,
7979
injectedJavaScript,
8080
injectedJavaScriptBeforeContentLoaded,
@@ -112,7 +112,7 @@ const WebViewComponent = forwardRef<{}, IOSWebViewProps>((props, ref) => {
112112
}, []);
113113

114114
const { onLoadingStart, onShouldStartLoadWithRequest, onMessage, viewState, setViewState, lastErrorEvent, onLoadingError, onLoadingFinish, onLoadingProgress } = useWebWiewLogic({
115-
downloadOriginWhitelist,
115+
downloadWhitelist,
116116
onLoad,
117117
onError,
118118
onLoadEnd,

src/WebViewShared.tsx

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -63,24 +63,38 @@ const compileWhitelist = (
6363
const isDownloadMessageAllowed = ({
6464
data,
6565
url,
66-
compiledDownloadWhitelist,
66+
downloadWhitelist,
6767
}: {
68-
data: string,
69-
url: string,
70-
compiledDownloadWhitelist: readonly RegExp[],
68+
data: string;
69+
url: string;
70+
downloadWhitelist: { origin: string; allowedFileExtensions: string[] }[];
7171
}): boolean => {
72+
let parsedData;
73+
7274
try {
73-
const parsedData = JSON.parse(data);
74-
75-
if (parsedData?.method === 'download') {
76-
return _passesWhitelist(compiledDownloadWhitelist, url);
77-
}
75+
parsedData = JSON.parse(data);
7876
} catch {
79-
// Ignore invalid JSON — treat as non-download message
77+
return true; // Invalid JSON — treat as non-download message
78+
}
79+
80+
if (parsedData.method !== 'download') {
81+
return true;
82+
}
83+
84+
const { origin } = new URL(url);
85+
const fileExtension = parsedData.params?.fileName?.split('.').pop()?.toLowerCase();
86+
87+
if (!fileExtension || !origin) {
88+
return false;
8089
}
8190

82-
// Non-download messages are allowed by default
83-
return true;
91+
const matchingRule = downloadWhitelist.find(rule => {
92+
const ruleOriginRegex = stringWhitelistToRegex(rule.origin);
93+
94+
return ruleOriginRegex.test(origin) && rule.allowedFileExtensions.includes(fileExtension);
95+
});
96+
97+
return Boolean(matchingRule);
8498
};
8599

86100
const urlToProtocolScheme = (url: string): string | null => {
@@ -175,7 +189,7 @@ export {
175189
};
176190

177191
export const useWebWiewLogic = ({
178-
downloadOriginWhitelist,
192+
downloadWhitelist,
179193
startInLoadingState,
180194
onLoadStart,
181195
onLoad,
@@ -190,7 +204,7 @@ export const useWebWiewLogic = ({
190204
validateMeta,
191205
validateData,
192206
}: {
193-
downloadOriginWhitelist: readonly string[];
207+
downloadWhitelist: { origin: string; allowedFileExtensions: string[] }[];
194208
startInLoadingState?: boolean
195209
onLoadStart?: (event: WebViewNavigationEvent) => void;
196210
onLoad?: (event: WebViewNavigationEvent) => void;
@@ -263,7 +277,9 @@ export const useWebWiewLogic = ({
263277

264278
const onMessage = useCallback((event: WebViewMessageEvent) => {
265279
const { nativeEvent } = event;
266-
if (!passesWhitelistUse(nativeEvent.url)) return;
280+
const { url } = nativeEvent;
281+
282+
if (!passesWhitelistUse(url)) return;
267283

268284
// TODO: can/should we perform any other validation?
269285
try {
@@ -272,10 +288,10 @@ export const useWebWiewLogic = ({
272288

273289
if (!isDownloadMessageAllowed({
274290
data: parsedData.data,
275-
url: nativeEvent.url,
276-
compiledDownloadWhitelist: compileWhitelist(downloadOriginWhitelist, []),
291+
downloadWhitelist,
292+
url,
277293
})) {
278-
console.warn('Download request rejected: origin not in download whitelist');
294+
console.warn('Download request rejected: origin not in download whitelist or file extension not allowed');
279295
return;
280296
}
281297

@@ -286,7 +302,7 @@ export const useWebWiewLogic = ({
286302
} catch (err) {
287303
console.error('Error parsing WebView message', err);
288304
}
289-
}, [onMessageProp, passesWhitelistUse, validateData, validateMeta, downloadOriginWhitelist]);
305+
}, [onMessageProp, passesWhitelistUse, validateData, validateMeta, downloadWhitelist]);
290306

291307
const onLoadingProgress = useCallback((event: WebViewProgressEvent) => {
292308
const { nativeEvent: { progress } } = event;

src/WebViewTypes.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -788,13 +788,18 @@ export interface WebViewSharedProps extends ViewProps {
788788
*/
789789
javaScriptEnabled?: boolean;
790790

791-
/**
792-
* List of origin strings that are allowed to send download data through postMessage.
793-
* The strings allow wildcards and get matched against *just* the origin (not the full URL).
794-
* If a download request comes from an origin not in this whitelist, it will be ignored.
795-
* The default whitelisted origins are empty array [].
796-
*/
797-
readonly downloadOriginWhitelist?: string[];
791+
/**
792+
* List of objects defining the origins allowed to send download data through postMessage.
793+
* Each object contains:
794+
* - `origin`: The allowed origin (matches against the origin, not the full URL).
795+
* - `allowedFileExtensions`: An array of allowed file extensions for downloads from that origin (required).
796+
* If a download request comes from an origin not in this whitelist, or with a disallowed file extension, it will be ignored.
797+
* The default whitelist is an empty array [].
798+
*/
799+
readonly downloadWhitelist?: {
800+
origin: string;
801+
allowedFileExtensions: string[];
802+
}[];
798803

799804
/**
800805
* Defines a list of domain origins that can access camera.

0 commit comments

Comments
 (0)