flawopen.com/xss/Swift
Cross-Site Scripting (XSS) in Swift (WKWebView, Leaf)
Interpolating user text into loadHTMLString or evaluateJavaScript creates HTML or JavaScript injection in your app’s WKWebView (CWE-79). Apple documents baseURL as the base for relative URLs; it is not an HTML sanitizer or a sandbox switch. Escape for the exact output context, pass script values as arguments, and scope native message handlers.
In WKWebView, think of an HTML template like a print form: if user text is inserted as markup, WebKit treats tags as layout instructions; if it is pasted into a JavaScript string, punctuation can change the program. Pass text as data, not source. baseURL only resolves relative URLs; setting it to nil does not sanitize HTML or disable scripts. This is the boundary in loadHTMLString and evaluateJavaScript; callAsyncJavaScript(arguments:) passes the value as data.
Core Concepts & Subsystem Terms
loadHTMLString(_:baseURL:)- Apple documents
baseURLas the URL used to resolve relative URLs in the HTML string. It does not sanitize markup, disable JavaScript, or by itself establish a sandbox or origin guarantee. Decide the document origin and navigation policy separately; do not usenilas an XSS control. evaluateJavaScript with \(value)"showGreeting('\(name)')"builds JavaScript source by string interpolation. A'innameends the string literal, and anything after it runs as code in the page.callAsyncJavaScript(arguments:)- Available since iOS 14 and macOS 11, it runs a function body and passes
argumentsas real JavaScript values, not as source text.showGreeting(name)receives the string untouched, so quotes and</script>inside it are just characters. Leaf #(value) vs #unsafeHTML- In Vapor's Leaf templates,
#(bio)HTML-escapes the value.#unsafeHTML(bio)inserts it as raw markup and should only receive sanitized or constant content, exactly likeHtml.Rawor|safein other frameworks. WKScriptMessageHandler bridgesWKUserContentController.add(_:name:)exposes a message function to page JavaScript. Scripts in that page world, including injected scripts, can call it. The overload that accepts aWKContentWorldscopes which world can call the handler; content worlds isolate JavaScript variables, not the shared DOM. Validate every message and keep native actions narrow.
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 reaches the app
A profile bio is fetched from your API, and a display name arrives in a push notification or universal link. Both are strings controlled by another user or by whoever sent the link.
The value becomes page code
loadHTMLString places the bio into markup, and evaluateJavaScript places the name into script source. The benign test value <img src=x onerror=alert(1)> (or ');alert(1);// for the script) passes through intact.
WebKit runs it
The web view builds a real <img> element and fires onerror, or evaluates the extra statement appended to showGreeting(...).
The script acts with the page's privileges
The script runs in the document’s WebKit context and can manipulate its DOM and objects available there. A handler registered with the page-world add(_:name:) API is callable from page JavaScript. The document’s origin and navigation access depend on how it is loaded and configured; Apple documents baseURL as relative-URL resolution, not as a sanitizer.
Source Code: Flaw vs. Secure Implementation
// ProfileController.swift: each method below turns outside text into page code
import WebKit
final class ProfileController {
let webView: WKWebView
init(webView: WKWebView) {
self.webView = webView
}
// the bio is placed into markup unescaped, with the site's origin
func showBio(_ bio: String) {
webView.loadHTMLString("<div class=\"bio\">\(bio)</div>", baseURL: URL(string: "https://app.example.com"))
}
// a quote in name ends the string literal and adds a statement
func greet(_ name: String) {
webView.evaluateJavaScript("showGreeting('\(name)')")
}
}
// ProfileController.swift: markup is escaped, script data is passed as arguments
import WebKit
final class ProfileController {
let webView: WKWebView
init(webView: WKWebView) {
self.webView = webView
}
// baseURL controls relative-URL resolution; nil is not an XSS or sandbox control
func showBio(_ bio: String) {
let html = "<div class=\"bio\">" + bio.htmlEscaped + "</div>"
webView.loadHTMLString(html, baseURL: nil)
}
// name is passed as a JavaScript value, never as source text
func greet(_ name: String) {
webView.callAsyncJavaScript("showGreeting(name)", arguments: ["name": name], in: nil, in: .page, completionHandler: nil)
}
}
extension String {
// Escape for an HTML text node only; do not reuse for attributes, URLs, CSS, or JavaScript
var htmlEscaped: String {
var out = ""
for ch in self {
switch ch {
case "&": out += "&"
case "<": out += "<"
case ">": out += ">"
case "\"": out += """
case "'": out += "'"
default: out.append(ch)
}
}
return out
}
}
Engineering & System Hardening Checklist
- ✓Add a SwiftLint custom rule or Semgrep rule that fails CI on string interpolation inside
loadHTMLStringandevaluateJavaScriptarguments. - ✓HTML-escape every value placed into markup (
&,<,>,",'), or render with a template engine that escapes by default such as Leaf's#(value). - ✓Pass data to scripts with
callAsyncJavaScript(_:arguments:in:in:completionHandler:)instead of building JavaScript source. - ✓Treat
baseURLonly as relative-URL resolution. Do not rely onbaseURL: nilto sanitize content, disable scripts, or isolate a document; restrict navigation and resource access with explicit policy. - ✓Do not expose privileged native actions to untrusted page-world scripts. If a bridge is required, scope it to an app-owned
WKContentWorldwhere appropriate, validate sender and message data, and authorize each operation.
Sources
- CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
- Apple — WKWebView.loadHTMLString(_:baseURL:)
- Apple — WKWebView.callAsyncJavaScript
- Vapor Leaf — Overview (escaping and #unsafeHTML)
- OWASP MASTG — Testing WebViews
- OWASP Cross Site Scripting Prevention Cheat Sheet
- Apple — WKContentWorld
- Apple — WKUserContentController.add(_:contentWorld:name:)
- Apple — WKWebpagePreferences.allowsContentJavaScript