flawopen.com/xss/Kotlin
Cross-Site Scripting (XSS) in Kotlin (Ktor, kotlinx.html, Android WebView)
Kotlin XSS can enter through raw HTML or interpolated WebView scripts. See how Ktor escapes output and how to pass data safely.
Think of a Ktor page as a signboard: kotlinx.html +text engraves a value as text, while unsafe { } pastes it in as markup. A WebView script is a different channel: interpolating a name into evaluateJavascript changes the script itself. Pass the name as a JavaScript value with a safe argument API or JSONObject.quote; do not treat HTML escaping as JavaScript quoting.
Core Concepts & Subsystem Terms
String templates in HTML responses"<h1>Results for $q</h1>"is plain string interpolation; nothing is escaped. Sent withContentType.Text.Html, the browser parses whateverqcontained as markup.kotlinx.html text vs unsafe { }- In the kotlinx.html DSL used by Ktor's
respondHtml,+textinside an element is HTML-escaped.unsafe { +raw }inserts the string as markup with no escaping and should only receive sanitized or constant content. WebView.evaluateJavascriptevaluateJavascriptruns a string of JavaScript in the page loaded by the WebView. Interpolating a value between quotes lets a'in the value end the string literal and add new statements, which then run with the page's origin and anyaddJavascriptInterfacebridges.JSONObject.quoteorg.json.JSONObject.quote(value)returns a double-quoted JSON string literal with quotes, backslashes and control characters escaped. JSON string literals are valid JavaScript, so the result can be passed to a script function as a single, inert argument.OWASP Java HTML Sanitizer- For user-supplied HTML, a policy such as
Sanitizers.FORMATTING.and(Sanitizers.LINKS)keeps an allowlist of tags and attributes and drops scripts, event handlers andjavascript:URLs before the value reachesunsafe { }.
Framework mappings
- CWE reference for the weakness described in this guide: CWE-79
- OWASP Top 10:2025 — A05:2025 Injection
Step-by-Step Attack Flow
Input arrives from outside
A Ktor route reads q or bio from the query string; an Android activity receives name from a deep link or push notification.
The value bypasses escaping
A string template builds the HTML response, unsafe { } inserts the bio, or evaluateJavascript interpolates the name into a script. The benign test value <img src=x onerror=alert(1)> (or ');alert(1);// for the script) passes through intact.
The browser or WebView runs it
The page contains a real <img> element whose onerror fires, or the WebView executes the extra statement appended to showGreeting(...).
The script acts with the page's privileges
On the web it can call the site's APIs with the victim's cookies. In a WebView it can also call any object exposed through addJavascriptInterface, which may reach native app data.
Source Code: Flaw vs. Secure Implementation
// ProfileRoutes.kt: each path below lets outside text run as HTML or script
fun Application.profileRoutes() {
routing {
// the string template puts q into the HTML unescaped
get("/search") {
val q = call.request.queryParameters["q"].orEmpty()
call.respondText("<h1>Results for $q</h1>", ContentType.Text.Html)
}
// unsafe { } inserts the bio as raw markup
get("/profile") {
val bio = call.request.queryParameters["bio"].orEmpty()
call.respondHtml { body { div { unsafe { +bio } } } }
}
}
}
// a quote in name ends the string and adds a statement
fun showGreeting(webView: WebView, name: String) {
webView.settings.javaScriptEnabled = true
webView.evaluateJavascript("showGreeting('$name')", null)
}
// ProfileRoutes.kt: the DSL escapes text; HTML is sanitized, script args are JSON
private val bioPolicy = Sanitizers.FORMATTING.and(Sanitizers.LINKS)
fun Application.profileRoutes() {
routing {
// +text inside the DSL is HTML-escaped
get("/search") {
val q = call.request.queryParameters["q"].orEmpty()
call.respondHtml { body { h1 { +"Results for $q" } } }
}
// the sanitizer keeps formatting and links before unsafe { }
get("/profile") {
val bio = call.request.queryParameters["bio"].orEmpty()
call.respondHtml { body { div { unsafe { +bioPolicy.sanitize(bio) } } } }
}
}
}
// JSONObject.quote produces one inert string argument
fun showGreeting(webView: WebView, name: String) {
webView.settings.javaScriptEnabled = true
webView.evaluateJavascript("showGreeting(${JSONObject.quote(name)})", null)
}
Engineering & System Hardening Checklist
- ✓Add a detekt or Semgrep rule that fails CI on string templates in
respondText(..., ContentType.Text.Html),loadUrl("javascript:...")andloadData. - ✓Build HTML with the kotlinx.html DSL and review every
unsafe { }block; each must receive sanitized or constant content. - ✓Pass values into WebView scripts only as JSON literals (
JSONObject.quote) or throughWebMessagePort/postWebMessage, never by interpolation. - ✓Sanitize user-supplied HTML with an OWASP Java HTML Sanitizer policy before inserting it.
- ✓Leave
javaScriptEnabledoff unless the page needs it, expose noaddJavascriptInterfaceobject to untrusted content, and send a nonce-based Content-Security-Policy from Ktor.
Sources
- CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
- Ktor — HTML DSL (kotlinx.html)
- Android Developers — WebView.evaluateJavascript
- Android Developers — WebView security risks: cross-site scripting
- OWASP Java HTML Sanitizer
- OWASP Cross Site Scripting Prevention Cheat Sheet