Every professional community has its cryptic horror stories …
The story I’m about to tell is the one senior developers will tell interns on a dull winter late afternoon when the cold neon lights cast their shadows through an empty open space. More seriously this is the kind of software drama than can easily cost your company its reputation and you your career.
Everything starts with a good old ASP.Net like project using bullet proof out of the box forms authentication. Passwords are salted into network isolated databases, best security practices are implemented cautiously throughout, login page is carefully handcrafted, the authentication flow is resilient to timing attacks and the site is regularly tested by security experts. Rock solid we tell you!
Well, that was the case till this normal post patch morning where got reported cases of users ending up with the wrong identity while navigating the app. An identity swap , WHAT ? It’s as bad as if you’re browsing your Facebook feed when after clicking on a random notification icon you would end up impersonating your ex’s profile, having access to all their posts, pictures, instant chat, everything!! That’s really bad, isn’t it? Well hard to think about something worse… Impossible? No way such thing would happen you keep telling yourself. Well, that’s absolutely possible and even super easy to reproduce, how? Let’s dig in, shall we?
The good old cookie
Before delving into the more specific of this use case let’s reintroduce one of the pillar of the web security. The Auth. cookie.
While all the examples given comes from the Microsoft stem, all the principles remain the same across the different modern frameworks and I’m sure we could emulate this use case easily with different stacks.
The Auth. cookie is a dictionary of few properties related to the user identities that are encrypted server side using a private key (machine key set in the configuration file)
and then passed on in the HTTP Response header to the client.
Generally the Auth. cookie is attached to the HTTP Response after the login flow succeeds and is then stored in the client’s browser to the specific domain.
This token remains valid for a given period of time (generally configurable) and is passed alongside every request in the HTTP header.
This cookie is decrypted by the web app which checks its validity and accesses the pieces of information encapsulated (generally some information regarding the Identity) and then performs all the set of authorization filtering.
When the cookie expires, the web server will naturally rejects the request. Most web sites implement some sort of cookie sliding expiration technique which causes a fresh cookie to be reissued if the user stays active.
If configured in ASP.Net a new cookie is issued once the initial cookie is half-expired.
The fatal Pull-Request
Let’s assume that your web application contains some static content _ images, user documentation, marketing material_ which used to be publicly accessible and, for some business reasons, it’s been asked to our fellow developers to restrict this content only to the authenticated users.
There are many ways this can be achieved in ASP.Net / IIS, one way could be to remove the precondition in the ASP.Net URL Authorization HTTP modules
<add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" preCondition="managedHandler" />
and tells specifically ASP.Net to consider the static content as restricted to the sole authenticated users
From this point on, every single request to the resources within the Images folder will go through the standard HTTP authorization pipeline… including the cookie setting process.
The assassin reverse proxy
So what? Well you’re right that at this stage having the authorization layer on top of the static content isn’t a drama and fit the business requirement.
Sure, it’s not yet a bloodbath but we’re getting closer… and the final hit will come with the last piece of the puzzle … our loadbalancer
Let’s assume that our load balancer / reverse proxy is duly configured to interpret the cache-control header of our HTTP response and serves appropriately cached resources to our client.
This is a quite frequent behavior which can improve the performance of the application by reducing the traffic in the DMZ.
There are 3 main modes for cache-control attribute:
- public : the resource at a given URL is allowed to be cached and served to all users
- private: the resource at a given URL is cached per user
- no-cache: the resource will be fetched from the web server all the time
Our static content, being static, is naturally eligible to be cached and we’ve set our application to place the right header for the static resources.
There are multiple ways IIS would place the cache-control header onto the HTTP response.
Note that the HTTP caching doesn’t occur solely at the WebServer/Load balancer level, the client’s browser equally interpret the caching tags and fetches the resources only when necessary.
More information on the HTTP caching with this excellent article
“Le coup de grace”
Now we have everything for our disaster at scale, we just need to wait for having the right configuration which will ineluctably happen:
- An authenticated user Bob navigates through the application
- At some stage Bob sends a request to fetch a specific static resource A. His half expired cookie is placed normally in his HTTP header
- The web servers identifies that the cookie has to be renewed and attaches the new cookie to the response
- Let’s assume that the reverse proxy doesn’t have the static resource in cache anymore _ a regular cache expiration having been set, it will save the HTTP response ready to be served when another client _ INCLUDING Bob’s cookie
- Another user Alice is equally navigating the application and has previously received the static resource A alongside its caching attribute (cache control, ETag and whatnot). Alice’s browser might not request the resource A for a little while but…
- After a few minutes, Alice request a page containing resource A, the request hits the reverse proxy which returns the cached response alongside Bob’s cookie.
- Alice’s browser receives the new cookie and if its expiry is further down the line than Alice’s then will update the cookie globally for this domain.
- From now on, every request Alice will make to the application will effectively return Bob’s data.
- Alice takes screenshots of the application gracefully serving Bob’s sensitive information and posts everything on the internet… get ready for a not quite so fun moment with your boss.
Everyone dies at the end ?
I swear no developer has been hurt during the redaction of this post !
While fictional for the most part, this article outlines how intricate things can get in modern software architectures. Taken in isolation, the different layers might behave accordingly to their specs but put together you can end up building a scaffold to hang yourself with if you don’t see the big picture of your system as well as intimately understand the key concepts of the frameworks you’re utilizing.
This is surely hard and building apps mostly web public facing services can get tricky as there are a lot of different moving parts in there.
While really extreme I hope this example shows how quickly things can conspire to crush the best possible design. It is beyond good or bad teams, best practices or custom stuff, bad things happen and we shall envisage this with constant humility.