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 WKContentWorld for script execution, either the .page or 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';">

Configuration

This detector does not need any configuration.

References