Cross-site scripting via dart:html innerHtml / setInnerHtml from non-literal source
ID |
dart.code.cross_site_scripting |
Severity |
high |
Remediation Complexity |
hard |
Remediation Risk |
high |
Remediation Effort |
medium |
Resource |
Code |
Language |
Dart |
Description
dart:html innerHtml / outerHtml / setInnerHtml is assigned a non-literal value or a string-interpolated literal. If the value comes from user input or a network response, the browser executes attacker-controlled HTML and JavaScript in the page origin.
Rationale
This rule targets dart:html (the dart-to-JS / Flutter Web
surface). Setting element.innerHtml = userValue parses the
string as HTML and renders any <script>, <img onerror>,
<iframe>, javascript: URL, or similar attack vector. The
result is a classic stored / reflected / DOM XSS — depending on
where the tainted value came from — with full access to the page
origin’s DOM, cookies, sessionStorage, and any in-page JS state.
Common patterns this rule catches:
-
el.innerHtml = userBio— user-controlled profile field rendered without sanitization. -
el.innerHtml = 'Welcome, $name'— interpolation in a literal string;name = '<img src=x onerror=fetch(…)>'works. -
el.setInnerHtml(html)without avalidator:argument — thevalidator:named arg lets you supply aNodeValidatorBuilderthat restricts allowed tags / attributes.
Note: pure-Flutter mobile apps don’t use dart:html directly —
it’s a Web-target import. WebView XSS is covered by
unrestricted_webview_load and javascript_injection_webview.
The following code illustrates a vulnerable pattern detected by this rule:
class XssSamples {
void innerHtmlVar(Element el, String userHtml) {
// VULNERABLE: Cross-site scripting via dart:html innerHtml / setInnerHtml from non-literal source
el.innerHtml = userHtml;
}
}
Remediation
Use Element.text = … for textual content (Dart escapes for
you). For genuinely-HTML content, pass a strict
NodeValidatorBuilder to setInnerHtml:
import 'dart:html';
// Vulnerable
el.innerHtml = userBio;
el.innerHtml = 'Welcome, $name';
el.setInnerHtml(html);
// Fixed: textual content only
el.text = userBio;
// Fixed: HTML content with strict allow-list validator
final v = NodeValidatorBuilder()
..allowTextElements()
..allowElement('a', attributes: ['href']);
el.setInnerHtml(html, validator: v);
References
-
OWASP Top 10 2025 - A05 : Injection.
-
dart:html Element.setInnerHtml — API docs — Documents the
validator:named argument for allow-list HTML rendering -
Dart linter — unsafe_html — The first-party lint that this rule complements