22
Rails Security Threats: Authentication
This article was originally written by Diogo Souza on the Honeybadger Developer Blog.
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!
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 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.
![](https://res.cloudinary.com/practicaldev/image/fetch/s--YPd8x_hK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.honeybadger.io/images/blog/posts/rails-security-authentication-leaks/Figure01.png)
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.
![](https://res.cloudinary.com/practicaldev/image/fetch/s--eN0o7lzE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.honeybadger.io/images/blog/posts/rails-security-authentication-leaks/Figure02.png)
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:
![](https://res.cloudinary.com/practicaldev/image/fetch/s--E5H6u9T0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.honeybadger.io/images/blog/posts/rails-security-authentication-leaks/Figure03.png)
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!
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:
![](https://res.cloudinary.com/practicaldev/image/fetch/s--xKz-9sSY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.honeybadger.io/images/blog/posts/rails-security-authentication-leaks/Figure04.png)
The user doesn't exist.
Otherwise, if the user exists ([email protected], for example) but the password is wrong, the following message is displayed:
![](https://res.cloudinary.com/practicaldev/image/fetch/s--MAXoaJIx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.honeybadger.io/images/blog/posts/rails-security-authentication-leaks/Figure05.png)
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.
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:
![](https://res.cloudinary.com/practicaldev/image/fetch/s--V0eaurLP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.honeybadger.io/images/blog/posts/rails-security-authentication-leaks/Figure06.png)
Invalid credentials.
Another way to do that is by changing the two messages within the users.rb model.
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.
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.
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:
![](https://res.cloudinary.com/practicaldev/image/fetch/s--TH1WxTP5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.honeybadger.io/images/blog/posts/rails-security-authentication-leaks/Figure07.png)
The password is invalid.
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.
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:
![](https://res.cloudinary.com/practicaldev/image/fetch/s--lSUevWLX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.honeybadger.io/images/blog/posts/rails-security-authentication-leaks/Figure08.png)
Alert message exposing RailsGoat session cookie.
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:
![](https://res.cloudinary.com/practicaldev/image/fetch/s--thBBWpz2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.honeybadger.io/images/blog/posts/rails-security-authentication-leaks/Figure09.png)
Empty alert message.
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.
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.
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.
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.
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.
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.
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.
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.
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!
22