Demo
Learn how Cobalt’s Pentest as a Service (PtaaS) model makes you faster, better, and more efficient.
Demo
Learn how Cobalt’s Pentest as a Service (PtaaS) model makes you faster, better, and more efficient.

UX-Friendly Enumeration Protection in Ruby on Rails

How to avoid revealing the existence of records to attackers in web applications, while keeping a good user experience for legit users ...

How to avoid revealing the existence of records to attackers in web applications, while keeping a good user experience for legit users — a security tip.

TL;DR If not signed in, always redirect to the sign in form. If signed in, show the same error both if not found and not authorized.

Security in layers

Part of a successful data exfiltration, is knowing the existence of a resource e.g. a user-account, an organization-profile or the content users or organizations create in applications. The second step in a successful hack is actually retrieving or modifying the known content. E.g. if it’s possible for an attacker to tell whether a user is registered on a website, by listing registered emails, the attacker has taken a big step — now there’s only the password cracking left for a complete compromise (unless there are other defenses in place, such as two-factor authentication).

There are also less grave issues that might arise from enumerating users and other records. E.g. your competitors might run a script on a regular basis to enumerate who or which companies have registered profiles on your site, giving them a competitive edge.

Here’s a simple example of attempting to enumerate organizations of Cobalt— paste this in your terminal:

$ host='https://app.cobalt.io'
$ for org_id in {'cobalt,acme,sick-org'}; do
    curl -s -o /dev/null -w "%{http_code}\n" $host'/'$org_id
done
=>
302
302
302

All three responses were 302 (temporary redirect), though only the first organization exists. Doing the same on Github, where orgs are public we get:

$ host='https://github.com'
$ for org_id in {'cobalt,acme,sick-org'}; do
    curl -s -o /dev/null -w "%{http_code}\n" $host'/'$org_id
done
=>
200
200
404

The two first organizations exists, last one not.

Our Goal: Improve security and don’t hurt honest users

Often a security conscious developers would hide the existence of a record. This developer would make his or her application have the same behavior regardless of the existence of a record or the user not having access. This could be achieved by showing a 404-page in both cases (like Github) or 403 Access Denied, like AWS S3 (AWS S3 actually allows enumeration of buckets). However, showing error pages is not the best way to help good and honest users on their way to their content. Note Github offers assistance: If you’re not signed in, they present a login form on their 404-page.

Three scenarios have to be considered

In our application logic we’ll have three main scenarios: Anonymous users who are not signed in, authenticated user who are *authorized *to access a resource, and authenticated users who are not authorized.

1. Anonymous user (not signed in)

Redirect to sign-in. If not signed in, we don't yet know whether this is a legit user, an attacker, or someone else. So we'll simply redirect the user to our sign in page (alternatively, show a sign in form on our 404 page). In this way we give legit users a chance to sign in, and not just give them a 404-page so that they would be left all alone to figure out what to do next. Still, for attackers, nothing has been revealed.

Authenticated users

We know who they are, or who they claim to be. As before, we know the URL and therefore which resource the user is trying to access. With this we can from our permission logic determine if this user is in fact allowed to see or manipulate this resource.

2. Authorized

**Render resource, obey action. **Authorized action, hopefully our most common case, is when a resource exists at this path and a legit user is trying to access it. In case of a GET-request we would return a status code 200 and render the resource, everything is fine!

3. Not authorized

**Show 404 page, nothing here! **At this point, it is totally fine to show a 404-page, because we now know for sure that this user has no business knowing the existence of this resource, let alone accessing it. An alternative to showing a 404 page, would be to respond with a 403 status code (like mentioned above), or a similar type off 4XX error — the most important part here is to aways give the same response whether the resource exists or the user wasn’t allowed.

Example in Ruby on Rails using Devise

A typical flow for requesting a private resource, is first to authenticate the user in the request, and then later on, typically in the controller we attempt to fetch the resource from the database. Once we’ve fetched the resource, we’re able to check whether the user is actually allowed to perform an action on it. Using Devise in Rails we retrieve (authenticate) the user from the signed and encrypted session cookie. The authentication typically happens in a middleware step (early in the request lifecycle), and the authorization logic is queried later in the controller — after we’ve fetched the resource.

Two typical plugins AKA gems in Rails for handling authorization are, CanCanCan and Pundit. As far as the controller (the object in a Rails app that decides how to respond to a request) cares, both authorization/permission gems work in the same way. Here’s an example of the “show”-action for an organization. First the user is authenticated:

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # Unless skipped, we always fetch and set the 
  # current_user-variable if the session is valid. We do this before
  # even considering the resource they're interested in

  before_action :authenticate_user!
end

Then the request is authorized

# app/controllers/orgs_controller.rb
class OrgsController < ApplicationController
  def show
    @org = Org.find(params[:id]) # Raises 404, if not found
    # At this point in the request, we have the resource and the 
    # current_user and we can query our authorization logic.  

    authorize! :show, @org # Can current_user :show @org?

    # @org is rendered, if passing authorize!
  end

private

# In Pundit, the error might be Pundit::NotAuthorizedError
  rescue_from CanCan::AccessDenied do
    # If the current_user not have access to @org, we end up here.
    # Let's pretend the resource isn't here:

    raise ActiveRecord::RecordNotFound
  end
end

Alternatively, you might try configuring your authorization logic to raise a not-found error as the default or move the rescuing to a central place so you only have to define auth-error rescues once. Lastly, write tests for these errors that verify your controllers are behaving as they should, otherwise it’s only a matter of time before regressions appear.

Conclusion

There are many ways to avoid enumeration and to be honest it’s really hard to fully avoid. Being an application security startup, having friendly hackers, constantly breathing down our necks — we know that first hand. Pick your battles: Your engineering team should know which resource is the most sensitive or which resources are the top 5 — start with these and protect them well.

Back to Blog
About Christian Hansen
Christian Hansen is Co-founder and Chief Technology Officer at Cobalt. He’s focused on scaling the company and driving sustainable growth, as well as advancing product development through effective cross-functional partnerships with Product and Engineering. More By Christian Hansen
Pentester Spotlight: Matt Buzanowski, From US-Army to Red Teaming
Matt Buzanowski started his career in the US Army and now works with the Red Team he started and grew at a Fortune 100 company. Matt has been a member of the Core since 2017
Blog
Jun 29, 2022