Open Redirect

ID

swift.open_redirect

Severity

high

Resource

Injection

Language

Swift

Tags

CWE:601, NIST.SP.800-53, OWASP:2013:A10, PCI-DSS:6.5.1

Description

URL redirection to untrusted site ('Open Redirect').

Open Redirect vulnerabilities occur when web applications dynamically construct URLs for redirection using user inputs, without proper validation or constraints. These vulnerabilities can be exploited by attackers to redirect users to phishing sites, steal personal information, or perform malicious actions.

In iOS, macOS, and server-side Swift applications, open redirect vulnerabilities commonly occur in:

  • WKWebView: Loading URLs from user-controlled input

  • Vapor web applications: Response.redirect() with external input

  • Deep linking: Custom URL scheme handling without validation

  • Universal Links: Opening URLs based on user parameters

Rationale

Attackers often mislead victims to visit a trusted site and redirect them to an alternate site trying to deceive the victim. This happens when a vulnerable application performs a redirect that concatenates untrusted input.

The attacker often encodes the URL in a way that it is difficult for the user to detect that it is at the wrong site.

In the malicious phishing site, typically mimicking the original site, threat actors may attempt to steal credentials, steal data, or perform other malicious actions.

Server-side forwards, even though they do not allow jumping to an external site, can be used by attackers to bypass the application’s access control checks and forward the attacker to an administrative function that is not normally permitted.

In Swift applications, open redirect vulnerabilities manifest in the following contexts:

iOS/macOS Applications (WebKit):

import WebKit
import UIKit

class WebViewController: UIViewController {
    var webView: WKWebView!

    // VULNERABLE: Loading user-controlled URL without validation
    func loadUserURL(urlString: String) {
        // FLAW: User input flows directly into URL loading
        if let url = URL(string: urlString) {
            let request = URLRequest(url: url)
            webView.load(request)  // Open redirect vulnerability
        }
    }

    // VULNERABLE: Deep link handling without validation
    func handleDeepLink(url: URL) {
        // FLAW: External URL opened without scheme/domain validation
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
              let redirectParam = components.queryItems?.first(where: { $0.name == "redirect" })?.value,
              let redirectURL = URL(string: redirectParam)
        else { return }

        webView.load(URLRequest(url: redirectURL))  // Unsafe redirect
    }

    // VULNERABLE: Universal Link with user-controlled destination
    func continueUserActivity(_ userActivity: NSUserActivity) {
        if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
           let url = userActivity.webpageURL,
           let redirectTo = URLComponents(url: url, resolvingAgainstBaseURL: false)?
               .queryItems?.first(where: { $0.name == "next" })?.value
        {
            // FLAW: Redirect to user-controlled URL
            if let destination = URL(string: redirectTo) {
                webView.load(URLRequest(url: destination))
            }
        }
    }

    // VULNERABLE: OAuth callback with unvalidated redirect_uri
    func handleOAuthCallback(redirectURI: String, code: String) {
        // FLAW: Redirect URI from query parameter not validated
        guard let url = URL(string: redirectURI) else { return }
        var components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
        components.queryItems = [URLQueryItem(name: "code", value: code)]

        if let finalURL = components.url {
            webView.load(URLRequest(url: finalURL))  // Open redirect
        }
    }
}

Vapor Server-Side Swift:

import Vapor

class VaporRedirectHandler {
    // VULNERABLE: Direct redirect to user input
    func handleRedirect(req: Request) throws -> Response {
        let redirectURL = req.query[String.self, at: "redirect"] ?? "/"

        // FLAW: User input directly used for redirection
        return req.redirect(to: redirectURL)  // Open redirect vulnerability
    }

    // VULNERABLE: Login redirect with unvalidated destination
    func loginRedirect(req: Request) async throws -> Response {
        let username = try req.content.decode(LoginData.self).username
        let nextPage = req.query[String.self, at: "next"] ?? "/dashboard"

        // Authenticate user...
        try await req.auth.login(username)

        // FLAW: Redirect to user-controlled destination after login
        return req.redirect(to: nextPage)  // Phishing risk
    }

    // VULNERABLE: API redirect endpoint
    func apiRedirect(req: Request) throws -> Response {
        guard let target = req.query[String.self, at: "target"] else {
            throw Abort(.badRequest)
        }

        // FLAW: No validation on redirect target
        var response = Response(status: .found)
        response.headers.replaceOrAdd(name: .location, value: target)
        return response
    }

    // VULNERABLE: Redirect with referer-based logic
    func refererRedirect(req: Request) throws -> Response {
        let referer = req.headers.first(name: .referer) ?? "/"

        // FLAW: Referer header is user-controlled
        return req.redirect(to: referer)
    }
}

struct LoginData: Content {
    let username: String
    let password: String
}

Foundation URL Handling:

import Foundation
import UIKit

class URLHandler {
    // VULNERABLE: URL construction from user input
    func constructRedirectURL(baseURL: String, userPath: String) -> URL? {
        // FLAW: User input concatenated to form URL
        let fullURLString = baseURL + userPath
        return URL(string: fullURLString)  // Open redirect if userPath is absolute
    }

    // VULNERABLE: UIApplication URL opening
    func openExternalURL(urlString: String) {
        // FLAW: User-controlled URL opened in browser
        if let url = URL(string: urlString) {
            UIApplication.shared.open(url) { success in
                if success {
                    print("URL opened successfully")
                }
            }
        }
    }

    // VULNERABLE: URLSession with user-controlled redirect
    func fetchAndRedirect(redirectParam: String, completion: @escaping (Data?) -> Void) {
        guard let url = URL(string: redirectParam) else {
            completion(nil)
            return
        }

        // FLAW: Following redirects to user-controlled URLs
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            completion(data)
        }
        task.resume()
    }
}

Remediation

If possible, avoid using (client-side) redirects and (server-side) forwards unless strictly necessary.

Otherwise, to mitigate Open Redirect vulnerabilities, apply these best practices:

  1. Whitelist URLs: Restrict redirection targets to a predefined list of trusted URLs. Only allow redirections to URLs that have been explicitly marked as safe or necessary for application functionality. Another option is to use a map of allowed URLs or domains and use an indirect reference from the request to choose a valid redirect URL from the map.

  2. Input Validation and Normalization: When constructing redirection URLs, validate and normalize the user inputs. Ensure the inputs conform to expected patterns, such as being a relative URL and not containing prohibited protocols or domains.

  3. Avoid Using sendRedirect for User-Controlled Paths: Prefer using server-side routing logic that does not involve dynamic user-generated paths or URLs for redirection purposes.

  4. Security Awareness and User Warnings: Inform users of potential risks when following redirects, and warn them against entering sensitive information on unfamiliar sites.

  5. Regular Security Audits and SAST: Conduct periodic security audits of the codebase and utilize SAST tools to detect and address Open Redirect vulnerabilities throughout the software development lifecycle.

By implementing these strategies, you can effectively reduce the risk of open redirect vulnerabilities, thereby safeguarding user interactions and maintaining the integrity of your application’s navigational logic.

Swift-Specific Open Redirect Prevention

URL Scheme and Domain Validation:

import WebKit
import Foundation

class SecureWebViewController: UIViewController {
    var webView: WKWebView!

    // List of allowed domains
    private let allowedDomains = [
        "example.com",
        "trusted-partner.com",
        "api.example.com"
    ]

    // List of allowed URL schemes
    private let allowedSchemes = ["http", "https"]

    // FIXED: Validate URL before loading
    func loadUserURL(urlString: String) {
        guard let url = URL(string: urlString),
              isURLAllowed(url)
        else {
            print("Rejected untrusted URL: \(urlString)")
            showError(message: "Invalid or untrusted URL")
            return
        }

        webView.load(URLRequest(url: url))
    }

    // URL validation with scheme and domain whitelist
    func isURLAllowed(_ url: URL) -> Bool {
        // Validate scheme
        guard let scheme = url.scheme?.lowercased(),
              allowedSchemes.contains(scheme)
        else {
            return false
        }

        // Validate domain against whitelist
        guard let host = url.host else {
            return false
        }

        return allowedDomains.contains { allowedDomain in
            host == allowedDomain || host.hasSuffix("." + allowedDomain)
        }
    }

    // FIXED: Deep link with validation
    func handleDeepLink(url: URL) {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
              let redirectParam = components.queryItems?.first(where: { $0.name == "redirect" })?.value,
              let redirectURL = URL(string: redirectParam),
              isURLAllowed(redirectURL)
        else {
            print("Invalid deep link redirect")
            return
        }

        webView.load(URLRequest(url: redirectURL))
    }

    // FIXED: Relative URL only
    func loadRelativeURL(path: String) {
        // Ensure path is relative (no scheme)
        guard !path.contains("://"),
              let baseURL = URL(string: "https://example.com"),
              let url = URL(string: path, relativeTo: baseURL)
        else {
            print("Invalid relative path")
            return
        }

        webView.load(URLRequest(url: url))
    }

    func showError(message: String) {
        let alert = UIAlertController(
            title: "Error",
            message: message,
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }
}

Vapor URL Validation:

import Vapor

class SecureVaporHandler {
    // Whitelist of allowed redirect destinations
    private let allowedRedirects: Set<String> = [
        "/dashboard",
        "/profile",
        "/settings",
        "/logout"
    ]

    // FIXED: Validate against whitelist
    func handleRedirect(req: Request) throws -> Response {
        let redirectURL = req.query[String.self, at: "redirect"] ?? "/"

        // Validate redirect URL against whitelist
        guard allowedRedirects.contains(redirectURL) else {
            throw Abort(.badRequest, reason: "Invalid redirect destination")
        }

        return req.redirect(to: redirectURL)
    }

    // FIXED: Login redirect with validation
    func loginRedirect(req: Request) async throws -> Response {
        let username = try req.content.decode(LoginData.self).username
        let nextPage = req.query[String.self, at: "next"] ?? "/dashboard"

        // Authenticate user
        try await req.auth.login(username)

        // Validate redirect is local path only
        guard isLocalPath(nextPage) else {
            return req.redirect(to: "/dashboard")
        }

        return req.redirect(to: nextPage)
    }

    // FIXED: Use indirect reference map
    func apiRedirect(req: Request) throws -> Response {
        guard let target = req.query[String.self, at: "target"] else {
            throw Abort(.badRequest)
        }

        // Map of allowed redirect identifiers
        let redirectMap: [String: String] = [
            "home": "/",
            "profile": "/user/profile",
            "settings": "/user/settings"
        ]

        guard let redirectURL = redirectMap[target] else {
            throw Abort(.badRequest, reason: "Invalid redirect target")
        }

        return req.redirect(to: redirectURL)
    }

    // Helper: Check if path is local (relative)
    private func isLocalPath(_ path: String) -> Bool {
        // Reject absolute URLs with scheme
        if path.contains("://") {
            return false
        }

        // Reject protocol-relative URLs
        if path.hasPrefix("//") {
            return false
        }

        // Ensure path starts with /
        if !path.hasPrefix("/") {
            return false
        }

        return true
    }
}

struct LoginData: Content {
    let username: String
    let password: String
}

Key Prevention Strategies:

  1. Whitelist allowed domains - Maintain explicit list of trusted domains

  2. Validate URL schemes - Only allow http/https, reject javascript:, data:, file:

  3. Use relative URLs - Prefer relative paths over absolute URLs for internal navigation

  4. Indirect reference maps - Map user input to predefined URLs instead of direct usage

  5. Local path validation - For server-side redirects, ensure paths are local and relative

  6. OAuth redirect_uri validation - Validate redirect_uri against registered callback URLs

  7. Reject protocol-relative URLs - URLs starting with // can bypass domain checks

  8. Deep link validation - Validate URL scheme and parameters in custom URL handlers

Example Extension for URL Validation:

import Foundation

extension URL {
    /// Checks if URL is safe to load (allowed scheme and domain)
    func isSafeToLoad(allowedDomains: [String], allowedSchemes: [String] = ["http", "https"]) -> Bool {
        // Check scheme
        guard let scheme = self.scheme?.lowercased(),
              allowedSchemes.contains(scheme)
        else {
            return false
        }

        // Check domain
        guard let host = self.host else {
            return false
        }

        return allowedDomains.contains { allowedDomain in
            host == allowedDomain || host.hasSuffix("." + allowedDomain)
        }
    }

    /// Checks if URL is a relative path (safe for internal navigation)
    func isRelativePath() -> Bool {
        return self.scheme == nil && self.host == nil
    }
}

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