Rails Security Threats: Authentication

Authentication is at the heart of most web development, yet it is difficult to get right. In this article, Diogo Souza discusses common security problems with authentication systems and how you can resolve them. Even if you never build an authentication system from scratch (you shouldn't), understanding these security concerns will help you make sure whatever authentication system you use is doing its job.

Part one of this series, covered Injection Attacks

In the second article of our series about OWASP Top 10 Web Application Security Risks, we'll dive into the universe of broken authentication and data exposure threats.

More specifically, we'll talk about how easy it is for a hacker to trick the code you've built and perform attacks to obtain users’ data:

  • User enumeration: When they exploit your login pages by brute-force testing a list of possible users just to check if they exist in your database.
  • Weak passwords: When your system allows for weak passwords, hackers can carry out a brute force attack to guess your users' passwords.
  • Unrestricted cookies: When your system stores sensitive data in cookies without proper security settings, hackers can steal the information through XSS attacks.

We will also go into detail about sensitive data that are not sufficiently protected, making room for vulnerabilities, such as the following:

  • Unsafe sensitive storage: when sensitive data are encrypted with weak algorithms, such as MD5.
  • Exposing sensitive data: when developers unintentionally expose unencrypted sensitive data in URLs or hidden fields, for example.

As with the first article in the series, we'll also make use of RailsGoat to explore each of these threats in practice. If you're new here, please refer to the previous article to get the app set up and running.

Let's jump right in!

Authentication Threats

We can't live without authentication. Whether it is on your back-end APIs or within your front-end forms, it’s one of the most important steps in application development since it highlights the border limits of your security measures.

Not only for the authentication itself but for what comes next: session management. Where are you going to store your passwords and tokens? Are they properly encrypted? Are you using a trustworthy algorithm? Is your password complex enough?

There are many questions to keep an eye on. Let's break them down a bit and understand some common attacks involving authentication and session management within Rails applications.

User Enumeration

User enumeration is a famous technique that attackers use to check (via the use of brute force) whether the given data exist within your system.

One of the most notorious examples of this attack happens with the current Facebook login page. If you enter some invalid and random credentials like those shown in the image below, Facebook will process the request and check for the existence of the user within their database.

Facebook's login page

Facebook's login page.

When you click the Log In button, Facebook will return an error message stating that your username (which can be either an email or phone number) isn't valid.

Invalid username message

Invalid username message.

Therefore, the attacker knows that the application tells you whether a user is registered. For free.

If the attacker has a list of emails (whether they were randomly generated or bought somewhere), it's also possible to ask the application whether the password is correct:

Invalid password message

Invalid password message.

Once an attacker knows how the system responds separately to each validation, a list can be created of possible users and common/weak passwords. Brute-force tests can then be conducted repeatedly against the system until access is gained.

Of course, Facebook developers know about this, and it’s why they implemented additional protections, such as invisible captchas and validations over the number of requests coming from a specific IP address.

One of the things you can do to avoid user enumerations is validate both the username and password together and return a generic message. Let's see this approach in action!

The Threat in Action

In the RailsGoat app, open the user.rb file in the app/models folder and locate the authenticate method. Within it, you may find the following code snippet:

raise "#{email} doesn't exist!" if !(user)
if user.password == Digest::MD5.hexdigest(password)
  auth = user
else
  raise "Incorrect Password!"
end

Exactly! The messages are being set in a way that will allow attackers to know whether the user doesn't exist or the password is incorrect.

Test it out. Go to the RailsGoat login page and type a random email and password. You may see the following error message:

The user doesn't exist

The user doesn't exist.

Otherwise, if the user exists (ken@metacorp.com, for example) but the password is wrong, the following message is displayed:

Incorrect password

Incorrect password.

Considering that your app only allows strong passwords, the attacker can still create a list of enumerated valid client emails and target phishing emails to them, creating the impression that you're the one who's requesting the malicious action.

How to Resolve This Issue

The quickest action you can take to make it safer is to change your message and complicate the hacker's life.

Within the sessions_controller.rb (app/controllers folder), locate the create method and change the following code snippet

flash[:error] = e.message

to the following:

flash[:error] = "Your credentials aren't valid."

Now, every time users type a wrong username or password, that's the message they'll receive:

Invalid credentials

Invalid credentials.

Another way to do that is by changing the two messages within the users.rb model.

Weak Passwords

We can't stress this enough. Require your users to input strong passwords and make sure to create some validation code to check whether they meet the criteria for a strong password.

This is one of the most important steps to prevent user enumerations.

The Threat In Action

In RailsGoat, open the user.rb file and locate the first line of code right before the class definition:

validates :password,
    presence: true,
    confirmation: true,
    length: {
      within: 6..40
    },

    ...

This is a clear example of the password being weakly validated since only its length is checked.

How to Resolve This Issue

The solution is quite simple; validate your password against some stronger requirements, such as the following:

  • at least 1 lowercase and 1 uppercase letter,
  • at least 1 digit,
  • at least 10 chars.

The more requirements you add, the safer your passwords will be. Just be sure to not push it too much since this can lead to a complexity increase in the usability and password recovery flow.

To solve this within RailsGoat, simply substitute the length property with the following:

:format => {:with => /\A.*(?=.*[a-zA-Z])(?=.*[0-9])(?=.{10,}).*\z/},

Then, go to the sign-up page, fill in the fields, and provide a weak password. When you submit, this will be the error message displayed:

The password is invalid

The password is invalid.

Unrestricted Cookies

In the previous article, we dedicated some time to understanding how XSS attacks occur. Since they take place by allowing attackers to run malicious scripts, important information can be stolen from session cookies if we don't prevent access to attributes, such as the document.cookie in your JavaScript code.

Remember that the dispute between Web storage vs. cookies is often discussed within the community. While Web storage is more practical, an attacker can gain full access to objects stored there without proper protection from XSS threats.

Cookies, in turn, are a bit safer if you take the right steps to make them so, such as setting the HttpOnly flag.

In short, a cookie that holds session information and is set with the HttpOnly flag can't be accessed from the JavaScript Document.cookie API. This way, only the server will receive it.

Alternatively, we also have the Secure attribute, which will ensure that a cookie is only sent to the server if (and only if) the request happens within HTTPS (never within HTTP). This will make your requests safer in case someone is sniffing them as a man-in-the-middle.

The Threat In Action

Rails take a step for you by automatically setting all cookies with the HttpOnly flag. This is great because it helps unaware developers avoid having their apps hacked.

To test this example, we'd have to disable this feature, which RailsGoat explicitly did within the session_store.rb file, located in the config/initializers folder. Check it out!

Then, go to the registration page once more, fill in the fields properly, and input the following content into the Name field:

<script>
  alert(document.cookie);
</script>

When you submit the form, the user will be created, along with the following subsequent alert message:

Alert message exposing RailsGoat session cookie

Alert message exposing RailsGoat session cookie.

How to Resolve This Issue

In this case, that's pretty straightforward, just make sure not to disable the HttpOnly flag on your Rails apps.

So, remove the httponly: false setting and restart the server. When you try to perform the same operation, the following alert message will be displayed:

Empty alert message

Empty alert message.

Other Scenarios

Imagine that you're accessing a Web application from computers that aren't secure, such as public library computers or LAN houses. If the application is not configured to properly log you out after a specified period of inactivity, then your session will still be there.

Simply closing a browser tab isn't enough to log out of an application.

Another common issue within developers’ code is when you expose IDs of any kind in the front end. It's not that hard to think of scenarios in which keeping a user's ID in a hidden input or even in the URL will make your life easier when the user further requests something else from the server.

However, this is halfway through a stealing attack.

Sensitive Data Exposure

This topic is perhaps one of the most underestimated in terms of dedicating enough effort to ensure the security of sensitive information.

Whether your data is in constant transit or at rest, it's extremely important to separate ordinary data from sensitive data. Sensitive data include credit card numbers and codes, passwords, personal identifier numbers, or anything that relates to compliance or privacy laws (such as the EU's GDPR and the PCI).

Other than that, depending on the specific business area your application is running in, it's important to consult local legislation to determine whether other compliance rules also apply.

Here are some clear examples of this vulnerability:

  • Is your data somehow encrypted or transported as plain text through the Web?
  • If you encrypt data, what algorithm are you using? Is it strong and reliable against the newest types of crypto attacks?
  • Do you use default cryptographic keys if none are provided?
  • Have you checked whether your HTTP requests are enforced by proper security headers?

Let's analyze some of the most common scenarios.

Unsafe Sensitive Storage

If you've been working with Web applications for a while, chances are that you've heard about (or maybe used) the MD5 message-digest algorithm. Although it is still widely used for hashing passwords, it has been proven to be extremely weak.

Here's important to understand the difference between hashing and encrypting information. Encryption is supposed to happen when some sort of key is used to decrypt data. Hashing refers to the method of conversion; you can turn data into a hash but not vice versa.

This applies to all sorts of sensitive information, although MD5 is mostly known for being used with password hashing. If your application keeps the user's Social Security Number (SSN), for example, make sure to not only store them safely within your database but also watch how the data are transmitted through your app to other databases and, especially, the browser.

The Threat In Action

As we've seen, RailsGoat purposely stores the user's password as MD5 hashes. You can see this within the user.rb model:

def hash_password
  if self.password.present?
      self.password = Digest::MD5.hexdigest(password)
  end
end

Every time the user logs in, the app hashes the provided password and checks whether the results are equal. Otherwise, the passwords do not match, so an error is thrown.

How to Resolve This Issue

There are many ways to tackle this issue, but perhaps one of the most famous is through salt hashes, such as the ones provided by BCrypt.

Although Rails comes with default capabilities to deal with Bcrypt, a notorious lib has been widely used for this purpose: bcrypt-ruby:

gem install bcrypt

When you map your model with it, how the password is set and obtained from the database is defined. The lib adapts everything else automatically:

require 'bcrypt'

class User < ActiveRecord

  include BCrypt

  def password
    @password ||= Password.new(password_hash)
  end

  def password=(new_password)
    @password = Password.create(new_password)
    self.password_hash = @password
  end
end

However, the flow requires an additional string column (password_hash) at the table to store the password hash for the BCrypt algorithm.

This way, you can set the password value directly to the model object, and BCrypt will take care of securely hashing it. The same will happen when the password is retrieved to compare with the user's input.

Exposing Too Much

Whether you're working with REST APIs or a GraphQL endpoint, for example, make sure to return only what's necessary to make the client work.

If your JavaScript client requests information from an API and uses only a portion of it, it doesn't impede an attacker from grabbing your endpoint and calling it once again to retrieve the whole response or sniff it out with a proxy tool.

Always review your APIs to certify that regardless of the sensitive information being returned, it will only do so with proper encryption and in the right places.

The Threat In Action

When it comes to user's data, it's important to create secure mechanisms that ensure sensitive information won't be leaked.

Open the users_controller.rb file in the api/v1 folder. There, you will find the following method:

def show
  respond_with @user.as_json
end

As simple as it is, when accessed by the Web client, this endpoint will return all the user's fields populated within the response, including the password.

Not only the user model but also other models that hold sensitive information need a way to select attributes that will be visible to APIs.

How to Resolve This Issue

Luckily, Rails provides a very easy way to deal with it. Let's just override the as_json method with the following:

def as_json
  super(only: [:id, :email])
end

Now, rather than exposing everything by default, we're only responding with the required data. For each model, make sure to select the important fields and apply the same rule of thumb.

Wrapping Up

Today, we navigated through the waters of broken authentication and sensitive data exposure. By following these rules, you'll surely guarantee a much safer application for your users and clients.

Additionally, the importance of going through the official Ruby on Rails Security Documentation can’t be overemphasized. There, you may find more information about session hijacking, storage mechanisms, and strategies to encrypt your data in the best way possible.

See you at our next stop!

What to do next:
  1. Try Honeybadger for FREE
    Honeybadger helps you find and fix errors before your users can even report them. Get set up in minutes and check monitoring off your to-do list.
    Start free trial
    Easy 5-minute setup — No credit card required
  2. Get the Honeybadger newsletter
    Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo

    Diogo Souza

    Diogo is a more of an explorer than a programmer. Most of the best discoveries are made prior to the code itself. if free_time > 0 read() draw() eat() end

    More articles by Diogo Souza
    Stop wasting time manually checking logs for errors!

    Try the only application health monitoring tool that allows you to track application errors, uptime, and cron jobs in one simple platform.

    • Know when critical errors occur, and which customers are affected.
    • Respond instantly when your systems go down.
    • Improve the health of your systems over time.
    • Fix problems before your customers can report them!

    As developers ourselves, we hated wasting time tracking down errors—so we built the system we always wanted.

    Honeybadger tracks everything you need and nothing you don't, creating one simple solution to keep your application running and error free so you can do what you do best—release new code. Try it free and see for yourself.

    Start free trial
    Simple 5-minute setup — No credit card required

    Learn more

    "We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release."
    — Michael Smith, Cofounder & CTO of YvesBlue

    Honeybadger is trusted by top companies like:

    “Everyone is in love with Honeybadger ... the UI is spot on.”
    Molly Struve, Sr. Site Reliability Engineer, Netflix
    Start free trial
    Are you using Sentry, Rollbar, Bugsnag, or Airbrake for your monitoring? Honeybadger includes error tracking with a whole suite of amazing monitoring tools — all for probably less than you're paying now. Discover why so many companies are switching to Honeybadger here.
    Start free trial
    Stop digging through chat logs to find the bug-fix someone mentioned last month. Honeybadger's built-in issue tracker keeps discussion central to each error, so that if it pops up again you'll be able to pick up right where you left off.
    Start free trial
    “Wow — Customers are blown away that I email them so quickly after an error.”
    Chris Patton, Founder of Punchpass.com
    Start free trial