---
title: Validate Universal Link và URL Scheme để tránh open redirect và hijacking
impact: HIGH
impactDescription: Xử lý deep link không validate cho phép attacker craft URL độc hại dẫn user đến webview chứa phishing page, hoặc trigger sensitive actions như logout/approve bằng custom URL scheme.
tags: swift, ios, deeplink, universal-link, url-scheme, open-redirect, hijacking, security
---

## Validate Universal Link và URL Scheme để tránh open redirect và hijacking

Custom URL scheme (`myapp://`) có thể bị hijack bởi app khác. Universal Links an toàn hơn nhưng vẫn cần validate parameters. Không bao giờ render URL từ deep link trực tiếp trong WKWebView hay navigate đến destination không thuộc domain whitelist.

**Incorrect (không validate deep link destination):**

```swift
import UIKit

class SceneDelegate: UIResponder, UISceneDelegate {

    // !! Xử lý universal link - mở URL tùy ý trong webview
    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
        guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
              let incomingURL = userActivity.webpageURL else { return }

        // !! Lấy "redirect" param từ URL và mở trong WebView không validate
        let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: false)
        let redirectURL = components?.queryItems?.first(where: { $0.name == "redirect" })?.value ?? ""
        openWebView(urlString: redirectURL)  // Open redirect! Phishing!
    }

    // !! Custom scheme - trigger hành động nhạy cảm không xác nhận
    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
        // myapp://approve?transactionId=123
        if url.host == "approve" {
            let transactionId = url.queryParameters["transactionId"] ?? ""
            approveTransaction(id: transactionId)  // Trigger action không confirm!
        }
        return true
    }
}
```

**Correct (validate source, destination và require user confirmation):**

```swift
import UIKit

struct DeepLinkValidator {
    // Whitelist domain cho redirect
    private static let allowedRedirectHosts: Set<String> = [
        "app.example.com",
        "www.example.com",
        "help.example.com"
    ]

    // Validate redirect URL chỉ đến domain của mình
    static func validateRedirectURL(_ urlString: String) throws -> URL {
        guard let url = URL(string: urlString),
              let host = url.host,
              url.scheme == "https" else {
            throw DeepLinkError.invalidURL(urlString)
        }
        guard allowedRedirectHosts.contains(host) else {
            throw DeepLinkError.untrustedHost(host)
        }
        return url
    }

    // Validate transaction ID là UUID format
    static func validateTransactionId(_ id: String) throws -> UUID {
        guard let uuid = UUID(uuidString: id) else {
            throw DeepLinkError.invalidParameter("transactionId must be UUID")
        }
        return uuid
    }
}

enum DeepLinkError: LocalizedError {
    case invalidURL(String), untrustedHost(String), invalidParameter(String)

    var errorDescription: String? {
        switch self {
        case .invalidURL(let u): return "Invalid URL: \(u)"
        case .untrustedHost(let h): return "Untrusted host: \(h)"
        case .invalidParameter(let p): return "Invalid parameter: \(p)"
        }
    }
}

class SceneDelegate: UIResponder, UISceneDelegate {

    // SAFE: Validate trước khi open webview
    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
        guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
              let incomingURL = userActivity.webpageURL else { return }

        let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: false)
        let rawRedirect = components?.queryItems?.first(where: { $0.name == "redirect" })?.value ?? ""

        do {
            let safeURL = try DeepLinkValidator.validateRedirectURL(rawRedirect)
            openWebView(url: safeURL)  // Đã validate
        } catch {
            logger.warning("Rejected deep link redirect: \(error.localizedDescription)")
            // Không navigate, hoặc mở trang default thay thế
        }
    }

    // SAFE: Require user confirmation trước action nhạy cảm từ URL scheme
    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
        guard url.scheme == "myapp" else { return false }

        if url.host == "approve" {
            do {
                let rawId = url.queryParameters["transactionId"] ?? ""
                let transactionId = try DeepLinkValidator.validateTransactionId(rawId)
                // SAFE: Hiển thị confirmation alert trước
                showApprovalConfirmation(transactionId: transactionId)
            } catch {
                logger.warning("Invalid approve deep link: \(error.localizedDescription)")
            }
        }
        return true
    }

    private func showApprovalConfirmation(transactionId: UUID) {
        let alert = UIAlertController(
            title: "Confirm Transaction",
            message: "Approve transaction \(transactionId.uuidString.prefix(8))...?",
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "Approve", style: .default) { _ in
            self.approveTransaction(id: transactionId)
        })
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        topViewController?.present(alert, animated: true)
    }
}
```

**Tools:** OWASP MASVS-PLATFORM-1, Apple App Review Guidelines (2.5.9), URLComponents, Proxyman
