JavaScript injection in a web view
ID |
swift.javascript_injection_webview |
Severity |
critical |
Resource |
Injection |
Language |
Swift |
Tags |
CWE:94, MASWE:0040, NIST.SP.800-53, OWASP:2021:A3, PCI-DSS:6.5.10 |
Description
Evaluating JavaScript code with inserted text from a remote origin may lead to remote code execution. Code written by an attacker can execute unauthorized actions, including exfiltration of sensitive local data through a third party web service.
Rationale
The following is an example of a vulnerable code:
import WebKit
let webview = WKWebView(...)
let userInput = getRemoteInput() // Tainted data from a remote source
let jsCode = "doSomething('\(userInput)')" // Tainted data inserted into JavaScript code
webview.evaluateJavaScript(jsCode) // FLAW
Threat actors can insert arbitrary JavaScript code into the userInput variable, which is then executed by the underlying JavaScript engine. This may lead to remote code execution, sensitive data exfiltration, malware download, and other attacks.
For example, using '); EVIL_CODE_HERE; // as attack payload, an attacker may execute arbitrary code.
Remediation
When loading JavaScript into a WebView, evaluate only known, locally-defined source code. If part of the input comes from a remote source, do not inject it into the JavaScript code to be evaluated. Instead, send it to the WKWebView class as data using an API such as callAsyncJavaScript with the arguments dictionary to pass remote data objects.
import WebKit
let webview = WKWebView(...)
let userInput = getRemoteInput() // Tainted data from a remote source
// FIXED - code and data in separate planes
try await webview.callAsyncJavaScript(
"doSomething(input)",
arguments: ["input": userInput],
contentWorld: .page)
Never use evaluateJavaScript except for running fixed JavaScript code that is fully defined in your application. That method does not provide an arguments dictionary to pass values. Use callAsyncJavaScript instead.
The following are additional best practices that can be applied to reduce the risk of JavaScript injection:
-
If JavaScript execution is unnecessary, disable it in the web view configuration:
// Disable JavaScript execution in web view webView.configuration.preferences.javaScriptEnabled = false // Other useful settings to harden the web view webView.configuration.preferences.javaScriptCanOpenWindowsAutomatically = false webView.configuration.preferences.setValue(false, forKey: "allowFileAccessFromFileURLs")Alternatively, use WKWebpagePreferences.allowsContentJavaScript to disable JavaScript on a per-navigation basis
-
Avoid, at any cost, combining code and untrusted inputs in the same plane. In this case, JavaScript code run in the underlying engine used by the WKWebView component.
-
When that is not possible, validate any data received from externally-controlled sources before using it in your application. Always use a whitelist approach, and reject any input that does not strictly conform to the expected format.
-
Use a dedicated
WKContentWorldfor script execution, either the.pageor a custom one) to isolate it from web content scripts.let isolatedWorld = WKContentWorld(worldName: "AppSecureWorld") webView.callAsyncJavaScript("processData(arguments[0])", arguments: [remoteData], in: nil, contentWorld: isolatedWorld ) -
If you control the web content loaded in the web view, consider using a Content Security Policy (CSP) to restrict the sources from which scripts can be loaded and executed. This reduces exposure to cross-site scripting (XSS) attacks:
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; object-src 'none';">
References
-
CWE-94 : Improper Control of Generation of Code ('Code Injection').
-
OWASP Top 10 2021 - A03 : Injection.
-
MASWE-0040: Insecure Authentication in WebViews.
-
WKWebView.callAsyncJavaScript documentation.