Authenticating to a website is something most of us probably do multiple times every day. Just looking at my open tabs right now I’ve got Facebook, Stack Overflow, Bit.ly, Hotmail, YouTube and a couple of non-technology forums all active, each one individually authenticated to.
In each case I trust the site to appropriately secure both my current session and any persistent data – such as credentials – but beyond observing whether an SSL certificate is present, I have very little idea of how the site implements authentication and session management. At least not without doing the kind of digging your average user is never going to get involved in.
In some instances, such as with Stack Overflow, an authentication service such as OpenID is used. This is great for the user as it reuses an existing account with an open service meaning you’re not creating yet another online account and it’s also great for the developer as the process of authentication is hived off to an external service.
However, the developer still needs to take care of authorisation to internal application assets and they still need to persist the authenticated session in a stateless environment so it doesn’t get them entirely out of the woods.
Defining broken authentication and session management
Again with the OWASP definition:
Application functions related to authentication and session management are often not implemented correctly, allowing attackers to compromise passwords, keys, session tokens, or exploit other implementation flaws to assume other users’ identities.
And the usual agents, vectors, weaknesses and impacts bit:
|Security Weakness||Technical |
|Consider anonymous external attackers, as well as users with their own accounts, who may attempt to steal accounts from others. Also consider insiders wanting to disguise their actions.||Attacker uses leaks or flaws in the authentication or session management functions (e.g., exposed accounts, passwords, session IDs) to impersonate users.|| |
Developers frequently build custom authentication and session management schemes, but building these correctly is hard. As a result, these custom schemes frequently have flaws in areas such as logout, password management, timeouts, remember me, secret question, account update, etc. Finding such flaws can sometimes be difficult, as each implementation is unique.
|Such flaws may allow some or even all accounts to be attacked. Once successful, the attacker can do anything the victim could do. Privileged accounts are frequently targeted.|| |
Consider the business value of the affected data or application functions.
Also consider the business impact of public exposure of the vulnerability.
The first thing you’ll notice in the info above is that this risk is not as clearly defined as something like injection or XSS. In this case, the term “broken” is a bit of a catch-all which defines a variety of different vulnerabilities, some of which are actually looked at explicitly and in depth within some of the other Top 10 such as transport layer security and cryptographic storage.
Anatomy of broken authentication
Because this risk is so non-specific it’s a little hard to comprehensively demonstrate. However, there is one particular practice that does keep showing up in discussions about broken authentication; session IDs in the URL.
The challenge we face with web apps is how we persist sessions in a stateless environment. A quick bit of background first; we have the concept of sessions to establish a vehicle for persisting the relationship between consecutive requests to an application. Without sessions, every request the app receives from the same user is, for all intents and purposes, unrelated. Persisting the “logged in” state, for example, would be a lot more difficult to achieve without the concept of sessions.
In ASP.NET, session state is a pretty simple concept:
Programmatically, session state is nothing more than memory in the shape of a dictionary or hash table, e.g. key-value pairs, which can be set and read for the duration of a user's session.
Persistence between requests is equally simple:
ASP maintains session state by providing the client with a unique key assigned to the user when the session begins. This key is stored in an HTTP cookie that the client sends to the server on each request. The server can then read the key from the cookie and re-inflate the server session state.
So cookies help persist the session by passing it to the web server on each request, but what happens when cookies aren’t available (there’s still a school of belief by some that cookies are a threat to privacy)? Most commonly, we’ll see session IDs persisted across requests in the URL. ASP.NET even has the capability to do this natively using cookieless session state.
Before looking at the cookieless session approach, let’s look at how ASP.NET handles things natively. Say we have a really, really basic logon page:
With a fairly typical response after logon:
The simple version of what’s happening is as follows (it’s easy to imagine the ASPX structure so I’ll include only the code-behind here):
var username = txtUsername.Text; var password = txtPassword.Text; // Assume successful authentication against an account source... Session["Username"] = username; pnlLoginForm.Visible = false; pnlLoginSuccessful.Visible = true;
We’re not too worried about how the user is being authenticated for this demo so let’s just assume it’s been successful. The account holder’s username is getting stored in session state and if we go to “Page 2” we’ll see it being retrieved:
Fundamentally basic stuff code wise:
var username = Session["Username"]; lblUsername.Text = username == null ? "Unknown" : username.ToString();
If we look at our cookies for this session (Cookies.aspx just enumerates all cookies for the site and outputs name value pairs to the page), here’s what we see:
Because the data is stored in session state and because the session is specific to the client’s browser and persisted via a cookie, we’ll get nothing if we try hitting the path in another browser (which could quite possibly be on another machine):
And this is because we have a totally different session:
Now let’s make it more interesting; let’s assume we want to persist the session via the URL rather than via cookies. ASP.NET provides a simple cookieless mode configuration via the web.config:
<system.web> <sessionState cookieless="true" /> </system.web>
And now we hit the same URL as before:
Whoa! What just happened?! Check out the URL. As soon as we go cookieless, the very first request embeds the session ID directly into a re-written URL (sometimes referred to as “URL mangling”). Once we login, the link to Page 2 persists the session ID in the hyperlink (assuming it’s a link to a relative path):
Once we arrive at Page 2, the behaviour is identical to the cookie based session implementation:
Here’s where everything starts to go wrong; if we take the URL for Page 2 – complete with session ID – and fire it up another browser, here’s what happens:
Bingo, the session has now been hijacked.
What made this possible?
The problem with the cookieless approach is that URLs are just so easily distributable. Deep links within web apps are often shared simply by copying them out of the address bar and if the URL contains session information then there’s a real security risk.
Just think about the possibilities; ecommerce sites which store credit card data, social media sites with personal information, web based mail with private communications; it’s a potentially very long list. Developer Fusion refers to cookieless session state in its Top 10 Application Security Vulnerabilities in Web.config Files (they also go on to talk about the risks of cookieless authentication).
Session hijacking can still occur without IDs in the URLs, it’s just a whole lot more work. Cookies are nothing more than a collection of name value pairs and if someone else’s session ID is known (such as via an executed XSS flaw), then cookies can always be manipulated to impersonate them.
Fortunately, ASP.NET flags all cookies as HttpOnly – which makes them inaccessible via client side scripting - by default so the usual document.cookie style XSS exploit won’t yield any meaningful results. It requires a far more concerted effort to breach security (such as accessing the cookie directly from the file system on the machine), and it simply doesn’t have the same level of honest, inadvertent risk the URL attack vector above demonstrates.
Use ASP.NET membership and role providers
Now that we’ve seen broken authentication and session management firsthand, let’s start looking at good practices. The best place to start in the .NET world is the native membership and role provider features of ASP.NET 2 and beyond.
Prior to .NET 2, there was a lot of heavy lifting to be done by developers when it comes to identity and access management. The earlier versions of .NET or even as far back as the ASP days (now superseded for more than 8 years, believe it or not) required common functionality such as account creation, authentication, authorisation and password reminders, among others, to be created from scratch. Along with this, authenticated session persistence was also rolled by hand. The bottom line was a lot of custom coding and a lot of scope for introducing insecure code.
Rather than run through examples of how all this works, let me point you over to Scott Allen’s two part series on Membership and Role Providers in ASP.NET 2.0. Scott gives a great overview of the native framework features and how the provider model can be used to extend the functionality to fit very specific requirements, such as authentication against another source of user credentials.
What is worth mentioning again here though is the membership provider properties. We’re going to be looking at many of these conceptually so it’s important to understand there are native implementations within the framework:
|ApplicationName||Gets or sets the name of the application.|
|EnablePasswordReset||Gets a value indicating whether the current membership provider is configured to allow users to reset their passwords.|
|EnablePasswordRetrieval||Gets a value indicating whether the current membership provider is configured to allow users to retrieve their passwords.|
|HashAlgorithmType||The identifier of the algorithm used to hash passwords.|
|MaxInvalidPasswordAttempts||Gets the number of invalid password or password-answer attempts allowed before the membership user is locked out.|
|MinRequiredNonAlphanumericCharacters||Gets the minimum number of special characters that must be present in a valid password.|
|MinRequiredPasswordLength||Gets the minimum length required for a password.|
|PasswordAttemptWindow||Gets the time window between which consecutive failed attempts to provide a valid password or password answer are tracked.|
|PasswordStrengthRegularExpression||Gets the regular expression used to evaluate a password.|
|Provider||Gets a reference to the default membership provider for the application.|
|Providers||Gets a collection of the membership providers for the ASP.NET application.|
|RequiresQuestionAndAnswer||Gets a value indicating whether the default membership provider requires the user to answer a password question for password reset and retrieval.|
|UserIsOnlineTimeWindow||Specifies the number of minutes after the last-activity date/time stamp for a user during which the user is considered online.|
Using the native .NET implementation also means controls such as the LoginView are available. This is a great little feature as it takes a lot of the legwork – and potential for insecure implementations – out of the process. Here’s how it looks straight out of the box in a new ASP.NET Web Application template:
<asp:LoginView ID="HeadLoginView" runat="server" EnableViewState="false"> <AnonymousTemplate> [ <a href="~/Account/Login.aspx" id="HeadLoginStatus" runat="server">Log In</a> ] </AnonymousTemplate> <LoggedInTemplate> Welcome <span class="bold"><asp:LoginName ID="HeadLoginName" runat="server" /></span>! [ <asp:LoginStatus ID="HeadLoginStatus" runat="server" LogoutAction="Redirect" LogoutText="Log Out" LogoutPageUrl="~/" /> ] </LoggedInTemplate> </asp:LoginView>
Beyond the LoginView control there’s also a series of others available right out of the box (see the Visual Studio toolbox to the right). These are all pretty common features used in many applications with a login facility and in times gone by, these tended to be manually coded. The things is, now that we have these controls which are so easily implemented and automatically integrate with the customisable role provider, there really aren’t any good reasons not to use them.
The important message here is that .NET natively implements a great mechanism to authenticate your users and control the content they can access. Don’t attempt to roll your own custom authentication and session management schemes or build your own controls; Microsoft has done a great job with theirs and by leveraging the provider model have given you the means to tailor it to suit your needs. It’s been done right once – don’t attempt to redo it yourself without very good reason!
When you really, really have to use cookieless sessions
When you really must cater for the individuals or browsers which don’t allow cookies, you can always use the cookieless="AutoDetect" option in the web.config and .NET will try to persist sessions via cookie then fall back to URLs if this isn’t possible. Of course when it does revert to sessions in URLs we fall back to the same vulnerabilities described above. Auto detection might seem like a win-win approach but it does leave a gaping hole in the app ripe for exploitation.
There’s a school of thought that says adding a verification process based on IP address – namely that each request in a session must originate from the same address to be valid – can help mitigate the risk of session hijacking (this could also apply to a cookie based session state). Wikipedia talks about this in Session Fixation (the practice of actually settings another user’s session ID), but many acknowledge there are flaws in this approach.
On the one hand, externally facing internet gateways will often present the one IP address for all users on the internal side of the firewall. The person sitting next to you may well have the same public IP address. On the other hand, IP addresses assigned by ISPs are frequently dynamic and whilst they shouldn’t change mid-session, it’s still conceivable and would raise a false positive if used to validate the session integrity.
Get session expirations – both automatic and manual – right
Session based exploits are, of course, dependent on there being a session to be exploited. The sooner the session expires, either automatically or manually, the smaller the exploit window. Our challenge is to find the right balance between security and usability.
Let’s look at the automatic side of things first. By default, ASP.NET will expire authenticated sessions after 30 minutes of inactivity. So in practical terms, if a user is dormant for more than half an hour then their next request will cause a new session to be established. If they were authenticated during their first session, they’ll be signed out once the new session begins and of course once they’re signed out, the original session can no longer be exploited.
The shorter the session expiration, the shorter the window where an exploit can occur. Of course this also increases the likelihood of a session expiring before the user would like (they stopped browsing to take a phone call or grab some lunch), and forcing users to re-authenticate does have a usability impact.
The session timeout can be manually adjusted back in the web.config. Taking into consideration the balance of security and usability, an arbitrary timeout such as 10 minutes may be selected:
<system.web> <sessionState timeout="10" /> </system.web>
Of course there are also times when we want to expire the session much earlier than even a few minutes of inactivity. Giving users the ability to elect when their session expires by manually “logging out” gives them the opportunity to reduce their session risk profile. This is important whether you’re running cookieless session or not, especially when you consider users on a shared PC. Using the LoginView and LoginStatus controls mentioned earlier on makes this a piece of cake.
In a similar strain to session timeouts, you don’t want to be reusing session IDs. ASP.NET won’t do this anyway unless you change SessionStateSection.RegenerateExpiredSessionId to true and you’re running cookieless.
The session timeout issue is interesting because this isn’t so much a vulnerability in the technology as it is a risk mitigation strategy independent of the specific implementation. In this regard I’d like to reinforce two fundamental security concepts that are pervasive right across this blog series:
- App security is not about risk elimination, it’s about risk mitigation and balancing this with the practical considerations of usability and project overhead.
- Not all app security measures are about plugging technology holes; encouraging good social practices is an essential component of secure design.
Encrypt, encrypt, encrypt
Keeping in mind the broad nature of this particular risk, sufficient data encryption plays an important role in ensuring secure authentication. The implications of credential disclosure is obvious and cryptographic mitigation needs to occur at two key layers of the authentication process:
- In storage via persistent encryption at the data layer, preferably as a salted hash.
- During transit via the proper use of SSL.
Both of these will be addressed in subsequent posts – Insecure Cryptographic Storage and Insufficient Transport Layer Protection respectively – so I won’t be drilling down into them in this post. Suffice to say, any point at which passwords are not encrypted poses a serious risk to broken authentication.
Maximise account strength
The obvious one here is password strength. Weak passwords are more vulnerable to brute force attacks or simple guessing (dog’s name, anyone?), so strong passwords combining a variety of character types (letters, numbers, symbols, etc) are a must. The precise minimum criterion is, once again, a matter of balance between security and usability.
One way of encouraging stronger password – which may well exceed the minimum criteria of the app – is to visually illustrate password strength to the user at the point of creation. Google do a neat implementation of this, as do many other web apps:
This is a piece of cake in the ASP.NET world as we have the PasswordStrength control in the AJAX Control Toolkit:
<asp:TextBox ID="txtPassword" runat="server" TextMode="Password" /> <ajaxToolkit:PasswordStrength ID="PS" runat="server" TargetControlID="txtPassword" DisplayPosition="RightSide" StrengthIndicatorType="Text" PreferredPasswordLength="10" PrefixText="Strength:" TextCssClass="TextIndicator_txtPassword" MinimumNumericCharacters="0" MinimumSymbolCharacters="0" RequiresUpperAndLowerCaseCharacters="false" TextStrengthDescriptions="Very Poor;Weak;Average;Strong;Excellent" TextStrengthDescriptionStyles="Class1;Class2;Class3;Class4;Class5" CalculationWeightings="50;15;15;20" />
Of course this alone won’t enforce password strength but it does make compliance (and above) a little easier. For ensuring compliance, refer back to the MinRequiredNonAlphanumericCharacters, MinRequiredPasswordLength and PasswordStrengthRegularExpression properties of the membership provider.
Beyond the strength of passwords alone, there’s also the issue of “secret questions” and their relative strength. There’s mounting evidence to suggest this practice often results in questions that are too easily answered but rather than entering into debate as to whether this practice should be used at all, let’s look at what’s required to make it as secure as possible.
Firstly, avoid allowing users to create their own. Chances are you’ll end up with a series of very simple, easily guessed questions based on information which may be easily accessible (the Sarah Palin incident from a couple of years back is a perfect example).
Secondly, when creating default secret questions – and you’ll need a few to choose from - don’t fall for the same trap. Questions such as “What’s your favourite colour” are too limited in scope and “Where did you go to school” can easily be discovered via social networking sites.
Ideally you want to aim for questions which result in answers with the highest possible degree of precision, are stable (they don’t change or are forgotten over time) and have the broadest possible range of answers which would be known – and remembered - by the narrowest possible audience. A question such as “What was the name of your favourite childhood toy” is a good example.
Enable password recovery via resets – never email it
Let’s get one thing straight right now; it’s never ok to email someone their password. Email is almost always sent in plain text so right off the bat it violates the transport layer protection objective. It also demonstrates that the password wasn’t stored as a salted hash (although it may still have been encrypted), so it violates the objective for secure cryptographic storage of passwords.
What this leaves us with is password resets. I’m going to delve into this deeper in a dedicated password recovery post later on but for now, let’s work to the following process:
- Initiate the reset process by requesting the username and secret answer (to the secret question, of course!) of the account holder.
- Provide a mechanism for username recovery by entering only an email address. Email the result of a recovery attempt to the address entered, even if it wasn’t a valid address. Providing an immediate confirmation response via the UI opens up the risk of email harvesting for valid users of the system.
- Email a unique, tokenised URL rather than generating a password. Ensure the URL is unique enough not to be guessed, such as a GUID specific to this instance of the password reset.
- Allow the URL to be used only once and only within a finite period of time, such as an hour, to ensure it is not reused.
- Apply the same password strength rules (preferably reuse the existing, secure process) when creating the new password.
- Email a notification to the account holder immediately once the change is complete. Obviously do not include the new password in this email!
- Don’t automatically log the user in once the password is changes. Divert them to the login page and allow them to authenticate as usual, albeit with their new password.
This may seem a little verbose but it’s a minor inconvenience for users engaging in a process which should happen very infrequently. Doing password recovery wrong is a recipe for disaster; it could literally serve up credentials to an attacker on a silver plate.
In terms of implementation, once again the membership provider does implement an EnablePasswordReset property and a RequiresQuestionAndAnswer property which can be leveraged to achieve the reset functionality.
Remember me, but only if you really have to
People are always looking for convenience and we, as developers, are always trying to make our apps as convenient as possible. You often hear about the objective of making websites sticky, which is just marketing-speak for “make people want to come back”.
The ability to remember credentials or automate the logon process is a convenience. It takes out a little of the manual labour the user would otherwise perform and hopefully lowers that barrier to them frequently returning just a little bit. The problem is though, that convenience cuts both ways because that same convenience may now be leveraged by malicious parties.
So we come back around to this practical versus secure conundrum. The more secure route is to simply not implement a “remember me” feature on the website. This is a reasonable balance for, say, a bank where there could be serious dollars at stake. But then you have the likes of just about every forum out there plus Facebook, Twitter, YouTube etc who all do implement this feature simply because of the convenience and stickiness it offers.
If you’re going to implement this feature, do it right and use the native login control which will implement its own persistent cookie. Microsoft explains this feature well:
By default, this control displays user name and password fields and a Remember me next time check box. If the user selects this check box, a persistent authentication cookie is created and the user's browser stores it on the user's hard disk.
Then again, even they go on to warn about the dangers of a persistent cookie:
To prevent an attacker from stealing an authentication cookie from the client's computer, you should generally not create persistent authentication cookies. To disable this feature, set the DisplayRememberMe property of the Login control to false.
What you absolutely, positively don’t want to be doing is storing credentials directly in the cookie and then pulling them out automatically on return to the site.
Automatic completion of credentials goes a little bit further than just what you implement in your app though. Consider the browser’s ability to auto-complete form data. You really don’t want login forms behaving like this:
Granted, this is only the username but consider the implications for data leakage on a shared machine. But of course beyond this we also have the browser’s (or third party addons) desire to make browsing the site even easier with “save your password” style functionality:
Mozilla has a great summary of how to tackle this in How to Turn Off Form Autocompletion:
The easiest and simplest way to disable Form and Password storage prompts and prevent form data from being cached in session history is to use the autocomplete form element attribute with value "off"
Just turn off the autocomplete attribute at the form level and you’re done:
<form id="form1" runat="server" autocomplete="off">
My app doesn’t have any sensitive data – does strong authentication matter?
Yes, it actually matters a lot. You see, your authentication mechanism is not just there to protect your data, it also must protect your customers’ identities. Identity and access management implementations which leak customer information such as their identities – even just their email address – are not going to shine a particularly positive light on your app.
But the bigger problem is this; if your app leaks customer credentials you have quite likely compromised not only your own application, but a potentially unlimited number of other web applications.
Let me explain; being fallible humans we have this terrible habit of reusing credentials in multiple locations. You’ll see varying reports of how common this practice really is, but the assertion that 73% of people reuse logins would have to be somewhere in the right vicinity.
This isn’t your fault, obviously, but as software professionals we do need to take responsibly for mitigating the problem as best we can and beginning by keeping your customer’s credentials secure – regardless of what they’re protecting – is a very important first step.
This was never going to be a post with a single message resulting in easily actionable practices. Authentication is a very broad subject with numerous pitfalls and it’s very easy to get it wrong, or at least not get it as secure as it could - nay should - be.
Having said that, there are three key themes which keep repeating during the post:
- Consider authentication holistically and be conscious of its breadth. It covers everything from credential storage to session management.
- Beware of the social implications of authentication – people share computers, they reuse passwords, they email URLs. You need to put effort into protecting people from themselves.
- And most importantly, leverage the native .NET authentication implementation to the full extent possible.
You’ll never, ever be 100% secure (heck, even the US military doesn’t always get it right!), but starting with these objectives will make significant inroads into mitigating your risk.
- Membership and Role Providers in ASP.NET 2.0
- The OWASP Top Ten and ESAPI – Part 8 – Broken Authentication and Session Management
- GoodSecurityQuestions.com (yes, there’s actually a dedicated site for this!)
- How To: Use Forms Authentication with SQL Server in ASP.NET 2.0
- Session Attacks and ASP.NET