Sinatra ERB XSS escaping
While Rails escapes HTML by default, Sinatra requires some handholding to produce a similar protection. The following describes two simple solutions.
Setup
Given a basic Sinatra application
# lib/app.rb
class App < Sinatra::Base
get '/users' do
@users = User.all
erb :users
end
end
and a corresponding ERB template
<%# lib/views/users.erb %>
<% @users.each do |user| %>
<tr>
<td><%= user.id %></td>
<td><%= user.name %></td>
</tr>
<% end %>
any user-defined input will show the unescaped value. This setup is immediately vulnerable to persistent XSS attacks. For instance, a User
with a name
like <script src=http://www.example.com/malicious-code.js></script>
will appear as:
<tr>
<td>1</td>
<td><script src=http://www.example.com/malicious-code.js></script></td>
</tr>
Offensive solution
Use ERB::Util#h
on the specific inputs that should be escaped.
- include
ERB::Util
# lib/app.rb
class App < Sinatra::Base
include ERB::Util
end
- Explicitly escape desired inputs
- <td><%= user.id %></td>
+ <td><%= h user.name %></td>
This produces the desired result of:
<tr>
<td>1</td>
<td><script src=http://www.example.com/malicious-code.js></script></td>
</tr>
While this solution is very simple, if a developer forgets to escape a value the site becomes vulnerable.
Defensive solution
Assume all inputs are possible sources of attack and escape by default.
- Add
erubis
to your Gemfile
gem 'erubis'
- Add
escape_html
ERB setting in Sinatra
class App < Sinatra::Base
set :erb, { escape_html: true }
end
This produces the desired result of:
<tr>
<td>1</td>
<td><script src=http://www.example.com/malicious-code.js></script></td>
</tr>
without alterations to the ERB file.
Note: There are a few cases when you absolutely do not want to render inputs as escaped. In order to get raw inputs use ==
.
- With a
yield
in a layout
# lib/views/layouts/application.erb
<body>
<%== yield %>
</body>
- Partial templates
<% users.each do |user| %>
<%== erb ':user/show', user: user %>
<% end %>