CVE-2020-15679 (Swift, published 2022-12-22T20:15Z)
Description: value: An OAuth session fixation vulnerability existed in the VPN login flow, where an attacker could craft a custom login URL, convince a VPN user to login via that URL, and obtain authenticated access as that user. This issue is limited to cases where attacker and victim are sharing the same source IP and could allow the ability to view session states and disconnect VPN sessions. This vulnerability affects Mozilla VPN iOS 1.0.7 < (929), Mozilla VPN Windows < 1.2.2, and Mozilla VPN Android 1.1.0 < (1360).
Severity: nan (CVSS v3 base score: 7.6)
Weakness: CWE-384 \u2014 Session Fixation
Authenticating a user, or otherwise establishing a new user session, without invalidating any existing session identifier gives an attacker the opportunity to steal authenticated sessions.
Repository: https://github.com/mozilla-mobile/guardian-vpn-ios
Files changed:
FirefoxPrivateNetworkVPN.xcodeproj/project.pbxproj
FirefoxPrivateNetworkVPN/AppDelegate.swift
FirefoxPrivateNetworkVPN/Networking/GuardianAPI.swift
FirefoxPrivateNetworkVPN/Networking/GuardianURLRequest.swift
FirefoxPrivateNetworkVPN/Networking/GuardianURLRequestPath.swift
FirefoxPrivateNetworkVPN/Project Files/Bridging-Header.h	
FirefoxPrivateNetworkVPN/Project Files/Info.plist	
FirefoxPrivateNetworkVPN/Protocols/NetworkRequesting.swift
FirefoxPrivateNetworkVPN/Utilities/PKCECodeGenerator.swift
FirefoxPrivateNetworkVPN/ViewControllers/LoginViewController.swift
FirefoxPrivateNetworkVPNTests/Mocks/MockGuardianAPI.swift
Commit message: PGAND-410 Resolve oauth session fixation on iOS (#272)

* update login flow with PKCE

* fix MockGuardianAPI

Vulnerable code:
    func initiateUserLogin(completion: @escaping (Result<LoginCheckpointModel, GuardianAPIError>) -> Void) {
        let urlRequest = GuardianURLRequest.urlRequest(request: .login, type: .POST)
        networkLayer.fire(urlRequest: urlRequest) { result in
            DispatchQueue.main.async {
                completion(result.decode(to: LoginCheckpointModel.self))
            }
        }
    }

    func verify(urlString: String, completion: @escaping (Result<VerifyResponse, GuardianAPIError>) -> Void) {
        let urlRequest = GuardianURLRequest.urlRequest(with: urlString, type: .GET)
    private func headers(with token: String) -> [String: String] {
        return ["Authorization": "Bearer \(token)",
            "Content-Type": "application/json",
            "User-Agent": userAgentInfo]
    static func urlRequest(with urlString: String,
                           type: HttpMethod,
                           queryParameters: [String: String]? = nil,
                           httpHeaderParams: [String: String]? = nil,
                           body: Data? = nil) -> URLRequest {
        var urlComponent = URLComponents(string: urlString)!
        if let queryParameters = queryParameters {
            let queryItems = queryParameters.map { URLQueryItem(name: $0.key, value: $0.value) }
            urlComponent.queryItems = queryItems
        }

        var urlRequest = URLRequest(url: urlComponent.url!)
    case verify(String)
            return prefix + "login"
        case .verify(let token):
            return prefix + "login/verify/" + token
    func initiateUserLogin(completion: @escaping (Result<LoginCheckpointModel, GuardianAPIError>) -> Void)
    func verify(urlString: String, completion: @escaping (Result<VerifyResponse, GuardianAPIError>) -> Void)
    private var verificationURL: URL?
    private var verifyTimer: Timer?
    private var isVerifying = false
        guardianAPI.initiateUserLogin { [weak self] result in
            switch result {
            case .success(let checkpointModel):
                guard let loginURL = checkpointModel.loginUrl else { return }
                self?.verificationURL = checkpointModel.verificationUrl
                let safariViewController = SFSafariViewController(url: loginURL)
                DispatchQueue.main.async {
                    self?.addChild(safariViewController)
                    self?.view.addSubview(safariViewController.view)
                    safariViewController.view.frame = self?.view.bounds ?? CGRect.zero
                    safariViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
                    safariViewController.didMove(toParent: self)
                }
                safariViewController.delegate = self
                self?.safariViewController = safariViewController
            case .failure(let error):
                let loginError = error.getLoginError()
                let context: NavigableContext = loginError == .maxDevicesReached ? .maxDevicesReached : .error(loginError)
                self?.navigate(to: .landing, context: context)
            }
        }
    }

    deinit {
        verifyTimer?.invalidate()
    @objc private func verify() {
        guard let verificationURL = verificationURL else { return }
        if isVerifying { return }
        isVerifying = true
        guardianAPI.verify(urlString: verificationURL.absoluteString) { [weak self] result in
            guard let self = self else { return }
                DependencyManager.shared.accountManager.login(with: verification) { loginResult in
                    self.isVerifying = false
                    self.verifyTimer?.invalidate()
                    switch loginResult {
                    case .success:
                        self.navigate(to: .home)
                    case .failure(let error):
                        Logger.global?.log(message: "Authentication Error: \(error)")
                        let context: NavigableContext = error == .maxDevicesReached ? .maxDevicesReached : .error(error)
                        self.navigate(to: .landing, context: context)
                    }
                }
            case .failure:
                self.isVerifying = false
                return
}
// MARK: - SFSafariViewControllerDelegate
extension LoginViewController: SFSafariViewControllerDelegate {
    func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) {
        if didLoadSuccessfully && verifyTimer == nil {
            verifyTimer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(verify), userInfo: nil, repeats: true)
        self.verifyTimer?.invalidate()
    func initiateUserLogin(completion: @escaping (Result<LoginCheckpointModel, GuardianAPIError>) -> Void) {
        apiCallObservable.onNext(())
        guard let jsonUserURL = Bundle.main.url(forResource: "loginCheckpointModel", withExtension: "json"),
            let loginCheckpointModel = try? JSONDecoder().decode(LoginCheckpointModel.self, from: Data(contentsOf: jsonUserURL))
            else { return }
        completion(Result.success(loginCheckpointModel))
    }

    func verify(urlString: String, completion: @escaping (Result<VerifyResponse, GuardianAPIError>) -> Void) {

Fixed code:
		D54D31DD2519F33500D6ED57 /* PKCECodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54D31DC2519F33500D6ED57 /* PKCECodeGenerator.swift */; };
		D54D31DE2519F33500D6ED57 /* PKCECodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54D31DC2519F33500D6ED57 /* PKCECodeGenerator.swift */; };
		D54D31DC2519F33500D6ED57 /* PKCECodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PKCECodeGenerator.swift; sourceTree = "<group>"; };
				D54D31DC2519F33500D6ED57 /* PKCECodeGenerator.swift */,
				D54D31DD2519F33500D6ED57 /* PKCECodeGenerator.swift in Sources */,
				D54D31DE2519F33500D6ED57 /* PKCECodeGenerator.swift in Sources */,
    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
        if let sourceApplication = options[.sourceApplication] as? String,
            sourceApplication == "com.apple.SafariViewService" {
           NotificationCenter.default.post(name: .callbackURLNotification, object: nil,
                                           userInfo: ["callbackURL": url])
           return true
        }
        return false
    }

    func verify(code: String, codeVerifier: String, completion: @escaping (Result<VerifyResponse, GuardianAPIError>) -> Void) {
        let body: [String: Any] = ["code": code, "code_verifier": codeVerifier]
        guard let data = try? JSONSerialization.data(withJSONObject: body) else {
            completion(.failure(.couldNotEncodeData))
            return
        }

        let urlRequest = GuardianURLRequest.urlRequest(request: .verify, type: .POST, httpHeaderParams: headers(), body: data)
    private func headers(with token: String = "") -> [String: String] {
        var headers = ["Content-Type": "application/json",
                       "User-Agent": userAgentInfo]
        if !token.isEmpty {
            headers["Authorization"] = "Bearer \(token)"
        }
        return headers
    static private func urlRequest(with urlString: String,
                                   type: HttpMethod,
                                   queryParameters: [String: String]? = nil,
                                   httpHeaderParams: [String: String]? = nil,
                                   body: Data? = nil) -> URLRequest {
        let url = generateURL(urlString: urlString, queryParameters: queryParameters)
        var urlRequest = URLRequest(url: url!)

    static private func generateURL(urlString: String, queryParameters: [String: String]?) -> URL? {
        var urlComponent = URLComponents(string: urlString)!
        if let queryParameters = queryParameters {
            let queryItems = queryParameters.map { URLQueryItem(name: $0.key, value: $0.value) }
            urlComponent.queryItems = queryItems
        }

        return urlComponent.url
    }

    static func pkceLoginURL(codeChallenge: String) -> URL {
        let urlString = "\(GuardianURLRequest.baseURL)\(GuardianURLRequestPath.login.endpoint)"
        let queryParameters = ["code_challenge": codeChallenge, "code_challenge_method": "S256"]
        return generateURL(urlString: urlString, queryParameters: queryParameters)!
    }
    case verify
        let v2Prefix = "api/v2/vpn/"
            return v2Prefix + "login/ios"
        case .verify:
            return v2Prefix + "login/verify"
#import "CommonCrypto/CommonCrypto.h"
	<key>CFBundleURLTypes</key>
	<array>
		<dict>
			<key>CFBundleTypeRole</key>
			<string>Editor</string>
			<key>CFBundleURLName</key>
			<string>org.mozilla.ios.FirefoxVPN</string>
			<key>CFBundleURLSchemes</key>
			<array>
				<string>mozilla-vpn</string>
			</array>
		</dict>
	</array>
    func verify(code: String, codeVerifier: String, completion: @escaping (Result<VerifyResponse, GuardianAPIError>) -> Void)
//
//  PKCECodeGenerator
//  FirefoxPrivateNetworkVPN
//
//  This Source Code Form is subject to the terms of the Mozilla Public
//  License, v. 2.0. If a copy of the MPL was not distributed with this
//  file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
//  Copyright © 2020 Mozilla Corporation.
//

import Foundation

struct PKCECodeGenerator {
    static var generateCode: (codeChallenge: String, codeVerifier: String) {
        let codeVerifier = generateCodeVerifier()
        let codeChallenge = base64UrlEncode(sha256(string: codeVerifier))
        return (codeChallenge, codeVerifier)
    }

    private static func generateCodeVerifier() -> String {
        var buffer = [UInt8](repeating: 0, count: 32)
        _ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer)
        return base64UrlEncode(Data(buffer))
    }

    private static func base64UrlEncode(_ data: Data?) -> String {
        guard let data = data else { return "" }
        return data.base64EncodedString()
                   .replacingOccurrences(of: "+", with: "-")
                   .replacingOccurrences(of: "/", with: "_")
    }

    private static func sha256(string: String) -> Data? {
        guard let data = string.data(using: .utf8) else { return nil }
        var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
        data.withUnsafeBytes {
            _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
        }
        return Data(hash)
    }
}
extension Notification.Name {
    static let callbackURLNotification = Notification.Name("callbackURL")
}

    private let accountManager = DependencyManager.shared.accountManager
    private let PKCECode: (String, String) = PKCECodeGenerator.generateCode
    override func viewDidLoad() {
        super.viewDidLoad()
        let safariViewController = SFSafariViewController(url: GuardianURLRequest.pkceLoginURL(codeChallenge: PKCECode.0))
        addChild(safariViewController)
        view.addSubview(safariViewController.view)
        safariViewController.view.frame = self.view.bounds
        safariViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        safariViewController.didMove(toParent: self)
        safariViewController.delegate = self
        self.safariViewController = safariViewController

        NotificationCenter.default.addObserver(self, selector: #selector(handleCallback), name: .callbackURLNotification, object: nil)
    }
    @objc private func handleCallback(notification: Notification) {
        guard let url = notification.userInfo?["callbackURL"] as? URL,
            let queryItems = URLComponents(string: url.absoluteString)?.queryItems,
            let code = queryItems.first(where: { $0.name == "code" })?.value else {
            navigate(to: .landing)
            return
        }
        verify(code: code)
    }

    private func verify(code: String) {
        guardianAPI.verify(code: code, codeVerifier: PKCECode.1) { [weak self] result in
            guard let self = self else { return }
                self.login(verification: verification)
            case .failure(let error):
                self.navigate(to: .landing, context: .error(error))
    private func login(verification: VerifyResponse) {
        accountManager.login(with: verification) { [weak self] loginResult in
            guard let self = self else { return }
            switch loginResult {
            case .success:
                self.navigate(to: .home)
            case .failure(let error):
                Logger.global?.log(message: "Authentication Error: \(error)")
                let context: NavigableContext = error == .maxDevicesReached ? .maxDevicesReached : .error(error)
                self.navigate(to: .landing, context: context)
            }
}
// MARK: - SFSafariViewControllerDelegate
extension LoginViewController: SFSafariViewControllerDelegate {
    func verify(code: String, codeVerifier: String, completion: @escaping (Result<VerifyResponse, GuardianAPIError>) -> Void) {