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:
-
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.
-
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.
-
Avoid Using
sendRedirectfor User-Controlled Paths: Prefer using server-side routing logic that does not involve dynamic user-generated paths or URLs for redirection purposes. -
Security Awareness and User Warnings: Inform users of potential risks when following redirects, and warn them against entering sensitive information on unfamiliar sites.
-
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:
-
Whitelist allowed domains - Maintain explicit list of trusted domains
-
Validate URL schemes - Only allow http/https, reject javascript:, data:, file:
-
Use relative URLs - Prefer relative paths over absolute URLs for internal navigation
-
Indirect reference maps - Map user input to predefined URLs instead of direct usage
-
Local path validation - For server-side redirects, ensure paths are local and relative
-
OAuth redirect_uri validation - Validate redirect_uri against registered callback URLs
-
Reject protocol-relative URLs - URLs starting with
//can bypass domain checks -
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
-
CWE-601 : URL Redirection to Untrusted Site ('Open Redirect').
-
OWASP - Top 10 2021 Category A01 : Broken Access Control.
-
Unvalidated Redirects and Forwards Cheat Sheet, in OWASP Cheat Sheet Series.