CVE-2020-15679 (C#, 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-services/guardian-vpn-windows
Files changed:
installer/MozillaVPN.wxs
ui/src/Firefox Private Network.csproj	
ui/src/FxA/Account.cs
ui/src/FxA/Devices.cs
ui/src/FxA/Login.cs
ui/src/FxA/LoginSessionManager.cs
ui/src/FxA/ServerList/ServerList.cs
ui/src/JSONStructures/Login/FxALoginURLs.cs
ui/src/Main.cs
ui/src/Manager.cs
ui/src/ProductConstants.cs
ui/src/UI/Components/Card/Hero/HeroSubtext.xaml
ui/src/UI/Resources/Localization/Translations/en-us.ftl
ui/src/UI/Views/Settings/SettingsView.xaml
ui/src/ViewModels/MainWindowViewModel.cs
ui/src/WCF/IService.cs
ui/src/WCF/Service.cs
ui/src/app.manifest
ui/src/packages.config
Commit message: New non-polling authentication flow.

- Fixes intermittent hanging during auth
- Implements PKCE


Vulnerable code:
		<!--
            Manager.Account.Config = new FxA.Config(fxaJson);
            Manager.Account.LoginState = FxA.LoginState.LoggedIn;
            var devices = new FxA.Devices();
            var deviceName = string.Format("{0} ({1} {2})", System.Environment.MachineName, System.Environment.OSVersion.Platform, System.Environment.OSVersion.Version);
            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, "/vpn/account", RestSharp.Method.GET);
            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, "/vpn/device", RestSharp.Method.POST);
            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, "/vpn/device/" + sanitizedDevicePublicKey, RestSharp.Method.DELETE);
        /// Gets the unique login urls for the user's signin attempt.
        /// <returns>A <see cref="JSONStructures.FxALoginURLs"/> object.</returns>
        public JSONStructures.FxALoginURLs GetLoginURLs()
        {
            var api = new ApiRequest(string.Empty, "/vpn/login", RestSharp.Method.POST);
            JSONStructures.FxALoginURLs loginURLs;

            // Execute the request
            var response = api.SendRequest();

            if (response == null || response.StatusCode != System.Net.HttpStatusCode.OK)
            {
                ErrorHandling.ErrorHandler.Handle(new ErrorHandling.UserFacingMessage("toast-login-url-retrieval-error"), ErrorHandling.UserFacingErrorType.None, ErrorHandling.LogLevel.Error);
                return null;
            }

            try
            {
                loginURLs = JsonConvert.DeserializeObject<JSONStructures.FxALoginURLs>(response.Content);
                return loginURLs;
            }
            catch (Exception e)
            {
                ErrorHandling.ErrorHandler.Handle(e, ErrorHandling.LogLevel.Error);
            }

            return null;
        }
        /// Queries the verification URL to check if the user has logged in.
        /// <param name="tokenURL">Login verification URL.</param>
        /// <returns>Returns the response content if successful, otherwise returns null.</returns>
        public string QueryRawLoginState(string tokenURL)
            var api = new ApiRequest(string.Empty, tokenURL, RestSharp.Method.GET);
            // Execute the request
            var response = api.SendRequest();
            if (response == null || response.StatusCode != System.Net.HttpStatusCode.OK)
            {
                ErrorHandling.ErrorHandler.Handle("User has not logged in yet", ErrorHandling.LogLevel.Debug);
                return null;
            }
            return response.Content;
        }
        /// <summary>
        /// Deserializes the FxA login response from JSON.
        /// </summary>
        /// <param name="jsonContents">FxA login response.</param>
        /// <returns>A <see cref="JSONStructures.FxALogin"/> object.</returns>
        public JSONStructures.FxALogin ParseLoginState(string jsonContents)
        {
                var loginData = JsonConvert.DeserializeObject<JSONStructures.FxALogin>(jsonContents);
                return loginData;
        /// <param name="cancelToken">Token used to cancel the login process.</param>
        public bool StartLogin(CancellationToken cancelToken)
                var loginURLs = GetLoginURLs();
                if (loginURLs == null)
                var pollInterval = loginURLs.PollInterval % 31; // Max 30 seconds, no more
                Manager.Account.LoginState = FxA.LoginState.LoggingIn;
                StartQueryLoginThread(loginURLs.VerificationUrl, loginURLs.PollInterval, loginURLs.ExpiresOn, cancelToken);
                // Launch a browser
                OpenBrowser(loginURLs.LoginUrl);
                // Navigate to verification page
                UI.MainWindow mainWindow = (UI.MainWindow)Application.Current.MainWindow;
                mainWindow.NavigateToView(new FirefoxPrivateNetwork.UI.VerifyAccountView(loginURLs.LoginUrl), UI.MainWindow.SlideDirection.Left);
        /// Intiates the login verification thread.
        /// <param name="queryUri">Login verification URL.</param>
        /// <param name="timeoutSeconds">Timeout (secs) between verification query attempts.</param>
        /// <param name="expiresAt">Expiration date of the verification URL.</param>
        /// <param name="cancelToken">Token used to cancel the login thread.</param>
        public void StartQueryLoginThread(string queryUri, int timeoutSeconds, DateTime expiresAt, CancellationToken cancelToken)
            var loginThread = new Thread(() => QueryLoginThread(queryUri, timeoutSeconds, expiresAt, cancelToken))
                IsBackground = true,
            };
            loginThread.Start();
        /// Polls a verification URL periodically to confirm if the user has logged in.
        /// <param name="queryUri">Login verification URL.</param>
        /// <param name="timeoutSeconds">Timeout (secs) between verification query attempts.</param>
        /// <param name="expiresAt">Expiration date of the verification URL.</param>
        /// <param name="cancelToken">Token used to cancel the login thread.</param>
        private void QueryLoginThread(string queryUri, int timeoutSeconds, DateTime expiresAt, CancellationToken cancelToken)
            while (Manager.Account.LoginState == FxA.LoginState.LoggingIn && DateTime.Compare(DateTime.UtcNow, expiresAt) < 0)
            {
                try
                {
                    var queryRawData = QueryRawLoginState(queryUri);
                    if (cancelToken.IsCancellationRequested)
                    {
                        break;
                    }
                    if (queryRawData == null)
                    {
                        Thread.Sleep(TimeSpan.FromSeconds(timeoutSeconds));
                        continue;
                    }
                    var queryData = ParseLoginState(queryRawData);
                    if (queryData.User == null || queryData.User.Subscriptions == null || queryData.User.Subscriptions.Vpn == null)
                    {
                        Manager.Account.LoginState = LoginState.LoggedOut;
                        break;
                    }
                    if (!queryData.User.Subscriptions.Vpn.Active)
                    {
                        Manager.Account.LoginState = LoginState.LoggedOut;
                        break;
                    }
                    var processLoginResult = Manager.Account.ProcessLogin(queryRawData);
                    Manager.Account.LoginState = LoginState.LoggedIn;
                    Manager.StartUIUpdaters();
                    var maxDevicesReached = !processLoginResult && Manager.Account.Config.FxALogin.User.Devices.Count() >= Manager.Account.Config.FxALogin.User.MaxDevices;
                    Cache.FxAServerList.RetrieveRemoteServerList();
                    Application.Current.Dispatcher.Invoke(() =>
                        var owner = Application.Current.MainWindow;
                        if (owner != null)
                            if (!Manager.MustUpdate)
                            {
                                if (maxDevicesReached)
                                {
                                    ((UI.MainWindow)owner).NavigateToView(new UI.DevicesView(deviceLimitReached: true, fxaJson: queryRawData), UI.MainWindow.SlideDirection.Left);
                                }
                                else
                                {
                                    ((UI.MainWindow)owner).NavigateToView(new UI.OnboardingView5(), UI.MainWindow.SlideDirection.Left);
                                }
                            }

                            ((UI.MainWindow)owner).Show();
                            ((UI.MainWindow)owner).WindowState = WindowState.Normal;
                            ((UI.MainWindow)owner).Activate();
                    });
                }
                catch (Exception e)
                {
                    ErrorHandling.ErrorHandler.Handle(e, ErrorHandling.LogLevel.Debug);
                Thread.Sleep(TimeSpan.FromSeconds(5));

            LoginResultEvent?.Invoke(this, this, Manager.Account.LoginState);
                if (!login.StartLogin(tokenSource.Token))
            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, "/vpn/servers", Method.GET);

        /// <summary>
        /// Gets or sets the expiration date/time for the URLs.
        /// </summary>
        [JsonProperty("expires_on")]
        public DateTime ExpiresOn { get; set; }

        /// <summary>
        /// Gets or sets the recommended poll interval for the verification URL.
        /// </summary>
        [JsonProperty("poll_interval")]
        public int PollInterval { get; set; }
    internal class Entry : System.Windows.Application
        /// <summary>
        /// Global value that is used to indicate if there is already an instance of the application running.
        /// </summary>
        private static readonly Mutex RunOnceMutex = new Mutex(false, string.Concat(@"Local\", ProductConstants.GUID));

            if (args.Count() == 1)
            if (!RunOnceMutex.WaitOne(TimeSpan.Zero, true))
                // Already running, attempt to send a "show" command to the already running process before exiting
                var runningWindow = User32.FindWindow(ProductConstants.TrayWindowClassName, string.Empty);
                if (runningWindow != IntPtr.Zero)
                    User32.SendMessage(runningWindow, User32.WmShow, IntPtr.Zero, string.Empty);

                Environment.Exit(1);
                Manager.Initialize();
        public static void Initialize()
            InitializeViewModels();
        public static void InitializeViewModels()
            MainWindowViewModel = new ViewModels.MainWindowViewModel();
            FxAUrl = string.Concat(BaseUrl, "/api/v1");
            FxALoginUrl = string.Concat(FxAUrl, "/vpn/login");
                        <!-- Connection unsable -->
devices-remove-popup-content = Please confirm you would like to remove {$deviceName}.
                        <CheckBox Margin="18" Content="{Binding Path=[settings-auto-launch]}" Click="RunOnStartup_Click">
        public MainWindowViewModel()
            if (Manager.Account.LoginState == FxA.LoginState.LoggedIn && currentDevice != null)
        /// <summary>
        /// Login stub.
        /// </summary>
        /// <param name="req">Login request object.</param>
        /// <returns>WCF response.</returns>
        [OperationContract]
        [WebInvoke(Method = "POST", ResponseFormat = WebMessageFormat.Json, RequestFormat = WebMessageFormat.Json)]
        Response Login(LoginRequest req);

        /// <summary>
        /// Initiate login.
        /// </summary>
        /// <param name="req">LoginRequest WCF command object.</param>
        /// <returns>WCF response.</returns>
        public Response Login(LoginRequest req)
        {
            try
            {
                var loginInstance = new FxA.Login();
                var pollInterval = req.PollInterval % 31; // Max 30 seconds, no more
                Manager.Account.LoginState = FxA.LoginState.LoggingIn;
                loginInstance.StartQueryLoginThread(req.VerificationUrl, req.PollInterval, req.ExpiresOn, CancellationToken.None);
                return new Response(200, "Success");
            }
            catch (Exception ex)
            {
                return new Response(500, ex.Message);
            }
        }

        <requestedExecutionLevel level="asInvoker" uiAccess="false" />

Fixed code:
        <!-- Custom URL protocol for handling the PKCE auth flow -->
        <RegistryKey Root="HKCR" Key="mozilla-vpn">
          <RegistryValue Type="string" Name="URL Protocol" Value="mozilla-vpn"/>
          <RegistryValue Type="string" Value="URL:mozilla-vpn Protocol"/>
          <RegistryKey Key="DefaultIcon">
            <RegistryValue Type="string" Value="MozillaVPN.exe" />
          </RegistryKey>
          <RegistryKey Key="shell\open\command">
            <RegistryValue Type="string" Value="&quot;C:\Program Files\Mozilla\Mozilla VPN\MozillaVPN.exe&quot; &quot;%1&quot;" />
          </RegistryKey>
        </RegistryKey>

    <!--
    <Reference Include="Microsoft.IdentityModel.Logging, Version=6.7.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
      <HintPath>..\packages\Microsoft.IdentityModel.Logging.6.7.1\lib\net461\Microsoft.IdentityModel.Logging.dll</HintPath>
    </Reference>
    <Reference Include="Microsoft.IdentityModel.Tokens, Version=6.7.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
      <HintPath>..\packages\Microsoft.IdentityModel.Tokens.6.7.1\lib\net461\Microsoft.IdentityModel.Tokens.dll</HintPath>
    </Reference>
        /// <summary>
        /// Gets or sets the API version for making non-PKCE auth requests and handling responses.
        /// </summary>
        public static string OriginalApiVersion { get; set; } = "/api/v1";

            Manager.Account.Config = new Config(fxaJson);
            Manager.Account.LoginState = LoginState.LoggedIn;
            var devices = new Devices();
            var deviceName = string.Format("{0} ({1} {2})", Environment.MachineName, Environment.OSVersion.Platform, Environment.OSVersion.Version);
            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, $"{ProductConstants.BaseUrl}{OriginalApiVersion}/vpn/account", RestSharp.Method.GET);
using System.IO;
            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, $"{ProductConstants.BaseUrl}{Account.OriginalApiVersion}/vpn/device", RestSharp.Method.POST);
            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, $"{ProductConstants.BaseUrl}{Account.OriginalApiVersion}/vpn/device/" + sanitizedDevicePublicKey, RestSharp.Method.DELETE);
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Security.Permissions;
using System.Text;
using System.Web;
using Microsoft.IdentityModel.Tokens;
using RestSharp.Extensions;
        private static string codeVerifierPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
        private static string codeVerifierFile = "moz_gen_cvf.txt";

        /// Gets or sets the API version for making PKCE auth requests and handling responses.
        public static string ApiVersion { get; set; } = "/api/v2";
        /// Gets the unique login urls for the user's signin attempt.
        /// <returns>A <see cref="JSONStructures.FxALoginURLs"/> object.</returns>
        public JSONStructures.FxALoginURLs GetLoginURL()
            // Generate code_verifier
            string codeVerifier = GetUniqueToken(44);
            // Save the code_verifier for use in the verify request
            File.WriteAllText(Path.Combine(codeVerifierPath, codeVerifierFile), codeVerifier);
            // Generate the code_challenge by getting the SHA256 hash and encoding it to send in a request
            // Switched the code_challenge_method to plain since there were intermittent issues with the SHA256 method returning the user data each time.
            string codeChallenge = codeVerifier;
            // Make new instance of object that hold the URLs needed for a user to login
            JSONStructures.FxALoginURLs loginURLs = new JSONStructures.FxALoginURLs();
                // The codeChallenge is there to handle the PKCE request to the server
                loginURLs.LoginUrl = $"{ProductConstants.BaseUrl}{ApiVersion}/vpn/login/windows?code_challenge={codeChallenge}&code_challenge_method=plain";

                return loginURLs;
        public bool StartLogin()
                // Get the login and verify URLs
                JSONStructures.FxALoginURLs loginURL = GetLoginURL();

                if (loginURL == null)
                // Create the GET request for PKCE auth
                ApiRequest api = new ApiRequest(string.Empty, loginURL.LoginUrl, RestSharp.Method.GET);
                // Execute the request
                var response = api.SendRequest();
                if (response == null || response.StatusCode != System.Net.HttpStatusCode.OK)
                {
                    ErrorHandling.ErrorHandler.Handle(new ErrorHandling.UserFacingMessage("toast-login-url-retrieval-error"), ErrorHandling.UserFacingErrorType.None, ErrorHandling.LogLevel.Error);
                    return false;
                }

                Manager.Account.LoginState = LoginState.LoggingIn;

                // Launch a browser
                OpenBrowser(loginURL.LoginUrl);
        /// Gets verification code from mozilla-vpn:// redirect after the user logs in through the browser.
        /// <param name="code">Verification code from the custom URL protocol redirect after logging in.</param>
        public void VerifyUserLogin(string code)
            // Make the POST request to the verify endpoint
            ApiRequest api = new ApiRequest(string.Empty, $"{ProductConstants.BaseUrl}{ApiVersion}/vpn/login/verify", RestSharp.Method.POST);

            // Get code_verifier that was generated in the login request
            string code_verifier = File.ReadAllText(Path.Combine(codeVerifierPath, codeVerifierFile));

            // Create a new dictionary to hold the code and code_verifier.
            Dictionary<string, string> postBody = new Dictionary<string, string>();
            postBody.Add("code", code);
            postBody.Add("code_verifier", code_verifier);
            api.AddPostBody(postBody);

            // Execute the request
            var response = api.SendRequest();

            if (response == null || response.StatusCode != System.Net.HttpStatusCode.OK)
                ErrorHandling.ErrorHandler.Handle(new ErrorHandling.UserFacingMessage("toast-login-url-retrieval-error"), ErrorHandling.UserFacingErrorType.None, ErrorHandling.LogLevel.Error);
                return;
            }

            // Check login credentials, add the session info to the settings, and add a new device
            Manager.Account.ProcessLogin(response.Content);
        /// Generates a Base64 encoded string based on a SHA256 hash with the string it's given.
        /// <param name="text">The text that needs to be converted to a SHA256 hash.</param>
        /// <returns>A Base64 encoded SHA256 hash.</returns>
        private static string GetHashSha256(string text)
            SHA256Managed hashTool = new SHA256Managed();
            byte[] phraseAsByte = Encoding.UTF8.GetBytes(string.Concat(text));
            byte[] encryptedBytes = hashTool.ComputeHash(phraseAsByte);
            hashTool.Clear();
            string value = Convert.ToBase64String(encryptedBytes);
            return value;
        }
        /// <summary>
        /// Generates a random cryptographic string to use in a PKCE auth request and response.
        /// </summary>
        /// <param name="length">The number of characters that should be in the string.</param>
        /// <param name="chars">The acceptable characters that can be in the string.</param>
        /// <returns>A cryptographic string.</returns>
        private static string GetUniqueToken(int length, string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-_")
        {
            using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider())
            {
                byte[] data = new byte[length];
                // If chars.Length isn't a power of 2 then there is a bias if we simply use the modulus operator. The first characters of chars will be more probable than the last ones.
                // buffer used if we encounter an unusable random byte. We will regenerate it in this buffer.
                byte[] buffer = null;
                // Maximum random number that can be used without introducing a bias
                int maxRandom = byte.MaxValue - ((byte.MaxValue + 1) % chars.Length);
                crypto.GetBytes(data);

                char[] result = new char[length];

                for (int i = 0; i < length; i++)
                {
                    byte value = data[i];

                    while (value > maxRandom)
                        if (buffer == null)
                            buffer = new byte[1];

                        crypto.GetBytes(buffer);
                        value = buffer[0];
                    }

                    result[i] = chars[value % chars.Length];
                return new string(result);
                if (!login.StartLogin())
            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, $"{ProductConstants.BaseUrl}{Account.OriginalApiVersion}/vpn/servers", Method.GET);
using System.Collections.Generic;
using System.Diagnostics;
    internal class Entry : Application
            List<Process> processes = Process.GetProcessesByName(Process.GetCurrentProcess().ProcessName).ToList();

            if (args.Count() == 1 && !args[0].Contains("mozilla-vpn:"))
            if (processes.Count > 1)
                processes.Sort((x, y) => DateTime.Compare(y.StartTime, x.StartTime));

                if (processes.Count > 2)
                    for (int i = 1; i <= processes.Count - 2; i++)
                    {
                        processes[i].Kill();
                    }
                // Run this if the app is opened via the custom URL protocol with a code
                if (args.Count() == 1 && args[0].Contains("mozilla-vpn:"))
                {
                    // Get the code string that's returned from the server to be used in the verification request
                    Manager.Initialize(args.First());
                    VerifyUser(args.First());
                }
                else
                {
                    Manager.Initialize();
                }

        /// <summary>
        /// Starts the WireGuard tunnel service.
        /// </summary>
        /// <param name="args">
        /// Argument 0: "tunnel" keyword
        /// Argument 1: Path to the configuration file.
        /// </param>
        private static void VerifyUser(string args)
        {
            try
            {
                string code = args.Substring(args.IndexOf("code=") + 5);

                if (code != null && code.Length >= 44)
                {
                    FxA.Login verifyUser = new FxA.Login();

                    verifyUser.VerifyUserLogin(code);
                }
            }
            catch (Exception e)
            {
                ErrorHandling.ErrorHandler.Handle(e, ErrorHandling.LogLevel.Error);
                Environment.Exit(1);
            }
        }
        /// <param name="code">Optional verification code.</param>
        public static void Initialize(string code = "")
            InitializeViewModels(code);
        /// <param name="code">Optional verification code.</param>
        public static void InitializeViewModels(string code = "")
            MainWindowViewModel = new ViewModels.MainWindowViewModel(code);
            VersionUpdater.StopTask();
            ToastManager.StopThread();
            FxAUrl = string.Concat(BaseUrl, "/api/v2");
            FxALoginUrl = string.Concat(FxAUrl, "/vpn/login/windows");
                        <!-- Connection unstable -->
devices-remove-popup-content = Please confirm you would like to remove {$deviceName}. You will need to open the VPN client on the removed device to clear your network settings and access the internet again.
                        <CheckBox Margin="18" Content="{Binding Path=[settings-auto-launch]}" Click="RunOnStartup_Click" Visibility="Hidden">
        /// <param name="code">Optional verification code.</param>
        public MainWindowViewModel(string code)
            if (code.Length >= 44)
            {
                InitialViewFrameSourceType = typeof(UI.OnboardingView5);
            }
            else if (Manager.Account.LoginState == FxA.LoginState.LoggedIn && currentDevice != null)
        <requestedExecutionLevel level="highestAvailable" uiAccess="false" />
        <!--<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />-->
  <package id="Microsoft.IdentityModel.Logging" version="6.7.1" targetFramework="net472" />
  <package id="Microsoft.IdentityModel.Tokens" version="6.7.1" targetFramework="net472" />