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:

  1. HTML injection: User input embedded in HTML without encoding

  2. JavaScript injection: User input concatenated into JavaScript code

  3. Attribute injection: User input in HTML attributes without quotes/escaping

  4. URL injection: Malicious URLs (javascript:, data:) loaded in web views

  5. 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: & → &amp;, < → &gt; > → &gt;, " → &quot;, ' → &#x27;

<div>{DATA}</div>

In attribute, but not holding URL or JavaScript.

HTML entity encoding, as above, using \&#xHH. Always put the attribute value between quotes!

<div class="{DATA}"> …​ </div>

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.

<a href="{DATA}"> …​ </a>

In a JavaScript attribute

DANGEROUS. Avoid if possible.

<script src="{DATA}"> …​ </script>

<div onclick="{DATA}"> …​ </div>"

In inline CSS

Safe only with dynamic CSS property value

<div style="prop: {DATA}"> …​ </div>

<style> selector { prop: "{DATA}" } </style>

In script block

DANGEROUS. Avoid if possible. Use predefined JavaScript functions and choose functions to render based on user input. Use data-* attributes to pass data to the script: <script data-x="{DATA}">

<script>{DATA}</script>

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:

  1. Avoid HTML rendering of user input - Use native UI components

  2. HTML encode - Use .htmlEncoded() for web view content

  3. Validate URLs - Check scheme and domain before loading

  4. Never eval user input - Don’t use evaluateJavaScript with user data

  5. Use template auto-escaping - Leaf, Stencil auto-escape by default

  6. JSON encode for JS contexts - Use JSONEncoder for data in <script>

  7. 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