Almost every time we do a penetration test or code review, we find problems with authorization.  Sometimes we call these horizontal or vertical privilege escalation.  Sometimes we call it instance based restriction gaps or function based restriction gaps.  Ultimately, many applications fail to implement clear restrictions on who can do what.  This post attempts to revisit these types of findings, explain a bit about how we test for them and talk about some standard ways of addressing them.

Dispelling A Dangerous Assumption

Sometimes we hear developers say that a particular issue is not important because a user has to be authenticated (usually by presenting a user and password) before they could ever cause the issues we are showing.  Yet, in many systems, it is trivially easy to enroll with a throw away account and learn about how the system works.  That means that I can both get an account easily and I can generally hide who I really am while I’m testing.  So the truth is that in many many systems, it is perfectly possible to experiment with authorization as an anonymous but authenticated user.

In cases where the authentication process is more rigorous, or in a business to business setting, the risk may be reduced but it is probably not eliminated.  We certainly see people trying to guess and misuse API tokens.

Instance Based Authorization Failures (Horizontal Privilege Escalation)

The easiest way to think about instance based authorization (authz) failures, is to use an example.  Consider the case where I am working with a timesheet.  The system presents me with a form for my timesheet.  As I fill out the form, the javascript front end is watching for changes and saving them in the background as I go.  Here is an abbreviated example of a background POST that saves the current work in progress on a timesheet:

POST /entry/save HTTP/1.1
X-CSRF-Token: ABC--
Cookie: _time_sess=abc

day015_project18402963_task7138879=2.00&changed_fields=day015_project18402963_task7138879&of_user=307476&approval_period_start=014&approval_period_start_year=2019&x_requested_with=XMLHttpRequest&authenticity_token=S9zI

What is interesting here is that as a legitimate user, I can capture any number of realistic requests like this.  Once I have a few, I can examine them and replay them quite easily within a valid session.  In this case, see the red and bold user id number (307476).  That appears to be tied to my logged in user.  It is quite likely that other numbers would trigger this request being applied to other users.  The question is, is there anything in the back end logic to prevent a request for user 307477 from successfully updating that user’s data from my session?

The simplest possible prevention of this type of problem is to tie each instance of a timesheet to its owning user and to only allow users to update their own timesheets.  This might look like the following in CanCan, which is a handy Authz library used with Ruby on Rails that we like for illustration because it is direct:

class TimesheetController < ApplicationController
 load_and_authorize_resource
   def show
      # @timesheet is already loaded and authorized
   end
end

Typically this depends on the Timesheet object having a column tying it to a user that is the owner.  The magic here is that CanCan does all of the work to load and check the owner automatically for any kind of model object.

This might look like this in Java with Apache Shiro:

Subject currentUser = SecurityUtils.getSubject();
currentUser.checkPermission("timesheet:2345:approve");
approveTimesheet();

We also see this custom coded in many clients.

One way or another, we need to make sure to control access to objects at an instance level.

Function Based Authorization Failures (Vertical Privilege Escalation)

Typically function based access control is implemented with role mappings.  Some examples of vertical privilege escalation is if a user can call a function they shouldn’t be allowed to call.  For example, a team member Natalie is not a manager so she should not be able to approve timesheets.  We would never expect the following POST to start from her browser, because we wouldn’t expect the button that triggers the post from even showing.

POST /approve/finalize?return_to=… HTTP/1.1
   (I wonder if this is open redirect but that is another topic…)

Cookie: _timesheet_sess=ABC
Upgrade-Insecure-Requests: 1

authenticity_token=ABC&user_id=1770921&period_start=365&period_start_year=2018&from\page=0&commit=Approve+Timesheet

Since the Approve button is only visible to people in the admin or manager role, developers wouldn’t expect this post to ever happen so they might not write code to prevent it.

Note that the user id is also in this payload.  It is easy enough to imagine taking a request like this and replaying it for every id in the system for every period.  Burp Intruder would make that an easy exercise that requires no programming.

Something to keep in mind is that Single Page Apps (SPA’s) like those written in Angular, typically ship a Controller to the client which can be easily explored to identify potential endpoints that might not otherwise be obvious.  This only raises the importance of implementing comprehensive authorization on the server.

The Spring Security annotations illustrate how roles are often enforced:

@PreAuthorize("hasRole('ROLE_MANAGER')")
public void approve(Timesheet timesheet);

This might look like this in Java with Apache Shiro:

Subject currentUser = SecurityUtils.getSubject();
currentUser.checkPermission("timesheet:approve");
approveTimesheet();

You get the idea.

Note that we most commonly have to do both function and instance based approval.  Meaning, I can approve some timesheets but only those from people on my team – not across the company or all of the tenants in the solution.  That means that the real logic needs to check both:

  • The POST /approve/finalize …
  • The user_id=1770921

I must be a user that has the role required to approve timesheets AND I should be specifically allowed to approve this user’s timesheet.

Defining Intended Authorization

As is often the case with software, a lot of times the authorization gaps we find have to do with a lack of communication between a product manager (stakeholder) and the engineering team.  The requirements said that Matt should be able to see and submit his timesheet.  The requirements did not say that Matt should NOT be able to see Joe’s timesheet or delete Brian’s timesheet.  Since the requirements were never captured, the engineers may not have stopped to think about what could go wrong.  Sometimes making these requirements work is easy, sometimes it is hard.

Once we’ve defined the intended authorization logic, we can also test it.  This is a place where programmatic unit and functional testing can be extremely effective.  We always recommend writing security unit tests and incorporating things such as horizontal and vertical access attempts (and blocking) in the tests.

Bonus Points

Whenever authorization fails, we need to know.  Most systems don’t make it easy to call something where you are not authorized.  If you click a button to delete something as an admin, we presume that you saw the button.  If as described above, an unauthorized user sends a request that suggests they clicked the button and we detect and block that, we should also LOG that and escalate because there is almost certainly something nefarious going on.  Even in a horizontal access case, if I try to access a timesheet for another user with an ID I shouldn’t have access to and you block it, the event should be logged in such a way that security will see it.

Another area that makes a big difference when it comes to access control is simplifying management of user roles.  In practice, this might translate to a single source of the truth like Active Directory tied to a SSO solution that transmits group memberships from AD and end solutions that translate those AD group memberships into local group memberships or roles.  This way, by removing a user from a group in Active Directory, I can remove them from the admin role in the Timesheet system.

Advanced Paradigms

A lot of time instance based access control boils down to keeping a more detailed authorization database (or store of some kind) that understands user roles and groups and therefore object ownership.  Function based access control can be implemented based purely on roles and coded with annotations or fairly broad controller level directives in most languages.  Sometimes what we really want though is a more contextual understanding of the user.  Attribute based access control extends on the idea of RBAC and adds the idea that a “yes” or “no” decision on whether a user can do something may incorporate other attributes – things like geolocation, time of day, effective role, recent activity, etc.

We also see changes happening in terms of roles being assigned for a limited time.  Systems like AWS STS or Hashicorp Vault can help implement session based role assignment.  In this paradigm, a user is given the right to assume a role.  The user itself does not have any privileges directly assigned.  Rather, when they need them they ask and assuming they have been cleared to ask for that access, they are granted specific access to what they requested for a limited time.  Since the user doesn’t have any rights directly and the request for any session is audited, this reduces exposure due to lost credentials and makes any role usage visible at granular level.

While we’re talking, we should also think about how we implement authorization on our computing platforms.  Kubernetes offers RBAC based controls to define who can do what in a given cluster, pod, etc.  As defining the environment moves toward being a developer task, we need to keep these authorization scenarios top of mind.

An area we haven’t seen a good consistent solution is in service to service authorization.  Usually within a given set of boundaries, microservices authenticate (or not!) and then return or don’t.  We believe there is room to grow frameworks for controlling what data can be requested by which microservices and this is an area we’re putting in some research work.

Conclusion

As developers, we always need to check that the logged in user has the right to perform the action requested against the instance(s) requested.  That implies both checking the function and the instance.

References

Matt Konda

Matt is a software engineer. He's our CEO and former Chair & OWASP Board Member.

Want to stay up to date with the lastest from Jemurai?

Sign up for our monthly newsletter!