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_enabledis 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'}), '*');");
References
-
OWASP Top 10 2025 - A05 : Injection.
-
CWE-94 — Improper Control of Generation of Code (JavaScript injection in WebView)
-
package:webview_flutter (pub.dev) —
runJavaScript/runJavaScriptReturningResultreference -
package:flutter_inappwebview (pub.dev) —
evaluateJavascript,callAsyncJavaScript,injectCSSCodereference