Blog | Tristan Kernan

“That some of us should venture to embark on a synthesis of facts and theories, albeit with second-hand and incomplete knowledge of some of them – and at the risk of making fools of ourselves” (Erwin Schrödinger)

Security Stories Part 4

I am back once again with another entry from my archive of security stories. Once more, the purpose is education: as an engineer, it pays dividends to be curious and think critically about the software one is writing and maintaining.

Token interchange

One company I worked for leveraged magic links: the "click this link to log in" in an email that signs you in without needing to provide your password. The security basis for these links is the presumption that access to email justifies access to external accounts using the email. After all, if I control the email for [email protected], to access their account on foo.com I need only reset their password via email.

Let's set up a scenario so that the exploit path may be described: our imaginary company develops a job board, where organizations post job listings, and candidates apply, with their applications managed within the job board itself. The company decided to improve the candidate experience through magic links. To log in, an applicant is sent an email with a magic login link. After applying, an applicant is sent a magic link to access, update, or withdraw the application.

I had been suspicious of these magic links for some time, as any security minded engineer should be wary of non-standard authentication schemes. It was in reviewing a new feature related to magic links that I realized a core insecurity. First, some basic background about the implementation of magic links. They are typically of the form:

https://foo.com/login/magic/<token>

where <token> may be a randomly generated string mapping to a stored object (think cache or db), or a hash of an id (think hmac of user id), or an encoding of an id (think encrypting the id), or a combination (think jwt). This company followed the encoded id approach, that is, the <token> translated to an object id.

In studying the new feature, I made the connection that tokens could be used interchangeably between the two magic link systems, because the ids were not namespaced, i.e. encoded alongside the resource type. That meant a token to login user 1 could be used to access job application 1, and vice versa: an application magic link token for application 72 could be used to login as user 72.

The exploit lended itself to recursive scraping:

  1. create a new account, user N
  2. use user id N token to access application N
  3. access application N owner account, user M
  4. repeat step 2 with M

Given that an attacker may create many new user accounts, this means mass account takeover and data scraping. Ouch!

The key to securing these magic links was, as mentioned, namespacing the ids to prevent re-use. This looked like encoding the resource type alongside the object, in effect encoding the tuple (object id, resource type). For the user authentication tokens, that meant (<user id>, "user::login"), and for the application tokens, (<application id>, "application::crud").

Each consumer of tokens would check to ensure that the input token was of the appropriate resource to access the underlying functionality; e.g. the TokenLoginView would check that the token was of type "user::auth" before authenticating the given user.

As the magic links were part of a live system, with existing magic links in play, the migration from the old to the new token schema was a key challenge. In effect, the new token schema was rolled out alongside support for the previous, itself removed after an agreed-upon expiration timeline.

This vulnerability underlined the importance of the common phrase, "don't roll your own crypto." Here I'd amend that phrase to "don't roll your own authentication." As with all such phrases, it's not a commandment, but intended to share the common wisdom of warnings from the generations past that "here be dragons" when going down such paths.