This guide will give you an introduction to some security aspects of backend and fullstack frameworks
Slides - use arrow keys to navigate, esc to return to page view, f for fullscreen
Injection flaws, such as SQL, NoSQL, OS, and LDAP injection, occur when untrusted data is sent to an interpreter as part of a command or query. The attacker's hostile data can trick the interpreter into executing unintended commands or accessing data without proper authorization. OWASP Wiki
If you use your ORM correctly you should be save. But make sure to find out what you ORM does or does not do!
A good ORM like ActiveRecord will protect against SQL-Injection if used correctly.
For ActiveRecord: if you use the methods find
or where
without string interpolation
Rails will turn them into prepared statements - since Rails 3.1, 2011.
You can see this in the Rails console: The SQL statements contain placeholders $1, $2, and the bound values are supplied separately:
Project.find(42)
# SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2
# [["id", 42], ["LIMIT", 1]]
Project.where(title: params[:title])
# SELECT "projects".* FROM "projects" WHERE "projects"."title" = $1
# [["title", "Marios World"]]
Project.where("publication_date > ?", 1.year.ago)
# SELECT "projects".* FROM "projects" WHERE (publication_date > '2018-06-03 12:15:54.952581')
But if you use string interpolation to build up SQL queries, you open up your application to injection attacks. An example that is vunerable:
@projects = Project.where("title = '#{params[:title]}'")
# SELECT "projects".* FROM "projects" WHERE (title = 'Marios World')
If a malicious user enters ' OR ''='
as the name parameter, the resulting SQL query is:
SELECT "projects".* FROM "projects" WHERE (title = '' OR ''='')
As you can see the SQL Fragment was incorporated into the SQL query before the string was handed to ActiveRecord. The resulting query returns all records from the projects table. This is because the condition is true for all records.
Application functions related to authentication and session management are often implemented incorrectly, allowing attackers to compromise passwords, keys, or session tokens, or to exploit other implementation flaws to assume other users' identities temporarily or permanently. OWASP Wiki
You should check if your backend framework has the following features, or if you need to add them through packages.
Rails comes with basic built in functionality to handle authentication:
For most real world projects you will be using a gem:
We discussed using this in the chapter on Rails Authentication.
Use the gem pwned
to access an API that will tell you if a password is too common and has
been featured in password lists:
# Gemfile
gem "pwned"
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
validates :password, not_pwned: true
end
When implementing these features you can write integration tests to make sure they actually work.
test "can create user with password ljkw8723kjasf889r" do
get "/sign_up"
assert_response :success
assert_difference('User.count',1) do
post "/users", params: {
user: {
name:"Me Stupid",
email:"peter@prayalot.com",
password:'ljkw8723kjasf889r',
homepage:'https://some.where'
}
}
end
assert_select 'span', text: 'has previously appeared in a data breach and should not be used', count: 0
follow_redirect!
assert_select 'li', text: /Me Stupid/
end
Many web applications and APIs do not properly protect sensitive data, such as financial, healthcare, and Peronally Identifiable Information ... Sensitive data may be compromised without extra protection, such as encryption at rest or in transit, and requires special precautions when exchanged with the browser. OWASP Wiki
The OWASP advises: Determine the protection needs of data in transit and at rest. For example, passwords, credit card numbers, health records, personal information and business secrets require extra protection, particularly if that data falls under privacy laws, e.g. EU's General Data Protection Regulation (GDPR), or regulations, e.g. financial data protection such as PCI Data Security Standard (PCI DSS).
You should check if your backend framework has the following features, or if you need to add them through packages.
From Rails version 7 on ActiveRecord offers encryption. For older versions you can use the attr_encrypted gem to encrypt certain attributes in the database transparently.
While choosing to encrypt at the attribute level is the most secure solution, it is not without drawbacks. Namely, you cannot search the encrypted data, and because you can't search it, you can't index it either. You also can't use joins on the encrypted data.
By default, Rails logs all requests being made to the web application. You can filter certain request parameters from your log files by appending them to config.filter_parameters
in the application configuration. These parameters will be replaced by "[FILTERED]" in the log.
Started POST "/user/sign_in" for 127.0.0.1 at 2021-01-05 08:46:01 +0100
Processing by Devise::SessionsController#create as HTML
Parameters: {"utf8"=>"✓", "user"=>{"email"=>"brigitte.jellinek@fh-salzburg.ac.at", "password"=>"[FILTERED]", "remember_me"=>"1"}
In"}
# in initializers/filter_parameter_logging.rb
Rails.application.config.filter_parameters += [:password]
Provided parameters will be filtered out by partial matching regular expression. Rails adds default :password
in the appropriate initializer, which will take care of password
and password_confirmation
.
In the appropriate environment(s) force ssl:
# in config/environments/production.rb
# Force all access to the app over SSL,
# use Strict-Transport-Security, and use secure cookies.
config.force_ssl = true
This will do three things:
See this blog article for more details on configuring this
Restrictions on what authenticated users are allowed to do are often not properly enforced. Attackers can exploit these flaws to access unauthorized functionality and/or data, such as access other users' accounts, view sensitive files, modify other users' data, change access rights, etc. OWASP Wiki
As a programmer you need to
The simplest way is to use current_user
to control access to models.
Instead of simply loading data:
@project = Project.find(params[:id])
Instead, query the user's access rights, too:
@project = current_user.projects.find(params[:id])
For more complex setups with different roles that have different permissions
use a gem like cancancan
which will let you define access in a declarative way and give you an authorize!
method for controllers:
@project = Project.find(params[:id])
authorize! :read, @project
In some circumstances you need to have a resource that is available to anyone with the URL - think google docs or doodle.
But you do not want users to be able to enumerate all possible URLs:
https://my-schedule.at/calendar/17
https://my-schedule.at/calendar/18
https://my-schedule.at/calendar/19 ...
To avoid the enumeration attack you can switch from using serial/autocincrement as the primary key in the database to using UUIDs. Then the URLs will look like this:
https://my-schedule.at/calendar/0d60a85e-0b90-4482-a14c-108aea2557aa
https://my-schedule.at/calendar/39240e9f-ae09-4e95-9fd0-a712035c8ad7
https://my-schedule.at/calendar/a3240e9e-1209-4e95-9fd0-a712035c8ad4 ...
In postgresql you can use the extentions pgcrypto or uuid-ossp
CREATE EXTENSION pgcrypto;
CREATE TABLE calendar( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT );
INSERT INTO calendar (name) VALUES
('meeting on grdp'),
('security audit');
SELECT * from calendar;
0d60a85e-0b90-4482-a14c-108aea2557aa | meeting on grdp
39240e9f-ae09-4e95-9fd0-a712035c8ad7 | security audit
Rails can handle all this for you:
# in config/application.rb
config.generators do |g|
g.orm :active_record, primary_key_type: :uuid
end
Browsers restrict cross-origin HTTP requests initiated by scripts. For example, XMLHttpRequest and the Fetch API follow the same-origin policy. This means that a web application using those APIs can only request HTTP resources from the same origin the application was loaded from, unless the response from the other origin includes the right CORS headers.
(Text and Image originally from MDN by many contributres is licensed under CC BY-SA 2.5
In the most simple case, when doing a cross-origin request:
Origin
headerAccess-Control-Allow-Origin
headerfor example:
GET /resources/data.json HTTP/1.1
Host: bar.other
Origin: https://foo.example
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-Type: application/json
If a scripts tries to fetch a ressource cross-origin from server X and no
Access-Control-Allow-Origin
Header
is set on server X, then the browser will throw an error and not
continue with the script:
fetch("https://iou-brigitte.herokuapp.com/users.json")
.then(function(response) {
return response.json();
})
.then(function(data) {
console.log(data);
});
After setting the right Headers for the HTTP Response, the request goes through:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET
In Rails you
can use the rack-cors
gem to set the Header in middleware:
# Gemfile
gem 'rack-cors'
The configuration is done in an initializer:
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '/*', headers: :any, methods: :get
end
allow do
origins 'localhost:3000', '127.0.0.1:3000', 'https://my-frontend.org'
resource '/api/v1/*',
methods: %i(get post put patch delete options head)
end
end
In this example get-requests are allowed for all origins, while using the full api is only allowed for three specific domains.
Security misconfiguration is a result of insecure default configurations, incomplete or ad hoc configurations, open cloud storage, misconfigured HTTP headers, and verbose error messages containing sensitive information. Not only must all operating systems, frameworks, libraries, and applications be securely configured, but they must be patched/upgraded in a timely fashion. OWASP Wiki
This is especially relevant if you are running your own virtual machine:
You should check if your backend framework has the following features, or if you need to add them through packages:
The files config/database.yml
and config/secrets.yml
should not be added
to the repository - unless you extract out all the secrets into environment
variables (as a 12 factor app)
# in config/database.yml
default: &default
adapter: postgresql
encoding: unicode
# For details on connection pooling, see Rails configuration guide
# https://guides.rubyonrails.org/configuring.html#database-pooling
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
development:
<<: *default
database: myapp_development
test:
<<: *default
database: myapp_test
production:
<<: *default
url: <%= ENV['DATABASE_URL'] %>
Rails 5.2 and later generates two files to handle credentials (passwords, api keys, ...):
config/credentials.yml.enc
to store the credentials within the repoconfig/master.key
or ENV["RAILS_MASTER_KEY"]
to read the encryption key fromThe master key is never stored in the repo.
To edit stored credentials use rails credentials:edit
.
By default, this file contains the application's
secret_key_base
, but it could also be used to store other credentials such as
access keys for external APIs.
The credentials are accessible in
the running Rails app via Rails.application.credentials
.
For example, with the following decrypted config/credentials.yml.enc
:
secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
some_api_key: 123454321
Rails.application.credentials.some_api_key
returns 123454321
in any environment.
If you want an exception to be raised when some key is blank, use the bang version:
Rails.application.credentials.some_api_key!
# => raises KeyError: :some_api_key is blank
XSS flaws occur whenever an application includes untrusted data in a new web page without proper validation or escaping, or updates an existing web page with user-supplied data using a browser API that can create HTML or JavaScript. XSS allows attackers to execute scripts in the victim's browser which can hijack user sessions, deface web sites, or redirect the user to malicious sites. OWASP Wiki
You already know about escaping data when using it in HTML.
Your view layer probably does this automatically
The modern solution to XSS ist a Content Security Policy(CSP)
erb automatically escapes for HTML:
<%= @article.title %>
This escaping is not apporpriate for attributes:
<!-- DANGER, do not use this code -->
<p class=<%= params[:style] %> >...</p>
<!-- DANGER, do not use this code -->
An attacker could insert a space into the style parameter like so: x%22onmouseover=alert('hacked')
resulting in the following html
<!-- DANGER, do not use this code -->
<p class=x onmouseover=alert('hacked') >...</p>
<!-- DANGER, do not use this code -->
To construct HTML Attributes that are properly escaped
it is easiest to use view helpers like tag
and content_tag
:
<%= content_tag :p, "...", class: params[:style] %>
When transferring data from the backend to the frontend
through JSON you need to use json_encode
with the additional
option
<script>
const attributes = <%= raw json_encode(@attrs, escape_html_entities_in_json = true) %>
</script>
See brakeman for the rationale.
When building a JSON API use jbuilder
or active_model_serializers
as described in chapter APIs.
JSX automatically escapes for html:
If you want to output a string that should be interpreted as HTML you need to use dangerouslySetInnerHTML
It is either defined with the HTTP Header Content-Security-Policy
or through
the meta
tag.
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'" />
The post basic policy would be
default-src 'self'
This policy will ensure that
<script>
and onevent
attributes cannot be usedeval
, setImmediate
and window.execScript
cannot be usedThe first point is explicitly set by the directive. Point 2 and 3 are always in effect
when using CSP. You can explicitly allow unsave-inline
and unsave-eval
, if you want to
disable 2 and 3.
In Rails you can configure the Content Security Policy for your application in an initializer.
Example global policy:
# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
policy.default_src :self
policy.font_src :self, 'https://fonts.gstatic.com'
policy.img_src '*'
policy.object_src :none
policy.script_src :self, 'https://code.jquery.com'
policy.style_src :self, 'https://fonts.googleapis.com'
# Specify URI for violation reports
policy.report_uri "/csp-violation-report-endpoint"
end
This automatically forbids all 'unsave-inline' script: <script>
-tags
in the html code and event-handler-attributes like <button onclick=...>
.
To allow certain <script>
-tags in your code you can give
them a "nonce":
<script nonce="2726c7f26c">
var inline = 1; // good javascript
</script>
This must be the same nonce given in the CSP:
Content-Security-Policy: script-src 'nonce-2726c7f26c'
Rails can generate separate nonces for separate sessions automatically, see the Rails Security Guide.
If you want to handle violation reports, you need to set up a model, controller and route as described here.
Components, such as libraries, frameworks, and other software modules, run with the same privileges as the application. If a vulnerable component is exploited, such an attack can facilitate serious data loss or server takeover. Applications and APIs using components with known vulnerabilities may undermine application defenses and enable various attacks and impacts. OWASP Wiki
In the last years security features have been incorporated in gem, npm, .... Many, many malware packages are submitted to the repositories. Here an Example:
Before using a gem, look it up in the ruby-toolbox and check if it is actively maintained and if there are better alternatives.
There are several tools that check for vulnerabilities in dependencies:
Use bundle update --conservative gem_name
to safely update vulnerable dependencies.
When using script-tags to include javascript (e.g. bootstrap, react) from a cdn use Subresource Integrity checks. This way, if a hacker manages to change the script on the CDN your application will not be affected:
<script
src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
(This example from jquery also includes the CORS attribte crossorigin
set to anonymous
.
This way no user credentials will every be sent to code.jquery.com
).
This security problem used to be No 8 on the list, but was no longer listed in the 2017.
A CSRF attack forces a logged-on victim’s browser to send a forged HTTP request, including the victim’s session cookie and any other automatically included authentication information, to a vulnerable web application. This allows the attacker to force the victim’s browser to generate requests the vulnerable application thinks are legitimate requests from the victim. OWASP
First use GET and POST appropriately. Secondly, a security token in non-GET requests will protect your application from CSRF.
Rails can handle this for you
To protect against all other forged requests, we introduce a required security token that our site knows but other sites don't know. We include the security token in requests and verify it on the server. This is a one-liner in your application controller, and is the default for newly created Rails applications:
# in app/controller/application_controller.rb
protect_from_forgery with: :exception
This will automatically include a security token in all forms and Ajax requests generated by Rails. If the security token doesn't match what was expected, an exception will be thrown.
By default, Rails includes an unobtrusive scripting adapter,
which adds a header called X-CSRF-Token
with the security token on every non-GET
Ajax call. Without this header, non-GET Ajax requests won't be accepted by Rails.
When using another library to make Ajax calls, it is necessary to add the security
token as a default header for Ajax calls in your library.
Note that cross-site scripting (XSS) vulnerabilities bypass all CSRF protections. XSS gives the attacker access to all elements on a page, so they can read the CSRF security token from a form or directly submit the form.