From 5186362502cca10feebeb207c778b966aef3f17b Mon Sep 17 00:00:00 2001 From: Jan W Date: Thu, 28 Nov 2024 15:24:02 +0100 Subject: [PATCH 1/5] Revert "remove support for static html instead of real URLs" This reverts commit aa168919a1de76630de7b7acff9e582457257a4d. --- apple/RNCWebView.m | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apple/RNCWebView.m b/apple/RNCWebView.m index d67150a89..7a29d5411 100644 --- a/apple/RNCWebView.m +++ b/apple/RNCWebView.m @@ -645,6 +645,16 @@ - (void)refreshContentInset - (void)visitSource { + // Check for a static html source first + NSString *html = [RCTConvert NSString:_source[@"html"]]; + if (html) { + NSURL *baseURL = [RCTConvert NSURL:_source[@"baseUrl"]]; + if (!baseURL) { + baseURL = [NSURL URLWithString:@"about:blank"]; + } + [_webView loadHTMLString:html baseURL:baseURL]; + return; + } // Add cookie for subsequent resource requests sent by page itself, if cookie was set in headers on WebView NSString *headerCookie = [RCTConvert NSString:_source[@"headers"][@"cookie"]]; if(headerCookie) { From c0b13c8212837839ac9cf6ea96fbf010b4345d92 Mon Sep 17 00:00:00 2001 From: Jan W Date: Thu, 28 Nov 2024 15:24:02 +0100 Subject: [PATCH 2/5] Revert "remove support for html attribute" This reverts commit fa43d00bfced185e2ecfd199fd078764143feff5. --- .../com/reactnativecommunity/webview/RNCWebViewManager.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java index bb517d404..8566984ec 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -445,6 +445,12 @@ public void setIncognito(WebView view, boolean enabled) { @ReactProp(name = "source") public void setSource(WebView view, @Nullable ReadableMap source) { if (source != null) { + if (source.hasKey("html")) { + String html = source.getString("html"); + String baseUrl = source.hasKey("baseUrl") ? source.getString("baseUrl") : ""; + view.loadDataWithBaseURL(baseUrl, html, HTML_MIME_TYPE, HTML_ENCODING, null); + return; + } if (source.hasKey("uri")) { String url = source.getString("uri"); String previousUrl = view.getUrl(); From 99e9c82e9719c8fb02ae1efa612e4604f6615aca Mon Sep 17 00:00:00 2001 From: Jan W Date: Thu, 28 Nov 2024 16:41:39 +0100 Subject: [PATCH 3/5] security: harden --- src/WebView.android.tsx | 76 ++++++++++++++++---------------- src/WebView.ios.tsx | 59 +++++++++++++------------ src/__tests__/validation-test.js | 39 ++++++++++++++++ src/validation.ts | 13 ++++++ 4 files changed, 121 insertions(+), 66 deletions(-) create mode 100644 src/__tests__/validation-test.js create mode 100644 src/validation.ts diff --git a/src/WebView.android.tsx b/src/WebView.android.tsx index 092d792a7..969ed1dfc 100644 --- a/src/WebView.android.tsx +++ b/src/WebView.android.tsx @@ -27,6 +27,7 @@ import { } from './WebViewTypes'; import styles from './WebView.styles'; +import validateProps from './validation' const { getWebViewDefaultUserAgent } = NativeModules.RNCWebViewUtils; @@ -58,39 +59,41 @@ const setSupportMultipleWindows = true; const mixedContentMode = 'never' const hardMinimumChromeVersion = '100.0' // TODO: determinime a good lower bound -const WebViewComponent = forwardRef<{}, AndroidWebViewProps>(({ - overScrollMode = 'always', - javaScriptEnabled = true, - thirdPartyCookiesEnabled = true, - scalesPageToFit = true, - saveFormDataDisabled = false, - cacheEnabled = true, - androidHardwareAccelerationDisabled = false, - androidLayerType = "none", - originWhitelist = defaultOriginWhitelist, - deeplinkWhitelist = defaultDeeplinkWhitelist, - setBuiltInZoomControls = true, - setDisplayZoomControls = false, - nestedScrollEnabled = false, - startInLoadingState, - onLoadStart, - onError, - onLoad, - onLoadEnd, - onMessage: onMessageProp, - onOpenWindow: onOpenWindowProp, - renderLoading, - renderError, - style, - containerStyle, - source, - onShouldStartLoadWithRequest: onShouldStartLoadWithRequestProp, - validateMeta, - validateData, - minimumChromeVersion, - unsupportedVersionComponent: UnsupportedVersionComponent, - ...otherProps -}, ref) => { +const WebViewComponent = forwardRef<{}, AndroidWebViewProps>((props, ref) => { + const { + overScrollMode = 'always', + javaScriptEnabled = true, + thirdPartyCookiesEnabled = true, + scalesPageToFit = true, + saveFormDataDisabled = false, + cacheEnabled = true, + androidHardwareAccelerationDisabled = false, + androidLayerType = "none", + originWhitelist = defaultOriginWhitelist, + deeplinkWhitelist = defaultDeeplinkWhitelist, + setBuiltInZoomControls = true, + setDisplayZoomControls = false, + nestedScrollEnabled = false, + startInLoadingState, + onLoadStart, + onError, + onLoad, + onLoadEnd, + onMessage: onMessageProp, + onOpenWindow: onOpenWindowProp, + renderLoading, + renderError, + style, + containerStyle, + source, + onShouldStartLoadWithRequest: onShouldStartLoadWithRequestProp, + validateMeta, + validateData, + minimumChromeVersion, + unsupportedVersionComponent: UnsupportedVersionComponent, + ...otherProps + } = validateProps(props) + const messagingModuleName = useRef(`WebViewMessageHandler${uniqueRef += 1}`).current; const webViewRef = useRef(null); @@ -197,10 +200,7 @@ const WebViewComponent = forwardRef<{}, AndroidWebViewProps>(({ } } - if (typeof source === "object" && 'uri' in source && !passesWhitelist(source.uri)){ - // eslint-disable-next-line - source = {uri: "about:blank"}; - } + const safeSource = (typeof source === "object" && 'uri' in source && !passesWhitelist(source.uri)) ? { uri: 'about:blank' } : source; const NativeWebView = RNCWebView; @@ -220,7 +220,7 @@ const WebViewComponent = forwardRef<{}, AndroidWebViewProps>(({ ref={webViewRef} // TODO: find a better way to type this. - source={source} + source={safeSource} style={webViewStyles} overScrollMode={overScrollMode} javaScriptEnabled={javaScriptEnabled} diff --git a/src/WebView.ios.tsx b/src/WebView.ios.tsx index b8090ca38..0189bdde2 100644 --- a/src/WebView.ios.tsx +++ b/src/WebView.ios.tsx @@ -26,6 +26,7 @@ import { } from './WebViewTypes'; import styles from './WebView.styles'; +import validateProps from './validation' const codegenNativeCommands = codegenNativeCommandsUntyped as (options: { supportedCommands: (keyof T)[] }) => T; @@ -67,34 +68,36 @@ const enableApplePay = false; const dataDetectorTypes = 'none'; const hardMinimumIOSVersion = '12.5.6 <13, 13.6.1 <14, 14.8.1 <15, 15.7.1' -const WebViewComponent = forwardRef<{}, IOSWebViewProps>(({ - javaScriptEnabled = true, - cacheEnabled = true, - originWhitelist = defaultOriginWhitelist, - deeplinkWhitelist = defaultDeeplinkWhitelist, - textInteractionEnabled= true, - injectedJavaScript, - injectedJavaScriptBeforeContentLoaded, - startInLoadingState, - onLoadStart, - onError, - onLoad, - onLoadEnd, - onMessage: onMessageProp, - renderLoading, - renderError, - style, - containerStyle, - source, - incognito, - validateMeta, - validateData, - decelerationRate: decelerationRateProp, - onShouldStartLoadWithRequest: onShouldStartLoadWithRequestProp, - minimumIOSVersion, - unsupportedVersionComponent: UnsupportedVersionComponent, - ...otherProps -}, ref) => { +const WebViewComponent = forwardRef<{}, IOSWebViewProps>((props, ref) => { + const { + javaScriptEnabled = true, + cacheEnabled = true, + originWhitelist = defaultOriginWhitelist, + deeplinkWhitelist = defaultDeeplinkWhitelist, + textInteractionEnabled= true, + injectedJavaScript, + injectedJavaScriptBeforeContentLoaded, + startInLoadingState, + onLoadStart, + onError, + onLoad, + onLoadEnd, + onMessage: onMessageProp, + renderLoading, + renderError, + style, + containerStyle, + source, + incognito, + validateMeta, + validateData, + decelerationRate: decelerationRateProp, + onShouldStartLoadWithRequest: onShouldStartLoadWithRequestProp, + minimumIOSVersion, + unsupportedVersionComponent: UnsupportedVersionComponent, + ...otherProps + } = validateProps(props) + const webViewRef = useRef(null); const onShouldStartLoadWithRequestCallback = useCallback(( diff --git a/src/__tests__/validation-test.js b/src/__tests__/validation-test.js new file mode 100644 index 000000000..66fab0cf2 --- /dev/null +++ b/src/__tests__/validation-test.js @@ -0,0 +1,39 @@ +import validateProps from '../validation' + +describe('validateProps', () => { + + test('throws when providing static html without origin whitelist', () => { + expect(() => { + validateProps({ + source: { html: '

Wayne Foundation

'} + }) + }).toThrow('originWhitelist') + }) + + test('throws when providing static html with wildcard whitelist', () => { + expect(() => { + validateProps({ + originWhitelist: ['*', 'http://localhost'], + source: { html: '

Wayne Foundation

'} + }) + }).toThrow('originWhitelist') + }) + + test('throws when providing static html with empty whitelist', () => { + expect(() => { + validateProps({ + originWhitelist: [], + source: { html: '

Wayne Foundation

'} + }) + }).toThrow('originWhitelist') + }) + + test('returns props when origin whitelist present', () => { + const props = { + originWhitelist: ['http://localhost'], + source: { html: '

Wayne Foundation

'} + } + + expect(validateProps(props)).toBe(props) + }) +}) \ No newline at end of file diff --git a/src/validation.ts b/src/validation.ts new file mode 100644 index 000000000..43c7a62e3 --- /dev/null +++ b/src/validation.ts @@ -0,0 +1,13 @@ +import invariant from 'invariant' +import { AndroidWebViewProps, IOSWebViewProps } from './WebViewTypes' + +const validateProps =

(props: P): P => { + if(props.source && 'html' in props.source){ + const { originWhitelist } = props + invariant(originWhitelist && originWhitelist.length > 0 && !originWhitelist.includes('*'), 'originWhitelist is required when using html prop and cannot include *') + } + + return props +} + +export default validateProps \ No newline at end of file From 1feeb1ee318ad47cd5cd0cc822b7b3b430c94e06 Mon Sep 17 00:00:00 2001 From: Jan W Date: Thu, 28 Nov 2024 16:43:17 +0100 Subject: [PATCH 4/5] fix: qualify prop path --- src/validation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation.ts b/src/validation.ts index 43c7a62e3..c89550194 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -4,7 +4,7 @@ import { AndroidWebViewProps, IOSWebViewProps } from './WebViewTypes' const validateProps =

(props: P): P => { if(props.source && 'html' in props.source){ const { originWhitelist } = props - invariant(originWhitelist && originWhitelist.length > 0 && !originWhitelist.includes('*'), 'originWhitelist is required when using html prop and cannot include *') + invariant(originWhitelist && originWhitelist.length > 0 && !originWhitelist.includes('*'), 'originWhitelist is required when using source.html prop and cannot include *') } return props From d39f0a2f1b1f2c79569dd014e8a7a9354d8d745f Mon Sep 17 00:00:00 2001 From: Jan W Date: Thu, 28 Nov 2024 16:51:43 +0100 Subject: [PATCH 5/5] refactor: type import Co-authored-by: Andrew Toth --- src/validation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation.ts b/src/validation.ts index c89550194..0e0756350 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -1,5 +1,5 @@ import invariant from 'invariant' -import { AndroidWebViewProps, IOSWebViewProps } from './WebViewTypes' +import type { AndroidWebViewProps, IOSWebViewProps } from './WebViewTypes' const validateProps =

(props: P): P => { if(props.source && 'html' in props.source){