WebView executes JavaScript from a non-literal (potentially attacker-controlled) source

ID

dart.platform.javascript_injection_webview

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Platform

Language

Dart

Description

A WebView executes a JavaScript source built from a non-literal value (runJavaScript($JS), runJavaScriptReturningResult($JS), evaluateJavascript(source: $JS), callAsyncJavaScript(functionBody: $JS), injectCSSCode(source: $CSS), or string-interpolated literal). The injected script runs with the page’s full DOM and channel access.

Rationale

runJavaScript and evaluateJavascript are the canonical sinks for JavaScript-injection-in-WebView bugs. If any portion of the JS source comes from a tainted source (user input, network response, deeplink), the attacker can:

  • Read and exfiltrate page-origin cookies, sessionStorage, DOM forms.

  • Invoke JavaScript channels / handlers that the app registered (addJavaScriptChannel, addJavaScriptHandler), escalating into Dart-level actions like file writes or platform invocations.

  • Stage UI overlays (phishing dialogs that look like the app).

  • Pivot to other origins if webview_file_access_enabled is also set, exfiltrating local files.

Common bug shape: building a small JS snippet to set form values or scroll the page, using "document.getElementById('x').value = $user" with no escaping. This rule flags both bare-identifier arguments and $-interpolated string literals.

The following code illustrates a vulnerable pattern detected by this rule:

class JsInjection {
  Future<void> wvf(WebViewController controller, String js) async {
    // VULNERABLE: WebView executes JavaScript from a non-literal (potentially attacker-controlled) source
    await controller.runJavaScript(js);
    // VULNERABLE: WebView executes JavaScript from a non-literal (potentially attacker-controlled) source
    final r = await controller.runJavaScriptReturningResult(js);
    print('$r');
  }

  Future<void> wvfInterp(WebViewController controller, String user) async {
    // String interpolation makes the source non-static.
    await controller.runJavaScript("alert('${user}')");
  }
}

Remediation

Treat the WebView like a remote browser: never inject JavaScript built from runtime values. If you must communicate from Dart to JS, use a JavaScript channel: register the handler in JS, then post structured messages (JSON over postMessage) which the page’s own trusted script reads and applies safely.

// Vulnerable
controller.runJavaScript(js);                                 // js is variable
controller.runJavaScript('alert("$user")');                   // interpolation
webViewController.evaluateJavascript(source: src);

// Fixed: hardcoded JS literal
controller.runJavaScript("document.body.scrollTo(0, 0);");

// Fixed: pass values via a JS channel + JSON
controller.addJavaScriptChannel('AppBridge', onMessageReceived: (msg) {
  // safe Dart-side handler reads structured msg.message
});
controller.runJavaScript("window.postMessage(JSON.stringify({type:'sync'}), '*');");

Configuration

This detector does not need any configuration.

References

© 2026 Xygeni. All rights reserved