flawopen.com/Cross-Site Scripting/Kotlin

Cross-Site Scripting in Kotlin

High CWE-79 Draft — pending review
ELI5

Imagine a guestbook where visitors write a public note. Stored XSS is like someone writing a note that isn't just text — it's a hidden trick that makes the guestbook page itself start doing things, like stealing the next visitor's login session, the moment anyone opens the page to read it.

Key terms on this page
output encoding
Converting characters that have special meaning in HTML (like < and >) into harmless equivalents before inserting untrusted text into a page, so the browser (or WebView) displays it as text instead of running it as markup or script.
WebView
An embedded browser component inside a native Android app, used to render HTML/JS content — including, when misused, untrusted content that can carry the same XSS risk as a normal browser tab.

What's happening

On the server side (Ktor, Spring), Kotlin XSS follows the same JVM templating rules as Java — Thymeleaf's th:utext vs. th:text. On Android, a distinct and higher-severity version exists: a WebView rendering untrusted HTML while also exposing native app functionality to it via addJavascriptInterface().

Real-world impact

In 2005, an 19-year-old user exploited a stored XSS flaw on MySpace to create the "Samy" worm — a script that added itself to every profile that viewed it, infecting over a million profiles within about 20 hours and forcing MySpace offline to contain it. It remains one of the most-cited demonstrations of how quickly stored XSS can self-propagate.

Source: widely documented in security industry retrospectives — see References below.

Vulnerable vs. fixed

VULNERABLE
// untrusted HTML loaded with JS + native bridge
webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(
  NativeBridge(), "Android"
)
webView.loadData(untrustedHtml,
  "text/html", "UTF-8")
FIXED
// sanitize first; avoid exposing a bridge
val safeHtml = sanitize(untrustedHtml)
webView.settings.javaScriptEnabled = false
webView.loadData(safeHtml,
  "text/html", "UTF-8")

Why the fix works

Sanitizing the HTML against an allow-list before it reaches the WebView removes any <script> tags or event-handler attributes it might contain. Disabling JavaScript (when not genuinely needed) removes the execution engine entirely, and not registering a addJavascriptInterface() bridge means that even if a script did run, it has no path into native app code or data.

Kotlin/Android-specific gotchas

WebView XSS can be more severe than browser XSS

addJavascriptInterface() exposes real native/Java methods to whatever JavaScript runs in the WebView. Injected script isn't limited to stealing a session cookie — depending on what the bridge exposes, it can potentially call real app functionality directly.

Server-side Kotlin (Ktor/Spring) shares Java's Thymeleaf rules

If your Kotlin backend uses Thymeleaf, the same th:text (safe) vs. th:utext (unescaped) distinction from the Java page applies directly, unchanged.

Loading a javascript: URI is an XSS-equivalent vector

webView.loadUrl("javascript:...") built with any untrusted string is functionally the same risk as injecting a script tag — it executes arbitrary JS in the WebView's current page context.

Common misconceptions

"This is a native app, not a website — XSS doesn't apply"

Any WebView rendering HTML carries the same fundamental risk as a browser tab — the vulnerability class is identical, and the native-app context can make the impact worse, not better.

"Kotlin's null-safety features protect against this"

Null-safety and output-encoding safety are unrelated properties — a non-null, well-formed HTML string can still carry an injected script.

How to check if you're affected

grep -rn "addJavascriptInterface(" --include="*.kt" . grep -rn "loadData(\|loadUrl(\"javascript:" --include="*.kt" .
Android Lint's AddJavascriptInterface check flags this pattern automatically for apps targeting API levels below 17, where the risk is most severe; manual review is still needed for newer targets.

Prevention checklist

FAQ

Is this the same bug as SQL injection?

Same underlying shape — untrusted data mixed into a command's structure without encoding — but the target and damage differ: SQL injection targets the database, XSS targets other users' browsers or, on Android, potentially native app functionality.

Is loading a remote URL in a WebView the same risk as loadData()?

A remote URL you don't control carries the same class of risk if it can be influenced by an attacker (e.g., a redirect chain) — the safest posture is restricting WebView navigation to a known allow-list of trusted origins.

References

View in: Python JavaScript Go Java PHP C# Ruby C/C++ Rust Kotlin Swift Solidity (N/A)