Thu. Jul 9th, 2020

SVMAKERS.ORG

Shenango Valley Makers

How to Secure Your Rails API Without Being a Security Expert

Ruby on Rails is such a sweet tool to use. As a full-stack developer, I find it my framework of choice whenever I need to build a prototype/minimum-viable product. Its ease of use and quick setup allow me to move forward quickly into building the front-end of my projects.

This convenience can cause a developer to overlook the security aspects of their code. I know I’m guilty of it a lot of times. Even if one is accustomed to Test-Driven-Development, it doesn’t guarantee that the code they write is secure.

Personally, I never really paid much attention to security whenever I built apps. The reason is that I focus on building quick prototypes and minimally viable products (MVPs). But that’s not an excuse to write sloppy code that can easily open up users to attacks.

This is why, at the very least, as engineers, we have to be aware and be able to fix the most common vulnerabilities that we may accidentally put in our projects.

Open-source databases such as WhiteSource’s Vulnerability Lab provide you with updated information on most vulnerabilities.

You can learn about each vulnerability in greater detail including its severity and what fixes are available. Should you also discover new vulnerabilities, you can reach out to its maintainers for them to confirm and add it to their database.

In this article I’m going to run down the three most common vulnerabilities one may encounter in a Ruby on Rails API only app. However, some of the concepts presented apply regardless of which platform you’re using and you should investigate what similar tooling is available for your platform if you’re not on Rails.

I limit the topic to JSON-API projects since I personally would use Rails to only build APIs (and use React or any other front-end framework to build the client-side).

Session Hijacking

In a Rails API app, a common way to do authentication is by the generation of a JSON-Web Token (JWT). Every time a `POST` request is made to the authentication endpoint (usually `/sessions`), the Rails app can generate the JWT from the current user’s ID and an expiration timestamp (usually 24 hours).

An example of how to generate this JWT is using Ruby’s `jwt` gem`:

def self.encode(payload, exp = 24.hours.from_now)
   payload[:exp] = exp.to_i
   JWT.encode(payload, SECRET_KEY)
 end

The JWT will be sent as part of the response to the front-end app.

For authenticated requests (such as getting a list of all the current users’ friends in a social networking application), the Rails app should check the request’s headers for the presence of this JWT.

If the request doesn’t contain any JWT or an expired JWT, then the API will simply respond with a 401 (Unauthorized) error.

During authentication:

If the request doesn't contain any JWT or an expired JWT, then the API will simply respond with a 401 (Unauthorized) error.

Subsequent requests that require a requester to be authenticated:

Subsequent requests that require a requester to be authenticated:

The issue here is that assuming the JWT hasn’t expired yet, a hacker may be able to steal the JWT and access the API as an authenticated user. API authentication tokens have been exploited like this in several real-world attacks.

In this case, the only measure against this is making the JWT expire sooner. So instead of 24 hours, the JWT can be made to expire in 1 hour.

The good news is that the only way for a hacker to steal this token is either by the user accidentally (or foolishly) sending this token through the web or if the hacker physically goes to the machine that this token is in (at the client-side most commonly an auth token is saved at the localstorage).

Still, we want to cover as much ground as possible in regards to the security of any application we build.

So, an alternative but more cumbersome way to store our auth token is for the Rails API to save each JWT generated into a database. Here’s how that logic goes:

  1. An additional database table (and model) called `tokens` will be generated
        $ rails g model token value:string
  2. Every time a successful login has been made, the Rails API will generate a new JWT and save it as a record in the `tokens` table
        def self.encode(payload, exp = 24.hours.from_now)
           payload[:exp] = exp.to_i
           token = JWT.encode(payload, SECRET_KEY)
           Token.create(value: token)
        end
  3. This same JWT will be sent to the requester as part of the response.
  4. For requests that are only allowed for authenticated users, the Rails API will check for the existence of a JWT in the headers. Automatically the response will be a 401 (Unauthorized) if the headers don’t contain any.
  5. If the JWT is present in the headers, the first thing that the Rails API will do is check for its existence in the `tokens` database table. If it cannot be found then a 401 will be sent as response.
  6. If the JWT exists in the `tokens` table, then the Rails API will try to decode this (using a custom method). If it’s invalid (i.e, expired) then again a 401 response will be sent.
  7. If the JWT is valid, then a success response will be sent along with the data requested (or appropriate actions will be taken in the API).

We can write a method in the `application_controller.rb` file to check for the existence of the token in the headers, then in the database, and subsequently for its validity.

Using Ruby’s `pundit` gem`:

def pundit_user
   header = request.headers['Authorization']
   header = header.split(' ').last if header
   begin
    if Token.find_by(value: header)
             decoded = JsonWebToken.decode(header)
             return User.find(decoded['id'])
      end
    decoded = JsonWebToken.decode(nil)
   rescue ActiveRecord::RecordNotFound
     render json: { message: 'You are not authorized' }, status: :unauthorized
   rescue JWT::DecodeError
     render json: { message: 'Unauthorized access' }, status: :unauthorized
end

Drawing this sequence in a flowchart:

Drawing Ruby's pundit gem sequence in a flowchart

The only drawback for this solution is that every single login of the user will add a new record into the `tokens` table. For the lifetime of each user, this may mean thousands to millions of records depending on the frequency of login.

A possible fix is to have an automated database cleanup. But this is an article about security vulnerabilities and fixes.

However, the benefit of this approach is that we can create a method that allows us to log out all devices by destroying all records in the `tokens` table:

def logout_all
   Token.destroy_all
 end

As a final note, there is still of course a very real threat that hackers can get inside the database and steal the tokens. The best way to ensure that the JWTs are secured in a way that makes them useless to hackers is by using digital signatures.

SQL Injection

This particular security vulnerability doesn’t only affect Rails apps. Every web (or mobile) application that does SQL queries over the internet are susceptible to this.

In simple terms, an SQL injection attack happens whenever a malicious user manipulates request parameters in order to access database content.

For example, let’s say we have a database table called `users` (and a corresponding `User` model). Let’s also say that the way we coded a query to get a particular user’s data is as follows:

User.where("first_name = '%#{params[:first_name]}%'")

So the following query string in the URL will return a collection user with the name “Michael”:

/users?first_name=Michael

However, a malicious user will simply “inject” any string value in this query string in order to extend the SQL query statement in the first line. Doing so could allow this malicious user to access all users and be able to do what they want with that data.

Due to its prevalence, one would think that the maintainers of Ruby on Rails would have an out-of-the-box safeguard for this. Unfortunately, it doesn’t.

What’s great though is that we can secure our Rails APIs against this vulnerability through simple tweaks in our ActiveRecord queries.

We can then revise our SQL query above to make it more secure:

User.where("first_name = ?", params[:first_name])

Aside from the obvious syntactical difference, how else is this newly formed SQL query any different from the aforementioned pure stringed one?

In the pure string query, which is unsafe, every single bit is being passed into the database as-is.

So a hacker might pass an extended SQL query string, as previously mentioned, to get a list of all user data so they could either sell or delete (whatever they fancy).

In the revised SQL query, the second argument is dynamic (since Rails will “escape” it) and won’t go directly into the database before being sanitized by Rails. This incapacitates any malicious attempts by a hacker to access the database through query strings.

Authorization and Access Vulnerabilities

Authenticating a user is simply checking whether the user that is making a request is logged in. Authorization is providing another layer of data access protection.

Authorizing a user means providing only a certain level of access to features depending on what category of user is making such requests.

For instance, message exchanges between two users should be made private between them. Therefore, an authorization system should be in place that will check whether an app user trying to access conversation data is a participant in that conversation.

I listed this vulnerability last because it can be caused more by architectural decisions than code. There are certain best practices that we can employ to ensure our API has a high-quality authorization system. But probably the simplest architectural measure is providing the least access by default.

For example, the only default access all users should have is their own data.

As the user is given additional privileges (i.e when they become administrators) that’s the time they are also provided access to additional information or features.

Using `pundit` gem

Fortunately, as Rails developers we can use `pundit`. Pundit allows us to easily create “policies” that restrict the kinds of requests users can make depending on certain model attributes.

For more advanced policies the developer can simply create new classes. The resulting policies are also very easily tested.

Conclusion

There are a thousand types of vulnerabilities any project can have. All of the three I listed here are easily detected. But there are hundreds more that are due to the types of dependencies we install into our projects.

Luckily for us if we’re pushing our code to Github we will get notices should any vulnerability be detected in any of our code’s dependencies.

e're pushing our code to Github we will get notices should any vulnerability be detected in any of our code's dependencies.

What’s even more fascinating is that Github already provides us with the solution:

What's even more fascinating is that Github already provides us with the followng solution:

The main message I wish to impart in this article is for us developers to start becoming more security-oriented, from staying on top of Github security updates to programming language vulnerabilities.

The field of security may be an entirely new discipline altogether for most of us but we don’t have to be experts to be able to build secure apps.

Go to Source
Author: <a href="https://www.programmableweb.com/user/%5Buid%5D">Anton-Lawrence</a>