Reproduce RUMS-5458: WebView blank screen with New Architecture#1208
Reproduce RUMS-5458: WebView blank screen with New Architecture#1208mariusc83 wants to merge 1 commit into
Conversation
|
✨ Fix all issues with BitsAI or with Cursor
|
|
|
||
| // Verify the JS is syntactically valid by attempting to parse it | ||
| // eslint-disable-next-line no-eval | ||
| expect(() => eval(`(function() { ${injectedJs} })`)).not.toThrow(); |
Check warning
Code scanning / CodeQL
Improper code sanitization Medium test
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 months ago
To fix the problem, we should sanitize the string produced by JSON.stringify(allowedHosts) before embedding it into JavaScript source. Specifically, we should apply an escaping function that replaces characters that could break out of JavaScript/HTML contexts (<, >, /, \, control characters, \u2028, \u2029) with their safe escape sequences (e.g., \u003C), following the pattern in the background section. Then we must use this sanitized value both when creating the comment (// #allowedHosts=...) and when returning the formatted hosts string from formatAllowedHosts, to keep behavior consistent and ensure the comment content and the parsed value match.
Concretely:
- In
packages/react-native-webview/src/utils/webview-js-utils.ts, define a smallescapeUnsafeForJsEmbeddinghelper (similar to the example’sescapeUnsafeChars) that operates on a string and returns a safely escaped version. - In
formatAllowedHosts, afterJSON.stringify(allowedHosts)returnsjsonHosts, pass it throughescapeUnsafeForJsEmbeddingand return the escaped string. This meansformatAllowedHostsnow returns the already-sanitized value to be embedded. - In
wrapJsCodeWithAllowedHosts, instead of directly usingJSON.stringify(allowedHosts)again, reusehosts(the sanitized string fromformatAllowedHosts) when building the comment line:// #allowedHosts=${hosts}\n. This avoids duplicate logic, keeps the comment consistent with whatformatAllowedHostsvalidated, and ensures the comment payload is the escaped JSON.
No changes are needed in the test file’s logic; it will continue to call wrapJsCodeWithAllowedHosts and eval the result, but now the constructed code will be hardened against malformed or malicious allowedHosts content.
| @@ -41,7 +41,8 @@ | ||
| let jsCode = ''; | ||
| const hosts = formatAllowedHosts(allowedHosts); | ||
| if (hosts) { | ||
| jsCode = `// #allowedHosts=${JSON.stringify(allowedHosts)}\n`; | ||
| // hosts is already JSON-stringified and sanitized for safe embedding | ||
| jsCode = `// #allowedHosts=${hosts}\n`; | ||
| } | ||
|
|
||
| return javascriptCode | ||
| @@ -81,7 +82,8 @@ | ||
| "JSON.stringify returned 'undefined' for the given hosts" | ||
| ); | ||
| } | ||
| return jsonHosts; | ||
| // Sanitize the JSON string so it is safe to embed into JavaScript/HTML | ||
| return escapeUnsafeForJsEmbedding(jsonHosts); | ||
| } catch (e: any) { | ||
| if (NativeDdSdk) { | ||
| NativeDdSdk.telemetryError( | ||
| @@ -97,6 +99,29 @@ | ||
| } | ||
| } | ||
|
|
||
| const unsafeCharMap: { [key: string]: string } = { | ||
| '<': '\\u003C', | ||
| '>': '\\u003E', | ||
| '/': '\\u002F', | ||
| '\\': '\\\\', | ||
| '\b': '\\b', | ||
| '\f': '\\f', | ||
| '\n': '\\n', | ||
| '\r': '\\r', | ||
| '\t': '\\t', | ||
| '\0': '\\0', | ||
| '\u2028': '\\u2028', | ||
| '\u2029': '\\u2029' | ||
| }; | ||
|
|
||
| function escapeUnsafeForJsEmbedding(str: string): string { | ||
| // Replace characters that could break out of a JS/HTML context | ||
| return str.replace( | ||
| /[<>\/\\\b\f\n\r\t\0\u2028\u2029]/g, | ||
| (ch) => unsafeCharMap[ch] || ch | ||
| ); | ||
| } | ||
|
|
||
| const getErrorMessage = (error: any | undefined): string => { | ||
| const EMPTY_MESSAGE = 'Unknown Error'; | ||
| let message = EMPTY_MESSAGE; |
|
|
||
| expect(injectedJs).toBeDefined(); | ||
| // eslint-disable-next-line no-eval | ||
| expect(() => eval(`(function() { ${injectedJs} })`)).not.toThrow(); |
Check warning
Code scanning / CodeQL
Improper code sanitization Medium test
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 months ago
To fix the problem, we should ensure that the string we derive from allowedHosts is additionally sanitized before being concatenated into JavaScript code. The recommended pattern is to JSON‑stringify the data (already done) and then escape characters that could prematurely terminate a <script> tag or otherwise alter the JavaScript parsing context (such as <, >, /, certain control characters, and Unicode line separators). This preserves existing functionality (the native side still sees parseable JSON) while preventing injected values from breaking out of the intended context.
Concretely, in packages/react-native-webview/src/utils/webview-js-utils.ts:
- Add a small helper, e.g.
escapeUnsafeCharsForJsComment, that replaces unsafe characters in a string with safe Unicode escape sequences, following the pattern described in the background (mapping<→\u003C, etc.). - Use
formatAllowedHosts(allowedHosts)(which already returns a JSON string orundefined) instead of re‑callingJSON.stringify(allowedHosts)inwrapJsCodeWithAllowedHosts. - Apply the escaping helper to the JSON string before embedding it in the comment, e.g.
jsCode =// #allowedHosts=${escapeUnsafeCharsForJsComment(hosts)}\n; - Leave other behavior (try/catch wrapper, telemetry, etc.) unchanged.
The test file packages/react-native-webview/src/__tests__/WebviewDatadogNewArchIntegration.test.ts does not require logic changes; it will continue to work because the escaped JSON is still valid JSON and can be parsed by extractAllowedHostsFromInjectedJs. No additional imports are needed; the helper can be defined in the same module as wrapJsCodeWithAllowedHosts.
| @@ -34,6 +34,28 @@ | ||
| * @param javascriptCode The JS Code to wrap in a try and catch block. | ||
| * @returns the wrapped JS code. | ||
| */ | ||
| const unsafeCharMap: { [key: string]: string } = { | ||
| '<': '\\u003C', | ||
| '>': '\\u003E', | ||
| '/': '\\u002F', | ||
| '\\': '\\\\', | ||
| '\b': '\\b', | ||
| '\f': '\\f', | ||
| '\n': '\\n', | ||
| '\r': '\\r', | ||
| '\t': '\\t', | ||
| '\0': '\\0', | ||
| '\u2028': '\\u2028', | ||
| '\u2029': '\\u2029' | ||
| }; | ||
|
|
||
| function escapeUnsafeCharsForJsComment(str: string): string { | ||
| return str.replace( | ||
| /[<>/\\\b\f\n\r\t\0\u2028\u2029]/g, | ||
| (ch) => unsafeCharMap[ch] ?? ch | ||
| ); | ||
| } | ||
|
|
||
| export function wrapJsCodeWithAllowedHosts( | ||
| javascriptCode?: string, | ||
| allowedHosts?: string[] | ||
| @@ -41,7 +63,8 @@ | ||
| let jsCode = ''; | ||
| const hosts = formatAllowedHosts(allowedHosts); | ||
| if (hosts) { | ||
| jsCode = `// #allowedHosts=${JSON.stringify(allowedHosts)}\n`; | ||
| const safeHosts = escapeUnsafeCharsForJsComment(hosts); | ||
| jsCode = `// #allowedHosts=${safeHosts}\n`; | ||
| } | ||
|
|
||
| return javascriptCode |
Reproduction for RUMS-5458
Jira: RUMS-5458
Issue Summary
WebView tracking broken in bare RN app with New Architecture - blank screens and WEBVIEW error source. The
@datadog/mobile-react-native-webviewwrapper injects// #allowedHosts=[]when an emptyallowedHostsarray is provided, causing the native side (New Architecture) to callconfigureWebViewTrackingwith zero hosts. This interferes with WebView rendering by replacing the standard WebView withDdReactNativeWebViewbut without a properly configured tracking bridge.Reproduction Tests
WebviewDatadogNewArchBlankScreen.test.tsx)WebviewDatadogNewArchIntegration.test.ts)What the Tests Prove
allowedHosts=[], the JS side still injects// #allowedHosts=[]as a comment, and the native side extracts an empty array. This causesWebViewTracking.enable()to be called with no hosts, which can interfere with WebView rendering without providing any actual tracking benefit.[]as an empty array and passes it toconfigureWebViewTracking, which is unnecessary and may contribute to blank screens when the Datadog native WebView component replaces the standard one.Failure Output
Generated by rum:tee-triage-insights