Common authentication and authorization vulnerabilities (and how to avoid them)
Authentication and authorization are two cornerstones of modern web application security, but there are many ways to get them wrong. Learn how to identify common security defects and avoid vulnerabilities that could allow attackers to access restricted data and functionality by bypassing authentication, authorization, or both.
Your Information will be kept private.
Your Information will be kept private.
Sven Morgenroth on Paul’s Security Weekly #720
Appearing on the Paul’s Security Weekly cybersecurity podcast, Invicti security researcher Sven Morgenroth talked about the many challenges of secure authentication and authorization in web applications, complete with practical examples of real-life vulnerabilities. Watch the full interview below and read on for an overview of authentication and authorization security.
Authentication, authorization… What’s the difference?
While it may seem surprising that such fundamental topics are still often misunderstood and confused, authentication and authorization not only sound and look similar but are also closely related. Let’s start with clear working definitions:
- Authentication is about proving your identity. This could mean entering a username and password, logging in via an SSO flow, or providing a unique access key.
- Authorization is about checking if you have the right permissions. Once you are authenticated, the application should verify if you are allowed to access a specific resource or operation, typically based on your role in the application.
Because they are usually performed together and also go wrong together, authentication and authorization combined are sometimes called simply “auth” (which is also easier to spell and faster to type). With modern enterprise web applications so dependent on correctly enforcing access control to protect sensitive data, auth-related vulnerabilities and attacks are at the forefront of web security.
When auth goes wrong – common types of vulnerabilities
Implementing effective and comprehensive access control is always challenging, and there are many factors at play that make it even trickier. All too often, security is an afterthought in the software development process, and enforcing access control is no exception. For easier and faster development, entire applications might be built without any access restrictions and then have a login form bolted on as a final step, which increases the risk of auth-related security gaps.
Distributed software architectures take the auth challenge to a whole new level, with requests often passing through multiple services and interfaces. Ensuring proper access control across different contexts is extremely difficult, especially when it needs to span modules developed by entirely separate teams. When you add problems with passing auth information across APIs, data formats, origins, and physical systems, it’s no surprise that auth-related vulnerabilities are so common. Here are just a few typical examples, many previously discussed in detail on this blog.
Insecure value comparisons
First up are language-specific insecure value comparisons, also known as type juggling vulnerabilities. Two of the most popular web development languages, namely JavaScript and PHP, are loosely-typed languages, meaning that they are, by default, very permissive in the types of data they accept. Each also has its own quirks when comparing values, so without due care and attention to input validation, attackers may be able to enter a special value that happens to always be accepted in a specific context.
Imagine you have a login form where the username and password values are sent directly to a vulnerable PHP script that uses typical loose comparisons with the ==
operator. For reasons explained in detail in this post about PHP type juggling vulnerabilities, comparing any string to TRUE
or 0
using this operator will (by default) give the result TRUE
. So in the simplest case, just sending the boolean TRUE
in a JSON login request may be enough to bypass authentication.
Using strict comparisons with the ===
operator or (better still) a dedicated comparison function is usually enough to avoid this class of vulnerabilities, but in large software projects, even such seemingly trivial errors can sometimes make it into production. See our earlier post to learn more about a real-life PHP type juggling vulnerability in CMS Made Simple.
The pitfalls of path-based auth
Another bad practice that may lead to auth bypass attacks is implementing access control by checking for a specific path. For example, an application might check if the user is authorized to access the admin panel simply by comparing the path requested by the user with the path of the admin panel. Without input sanitization, it may be possible to bypass auth by inserting directory traversal strings to request a path that looks different (so it isn’t blocked) but eventually leads to the same admin panel URL. This is exactly what happened with the Oracle WebLogic Server authentication bypass vulnerability that we covered in detail back in 2020.
Reliance on path comparisons can lead to similar path/directory traversal vulnerabilities due to parsing inconsistencies across different servers. For example, a proxy server might be set up to restrict access to a specific path by returning an error code for unauthenticated users. If an attacker is able to slip in a path traversal string, the proxy might be fooled into allowing access to a path that looks different but actually resolves to the restricted resource.
Misplaced trust in auth sources
In complex architectures, deployments, and data flows, developers will often find themselves in situations where auth-related decisions are clearly someone else’s problem. This is especially true when dealing with secondary contexts, for example when handling requests received via an API rather than directly from users. In applications assembled from hundreds of microservices, user auth is often done only by the front-end API, so the back-end services have no idea who issued what request. If the front-end code has a path traversal vulnerability that lets attackers manually specify an endpoint that would normally be inaccessible, the back-end will duly accept and execute the request, potentially revealing sensitive data.
Deciding who and what your code should trust is a design decision that can have far-ranging consequences. One real-life vulnerability (long since fixed) comes from 2017, when the Uber website was found to be vulnerable to account hijacking via subdomain takeover. Security researcher Arne Swinnen noticed that one of Uber’s subdomains was actually pointing to another domain, at that time unclaimed, which he was able to claim. Uber’s auth flow included setting a session token that was valid for all Uber subdomains, including the one Arne had claimed. By redirecting users to that domain, he would have been able to read session tokens and take over user accounts. And all because of implicit trust in domain-level auth.
Missing function-level access control
Related to misplaced trust is missing function-level access control (and if the name looks familiar, that’s because it used to be a separate OWASP Top 10 category). This class covers failures to check if a user is authorized to call a specific function. We wrote about a real-life example of this a while back when analyzing a vulnerability in Maian Support Helpdesk. In this case, users could call certain API endpoints that were not visible in the user interface yet were still accessible, including some admin operations. Without systematic function-level access control, such omissions can easily evade testing and allow attackers to obtain unauthorized access.
Deciding where and how to implement access control can be especially tricky when working with GraphQL APIs, where there are many ways to access the same data through different queries. Ideally, each query should only return data that the current user is authorized to access, but this is easy to get wrong, and any error at this stage could allow attackers to obtain sensitive information. This is especially true when dealing with secondary contexts, which is common when adding a GraphQL layer to an existing REST API.
Token mismanagement
At the level of HTTP requests, access control depends on securely generating the right access tokens and sending them to the right system at the right time. Whenever attackers are able to get their hands on a valid access token (such as a session cookie), you run the risk of session hijacking. The risk of client-side token theft is especially high with complex single sign-on flows, where misconfigurations can allow malicious actors to intercept access tokens via redirects or read them from server logs.
Password reset tokens are another critical security mechanism that is prone to mismanagement and abuse. For example, an application might expose an API endpoint for generating password reset tokens. Any vulnerability that lets attackers call that API may also allow them to reset passwords for known user accounts, leading to account takeover. As discussed above, such vulnerabilities may be caused by confusion around authorization in secondary contexts or simply by assuming that because the endpoint is private, it cannot be called by unauthorized users.
Security best practices for implementing authentication and authorization
If you look at the OWASP Top 10 for 2021, you will see that broken access control is the #1 cause of web application security issues, with no fewer than 34 weaknesses grouped under this category. As shown above, implementing and enforcing authentication and authorization is never easy, and there are many ways it can all go wrong. Ensuring secure access is all about careful planning, secure implementation, and regular verification.
Follow these best practices to minimize the risk of auth-related vulnerabilities:
- Include auth in all planning and design work – many vulnerabilities result from bolting on access control at the last minute.
- Keep access control logic at the application level – denying resource access at the server level is never bulletproof.
- Follow secure coding practices and check access control in code audits to avoid code-level auth vulnerabilities.
- Whenever possible, use specialized libraries with a proven security track record rather than rolling your own.
- Routinely check your access controls across the entire development process, up to and including production.
- Ensure you have control over any third-party infrastructure that could be abused to break auth flows.