Ruby on Rails Security Guide (Tutorial)
Is Ruby on Rails secure?
Like with any framework, the answer to that question is complicated. When using Rails correctly you can build secure apps as it has many features built right into the core functionality. Security, however, is much more than just using a given framework, it mostly depends on developers using the framework and development methods. Let's dive into some examples.
Sample application
Clone the repository https://github.com/mateuszpalyz/rails_security and follow setup guidelines from README.
Now, at http://localhost:3001 we have a sample website where we can start hacking.
CSRF
As the Rails guides say, Cross-Site Request Forgery is an attack which “works by including malicious code or a link in a page that accesses a web application that the user is believed to have authenticated. If the session for that web application has not timed out, an attacker may execute unauthorized commands.”
So what does all that actually mean? Let’s imagine that we have a controller that implements a method for removing a given resource. What would happen if an attacker creates a link that uses that method and convinces us to click on that link, e.g. from email. In this case, Rails default settings kick in and we would get a CSRF token mismatch. That token is included in requests and then validated on the server side.
Of course, we can turn that protection off. Actually, playing with "skip_before_action :verify_authenticity_token" without a deeper understanding of what one is doing is quite a common issue.
Let’s see a live example, visit http://localhost:3001/csrf. This page displays some records from a database and we also have a link to send an email prepared by an attacker. Let’s click that link (it will open up in a browser thanks to letter_opener gem). In the mail, we have two links, one to destroy the action that uses default settings with CSRF protection turned on, and the other one which skips CSRF protection:
protect_from_forgery except: :destroy_csrf_off
def destroy_csrf_off
destroy
end
def destroy_csrf_on
destroy
end
def destroy
@some_record = SomeRecord.find(params[:id])
@some_record.destroy
redirect_to csrf_path
end
from https://github.com/mateuszpalyz/rails_security/blob/master/app/controllers/pages_controller.rb
When we click on the safe link we get when an exception but when we click on the other link, a record from the some_records table is removed. 😨
Cross-Site Scripting (XSS)
Again, quoting the Rails guide on Cross-Site Scripting, “This malicious attack injects client-side executable code.” How can an attacker inject that code? Basically, any place with user input can be vulnerable. Most often XSS can happen in WYSIWYG editors that don't correctly escape input submitted by a user. When code is injected, the attacker can run any malicious code they can think of, e.g. steal cookies. Usually, we're speaking about JavaScript injection but CSS is also vulnerable. Rails protects us from this kind of attack with a user input sanitizer. Let’s see that in action. Visit http://localhost:3001/xss, where we have a string that is potentially dangerous:
<script>alert('oh no, xss :(')</script>
The first link leads to a page where this string is not escaped:
<%= "<script>alert('oh no, xss :(')</script>".html_safe %>
This causes malicious code to be executed.
The second link leads to a page where the string is escaped and simply displayed but no code is executed.
<%= "<script>alert('oh no, xss :(')</script>" %>
from https://github.com/mateuszpalyz/rails_security/blob/master/app/views/pages/xss_free.html.erb#L15
SQL injection
The Rails guide warns, “SQL injection attacks aim at influencing database queries by manipulating web application parameters. A popular goal of SQL injection attacks is to bypass authorization. Another goal is to carry out data manipulation or reading arbitrary data.” This is a serious one and endangers not only the data of a single user but the whole database with any sensitive data that we have there. Let’s dive into an example.
Visit http://localhost:3001/sql_injection. Here, we have a page that displays records from the some_records table (name and priority), and below we have a search input that unfortunately is vulnerable to injection.
@some_records = SomeRecord.where("name like '%#{params.dig(:query, :name)}%'") # with sql injection
from https://github.com/mateuszpalyz/rails_security/blob/master/app/controllers/pages_controller.rb#L39
We interpolate user input directly into the query. If we were to use
@some_records = SomeRecord.where('name like ?', "%#{params.dig(:query, :name)}%") # without sql injection
from https://github.com/mateuszpalyz/rails_security/blob/master/app/controllers/pages_controller.rb#L38 that would have escaped user input and made the query secure (you can test that out later by uncommenting that line and commenting out the L39).
Let’s be nasty now, we could drop the whole some_records table but there will be more fun with getting data out of some other table. Here are some clues: union select can be used here, the other table is called some_other_records and it has the same number of columns as some_records -> 5. Happy hacking! If you need more help, a sample solution is hidden at the bottom of the page, just copy it into the search input.
Summary
As usual, with Rails we have a solid foundation thanks to which we can keep our applications secure. What is important is to know what kind of attack might be performed, what the countermeasures are, and not to fight with what Rails gives us out of the box. That way we can build secure apps. Stay safe.