Cross Site Scripting ('XSS')
ID |
swift.cross_site_scripting |
Severity |
critical |
Resource |
Injection |
Language |
Swift |
Tags |
CWE:79, MASWE:0072, NIST.SP.800-53, OWASP:2021:A03, PCI-DSS:6.5.7 |
Description
Improper neutralization of input during web page generation ('Cross-site Scripting' aka 'XSS').
Cross-Site Scripting is a prevalent web application vulnerability that allows attackers to inject malicious scripts into content delivered to other users. These scripts can hijack user sessions, deface websites, or redirect users to malicious sites. XSS commonly arises when an application takes user input, incorporates it into dynamic content served to clients, and fails to sanitize or escape this input correctly.
| There are different kinds of XSS. The kind relevant for this check is Reflected XSS, where the attacker causes the victim to supply malicious content to a vulnerable web application, which renders HTML content embedding a malicious script executed in the victim’s browser. A variant is named DOM-based XSS, where the vulnerable software does not generate content depending on user input but includes script code that uses user-controlled input. |
In iOS and server-side Swift applications, XSS vulnerabilities commonly occur in:
-
WKWebView/UIWebView: Loading user-controlled HTML or JavaScript
-
NSAttributedString: Rendering HTML content in native UI elements
-
Vapor web applications: Rendering templates or responses with user input
-
JavaScript bridges: Evaluating user-controlled JavaScript code
Rationale
A XSS vulnerability happens when untrusted input ends in a place where it is evaluated as HTML or JavaScript code, without proper sanitization. This means that the attacker’s chosen input can be executed by the victim’s browser when the page is rendered by the vulnerable application.
In Swift applications, XSS vulnerabilities manifest in several contexts:
iOS/macOS Applications (WebKit):
import WebKit
class WebViewController: UIViewController {
var webView: WKWebView!
// VULNERABLE: Loading user input as HTML without sanitization
func displayUserContent(userInput: String) {
// FLAW: User input flows directly into HTML rendering
let html = "<html><body><h1>\(userInput)</h1></body></html>"
webView.loadHTMLString(html, baseURL: nil) // XSS vulnerability
}
// VULNERABLE: User input in JavaScript evaluation
func executeUserScript(searchTerm: String) {
// FLAW: User input injected into JavaScript code
let script = "search('\(searchTerm)')"
webView.evaluateJavaScript(script) { result, error in
// JavaScript injection possible
}
}
// VULNERABLE: URL from user input loaded as web view source
func loadUserURL(urlString: String) {
if let url = URL(string: urlString) {
let request = URLRequest(url: url)
webView.load(request) // XSS if urlString is malicious
}
}
// VULNERABLE: User script injection
func addUserScript(scriptSource: String) {
let userScript = WKUserScript(
source: scriptSource, // Tainted from user input
injectionTime: .atDocumentEnd,
forMainFrameOnly: false
)
webView.configuration.userContentController.addUserScript(userScript)
}
}
iOS UI with NSAttributedString:
import UIKit
class AttributedTextViewController: UIViewController {
@IBOutlet weak var textView: UITextView!
// VULNERABLE: Rendering user HTML in native UI
func displayHTMLContent(htmlContent: String) {
// FLAW: User input treated as HTML
if let data = htmlContent.data(using: .utf8) {
let attributedString = try? NSAttributedString(
html: data, // XSS: Executes embedded scripts
options: [:],
documentAttributes: nil
)
textView.attributedText = attributedString
}
}
// VULNERABLE: User input in HTML attributes
func createStyledText(userName: String, userColor: String) {
// FLAW: User input in HTML without escaping
let html = """
<html>
<body>
<span style="color: \(userColor)">Hello, \(userName)</span>
</body>
</html>
"""
if let data = html.data(using: .utf8) {
textView.attributedText = try? NSAttributedString(html: data, documentAttributes: nil)
}
}
}
Vapor Server-Side Swift:
import Vapor
class VaporXSSHandler {
// VULNERABLE: User input in HTML response
func handleSearch(req: Request) throws -> Response {
let query = req.query[String.self, at: "q"] ?? ""
// FLAW: User input directly embedded in HTML
let html = """
<html>
<body>
<h1>Search results for: \(query)</h1>
</body>
</html>
"""
var response = Response(status: .ok)
response.headers.contentType = .html
response.body = .init(string: html) // XSS vulnerability
return response
}
// VULNERABLE: Template with unescaped user input
func renderProfile(req: Request) throws -> EventLoopFuture<View> {
let username = req.query[String.self, at: "name"] ?? "Guest"
// FLAW: If template doesn't escape, XSS possible
return req.view.render("profile", [
"username": username // Needs escaping in template
])
}
// VULNERABLE: JSON response rendered as HTML
func sendUserData(req: Request) throws -> Response {
let userData = req.query[String.self, at: "data"] ?? ""
// FLAW: User data in JavaScript context
let html = """
<script>
var userData = '\(userData)'; // XSS if userData contains quotes
displayData(userData);
</script>
"""
var response = Response(status: .ok)
response.body = .init(string: html)
return response
}
}
These examples demonstrate common XSS patterns:
-
HTML injection: User input embedded in HTML without encoding
-
JavaScript injection: User input concatenated into JavaScript code
-
Attribute injection: User input in HTML attributes without quotes/escaping
-
URL injection: Malicious URLs (javascript:, data:) loaded in web views
-
Script source injection: User-controlled script sources in WKUserScript
Remediation
Follow the recommendations given by OWASP in Cross-Site Scripting Prevention Cheat Sheet.
Output Encoding:
The best technique to protect against XSS is contextual output encoding: encode data written to HTML documents, but as XSS exploitation techniques vary by HTML context, each context has a specific encoding to prevent JavaScript code from being interpreted. In essence, consider the HTML page to be rendered as a template, with 'slots' where a developer is allowed to put untrusted data, escaping properly untrusted data according to context-specific rules before placing it in each 'slot'.
The following table summarizes the encoding to apply for each HTML context:
| Context | Encoding | Example |
|---|---|---|
Text nested in tags |
HTML entity encoding: & → &, < → > > → >, " → ", ' → ' |
|
In attribute, but not holding URL or JavaScript. |
HTML entity encoding, as above, using \&#xHH. Always put the attribute value between quotes! |
|
In an URL attribute |
Validate (whitelist) the URL. Use HTML attribute encoding, as above. For the query string use URL encoding before HTML attribute encoding. |
|
In a JavaScript attribute |
DANGEROUS. Avoid if possible. |
|
In inline CSS |
Safe only with dynamic CSS property value |
|
In script block |
DANGEROUS. Avoid if possible. Use predefined JavaScript functions and choose functions to render based on user input.
Use |
|
There are inherently dangerous contexts where output encoding is too complex and error-prone: JavaScript dynamic code, as within <`<script>` body, CSS dynamic code, as within <style> body, JavaScript event handlers onXYX, HTML comments. No output encoding should be tried, and only strict input validation with a limited set of values allowed should be attempted.
| If templates are used for content rendering, the templates engine chosen could not escape correctly untrusted data, for preventing XSS. |
Input Validation:
Input validation here is best understood as a complementary defense-in-depth strategy, particularly when the input type / format is known. A whitelist-approach is recommended. For dangerous contexts, strict input validation is the unique option.
Content Security Policy (CSP):
An allowlist that prevents content being loaded. It is easy to make mistakes with the implementation so it should not be your primary defense mechanism.
Most browsers could limit the damage via security restrictions (e.g. 'same origin policy'), but users generally allow scripting languages (e.g. JavaScript) in their browsers (disabling JavaScript severely limits a web site).
Web Application Firewalls (WAF):
While WAFs provide some XSS protection, they should be considered a complementary layer to output encoding and input validation within the application itself.
Swift-Specific XSS Prevention
Avoid Dynamic HTML Generation - Use Safe APIs:
import WebKit
class SecureWebViewController: UIViewController {
var webView: WKWebView!
// FIXED: Don't load user input as HTML - use safe alternatives
func displayUserContent(userInput: String) {
// Option 1: Display in native UI instead of web view
let label = UILabel()
label.text = userInput
// Option 2: If web view needed, load trusted content only
if let url = URL(string: "https://trusted-domain.com/page.html") {
webView.load(URLRequest(url: url))
}
}
// FIXED: Never evaluate user input as JavaScript
func executeSearch(searchTerm: String) { // searchTerm = external input
// Pass data via message handler instead of script injection
let message: [String: Any] = ["action": "search", "term": searchTerm]
let jsonData = try? JSONSerialization.data(withJSONObject: message)
let jsonString = String(data: jsonData!, encoding: .utf8)!
// Post message to JavaScript (safe - JS must parse JSON)
webView.evaluateJavaScript(
"window.postMessage(\(jsonString), '*')"
) { _, _ in }
}
// SECURE: Validate URL scheme before loading
func loadUserURL(urlString: String) {
guard
let url = URL(string: urlString),
let scheme = url.scheme,
// add further validations on external url
["http", "https"].contains(scheme.lowercased())
else {
print("Rejected non-HTTP(S) URL")
return
}
// Additional: Check against domain whitelist
let allowedDomains = ["example.com", "trusted.com"]
if let host = url.host,
allowedDomains.contains(where: { host.hasSuffix($0) })
{
webView.load(URLRequest(url: url))
}
}
}
Vapor Template Escaping:
import Vapor
import Leaf
class SecureVaporHandler {
// SECURE: Use leaf template engine auto-escaping
func renderProfile(req: Request) throws -> EventLoopFuture<View> {
let username = req.query[String.self, at: "name"] ?? "Guest"
// Leaf templates auto-escape by default
return req.view.render("profile", [
"username": username // Auto-escaped in template: #(username)
])
}
// SECURE: Manual HTML encoding if not using templates
func handleSearch(req: Request) throws -> Response {
let query = req.query[String.self, at: "q"] ?? ""
// HTML encode user input
let safeQuery = query.htmlEncoded()
let html = """
<html>
<body>
<h1>Search results for: \(safeQuery)</h1>
</body>
</html>
"""
var response = Response(status: .ok)
response.headers.contentType = .html
response.body = .init(string: html)
return response
}
}
| You should never use the #unsafeHTML tag in Leaf templates, unless you know what you are doing ! |
Key Prevention Strategies:
-
Avoid HTML rendering of user input - Use native UI components
-
HTML encode - Use
.htmlEncoded()for web view content -
Validate URLs - Check scheme and domain before loading
-
Never eval user input - Don’t use
evaluateJavaScriptwith user data -
Use template auto-escaping - Leaf, Stencil auto-escape by default
-
JSON encode for JS contexts - Use JSONEncoder for data in
<script> -
Content Security Policy - Configure CSP headers in Vapor responses
For Vapor, you may add CSP headers and Moat middleware for origins check (this may prevent CSRF attacks as well:
// Use Leaf for all HTML rendering (automatic escaping)
app.views.use(.leaf)
// Add security headers middleware with CSP header
let securityHeadersFactory = SecurityHeadersFactory()
.with(contentSecurityPolicy: ContentSecurityPolicyConfiguration(
value: "default-src 'self'; script-src 'self'"
))
app.middleware.use(securityHeadersFactory.build())
// Add CSRF protection
// See https://github.com/vapor-community/CSRF package
// Consider Moat for additional filtering
app.middleware.use(OriginCheckMiddleware(
origin: ["https://yourdomain.com"],
referer: ["https://yourdomain.com/"]
))
Configuration
The detector has the following configurable parameters:
-
sources, that indicates the source kinds to check. -
neutralizations, that indicates the neutralization kinds to check.
Unless you need to change the default behavior, you typically do not need to configure this detector.
References
-
CWE-79 : Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
-
OWASP Top 10 2021 - A03 : Injection.
-
MASWE-0072: Universal XSS.
-
Vapor Leaf Templating - Auto-escaping templates.