Where did I get these risks?
The OWASP (Open Web Application Security Project) Community launches a list of the top 10 biggest internet security-risks every year. This post will cover them and also give a demonstration to each one. All the code for the demonstrations is open source. Feel free to fork and add your own demonstrations!
Demonstration
Without further ado let's demonstrate and explain every risk.
1. Broken access control
This is OWASPs first and most important security risk: The broken access control. This risk includes everything that involves bad role management which allows users to act outside of their own permissions. As an example for that we have the /admin
route. While yes you do need to be logged in to view the page, you don't need to be an admin.
Other common cases of this issue contain: viewing someone elses account by providing their unique identifier (being able to view /users/5
when you are user 6
), CORS misconfiguration allowing requests from unwanted sources, etc.
To fix these issues your application should go beyond just hiding links when someone does not have permissions to access a page and also redirect them if they lack permissions. In rails that could look as follows:
def index
unless user_signed?
unless current_user.role == 'admin'
redirect_to new_user_session_path
end
end
end
In production code you would most likely use a library for access control, such as CanCanCan
2. Cryptographic failures
This risk is more of a symptom than a cause. It focuses on a wide range of mistakes the coder can do when he works with sensitive data. Most important to consider are: Is sensitive data being stored/transmitted in plain text? Is a wrong/old encryption algorithm used? Are the same/weak encryption keys being used?
This risk I already talked about and demonstrated in another tutorial called how (not) to store passwords so I will not go into it further in this article.
3. Injection
Injection is one of the most well known but sadly also most often ignored security risks. You can try it out yourself under /items
. There we have cars, from which certain ones are public and other ones that aren't. Now we can write an injection to display every car, no matter if it's public or not. Below the field we have some help to show us what the query at the end will look like. Our input will be Mercedes' OR '1'= '1
so that the query will look as follows in the end:
SELECT "items".* FROM "items" WHERE (public = true AND name= 'Mercedes' OR '1'= '1')
This selects every car, independent of its public status, because in the end it compares if '1' = '1'
, which is true for every car.
To fix this we sanitise the queries. Rails does that for us, if we query like this:
def index
if params[:query]
@query = params[:query]
@items = Item.where(public: true)
@items = @items.where('name LIKE ?', "%#{@query}%")
else
@items = Item.where(public: true)
@query = ""
end
end
4. Insecure design
Insecure design lays its focus on all software that is insecure in its nature rather than its implementation. While the other risks can be averted by implementing securely you have to rethink your design to fix this flaw.
An example given by OWASP itself is a cinema that allows the reservation of 15 seats before they want a deposit. A hacker could now just reserve up to 14 seats at every cinema or maybe even more under a 2nd name causing the cinema to lose a lot of money.
Our example is on /insecure-login
. There we have a few users, which have the emails insecure1@example.com
all through insecure5@example.com
whose passwords are all 123456
. Now if we go onto /insecure-login
and try to log into an account with any of those E-Mails I will see that the website tells me The password does not match the email
. That is a serious problem, because it gives a hacker the power of finding out which emails exist making a brute force process a lot easier.
Issues in this category are very hard to not run into.
5. Security Misconfiguration
This is a very wide term and with that also very hard to pinpoint. It contains every risk caused by bad configuration rather than faulty code. Common problems include: Having a default password oder username, leaving debugging features on in production code, unnecessary pages or features are left in.
Our example is at the /insecure-login
route again. When a user provides his E-Mail the Server prints the entire user onto the console. Something that I used to debug the website when creating the login system and now something that is a huge security risk. A hacker that can access my server logs now doesn't even have to get into my database to get access to the user data - I gave it to him for free. While peppering and salting makes this a lot less of a risk it is still an unnecessary risk.
This (existing) user has its details printed onto the console:
We are lucky that Rails is very good at security by default, so the users password gets filtered out.
6. Vulnerable and outdated components
This one I didn't demonstrate because it's a bit more complicated to do so.
This security flaw focuses on the use of old and flawed components. A famous example of this problem was the Log4Shell
remote code execution. What happened was, that the logger had a "message substitution" feature which made it possible to change event logs programatically with strings that call for external resources. Hackers used that to execute remote code onto the servers that were vulnerable, such as, but not limited to: Twitter, Cloudflare and Steam.
To fix problems like these you should have some sort of built in automation like renovate that updates your dependencies. What most web frameworks also have is a vulnerability checker. In rails its Brakeman
and in React it is built into the application itself, which can be executed using npm audit
.
7. Identification and Authentication failures
This vulnerability is concerned with flaws in the login and session handling process, such as allowing brute force or other automated attacks, allowing weak passwords, knowledge based answers for password revocery or exposure of the session identifier in the URL.
For our demonstration we want to brute force an account on /insecure-login
. The brute force code is on GitHub. If we execute it will open an instance of the browser and try to brute force the password. When it finds the correct password it writes that into passwords.txt
.
This should be prevented by someone that is trying to create a web-app, by, for example, limiting the amount of login tries a person can do or implementing a captcha.
8. Software and Data Integrity Failures
Software and data integrity failures explicitly focus on software that does not check if the sent datas integrity was kept in transit. That means to always check that the data you are sending and receiving hasn't been tampered with in any unexpected way. Another thing to keep in mind is the auto-update functionality of software. A security flaw I already had the displeasure of coming across was on a WordPress website which updated its libraries automatically. On one of those someone injected malicious code and the WordPress website just fetched and updated it without thinking about it.
This one is also rather hard to demonstrate on my web-app, but PwnFunction has a great video on it. Check it out if you are interested
9. Security logging and monitoring failures
This flaw is not preventing a certain type of attack, but rather just missing logging. Sufficient monitoring can help to prevent various attacks like the brute force attack from before. If there was logging someone would maybe have recognised that brute forcing is possible a long time ago.
10. Server-side request forgery (SSRF)
Server side request forgery is when a bad actor is able to execute remote code onto our server by passing weird parameters.
Our example is something you probably wouldn't see in reality. The server has the /ssrf
route, which takes a parameter called query
and evaluates it directly as a command. We can take advantage of that by running our code on the server. For example: we want to create our own admin account, so that we have full access to the server. We can do that by passing /ssrf?query=http://localhost:3000/ssrf?query=InsecureUser.create(email:"malicious@mail.com",password:"personal_password")
into the search window. If we take a look into our console we can see: A user was created.
How was the application created?
Project setup
First, we create our rails-app by typing rails new security-risks-demo
. Then we directly add the devise
gem by typing bundle add devise
. If you aren't familiar with the devise gem in rails, I wrote an article about it here. For everyone else: Next run the generator with rails generate devise:install
. Then we get prompted to create a few things, which we will do now. With rails g controller home index
we create a controller with an index function. Then we go into our routes.rb
file. In there we define our root
route like this:
Rails.application.routes.draw do
root to: "home#index"
end
Next we will hop into our application.html.erb
and add following code inside our body tag:
<body>
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
<!-- ... -->
</body>
Then we need to generate the user model with rails generate devise user
and migrate it with rails db:migrate
. Lastly we create the views with rails g devise:views
because we intent to customise them later.
A bug I ran into quite often when using devise was, that it crashed when I registered a user. The simple fix for that is to add config.navigational_formats = ['*/*', :html, :turbo_stream]
into your initializers/devise.rb
.
Next we add Bootstrap to make our styling easier. For the sake of simplicity we just add the tags into out application.html.erb
:
<!DOCTYPE html>
<html>
<head>
<title>SecurityRisksDemo</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
</head>
<body>
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
<%= yield %>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
</body>
</html>
Finally we create our home/index.html.erb
page:
<div style="height: 100vh; width: 100vw" class="d-flex justify-content-center align-items-center">
<div class="card">
<div class="card-body">
<h5 class="card-title">Actions</h5>
<% if user_signed? %>
<h6 class="card-subtitle mb-2 text-muted">Logged in as <%= current_user.email %></h6>
<%= button_to "Log out", destroy_user_session_path, method: :delete, class: "btn btn-danger" %>
<% else %>
<h6 class="card-subtitle mb-2 text-muted">You are not logged in</h6>
<%= link_to "Log in", new_user_session_path, class: "btn btn-primary" %>
<%= link_to "Sign up", new_user_registration_path, class: "btn btn-primary" %>
<% end %>
</div>
</div>
</div>
1. Broken access control
For this demonstration I first had to create a controller with rails g controller brokenAccessControl index
and change the get broken_access_control/index
to get 'admin', to 'broken_access_control#index'
. Next we add following html to broken_access_control/index.html.erb
:
<h1>Welcome to the admin dashboard</h1>
<p>
This place you should only be able to access if you
are an admin. Here are some admin only actions:
</p>
<button class="btn btn-danger">
Delete all users
</button>
<button class="btn btn-danger">
Crash the server
</button>
In the controller we want to make sure that the user is logged in, while we ignore the admin check:
class BrokenAccessControlController < ApplicationController
def index
unless user_signed?
redirect_to new_user_session_path
end
end
end
2. Cryptographic failures
The demonstration wasn't done in this article, but rather in how (not) to store passwords.
3. Injection
This risk is so easy to create that it's also easy to fall into. First we need to create our model item: rails g model item name:string price:decimal
and write it to our database with rails db:migrate
. Then we also need some items, which we will create in our seeds.rb
file:
Item.create(name: 'Mercedes', price: 144000, public: true)
Item.create(name: 'BMW', price: 86000, public: true)
Item.create(name: 'Audi', price: 72000, public: true)
Item.create(name: 'VW', price: 51000, public: false)
Item.create(name: 'Opel', price: 33000, public: false)
Item.create(name: 'Fiat', price: 28000, public: true)
Item.create(name: 'Renault', price: 23000, public: false)
Item.create(name: 'Ford', price: 22000, public: true)
Item.create(name: 'Toyota', price: 15000, public: true)
Item.create(name: 'Honda', price: 30000, public: false)
And then we seed it with rails db:seed
. Next comes our view, which we create with rails g controller item index
and change get 'item/index'
to get 'items', to: 'items#index'
in our routes.rb
.
Next we go into our item_controller.rb
where we check if there is a param and fetch the items according to their name if there is a parameter:
def index
if params[:query]
@items = Item.where("public = true AND name= '#{params[:query]}'")
@query = params[:query]
else
@items = Item.all
@query = ""
end
end
And then we show those results and a field for the query in the view:
<h1>Items</h1>
<%= form_with url: '/items', method: :get do |form| %>
<%= form.text_field :query, id: 'search' %>
<%= form.submit 'Search' %>
<% end %>
<% if @items.length > 0 %>
<table class="table table-striped">
<thead>
<tr>
<th>name</th>
<th>price</th>
</tr>
</thead>
<% @items.each do |item| %>
<tr>
<td><%= item.name %></td>
<td><%= item.price %></td>
</tr>
<% end %>
</table>
<% else %>
<p>No items found.</p>
<% end %>
An extra I added is that the user sees a live-updating text where their full query is listed. That is just a bit of html, css and javascript:
<!-- ... -->
<h6 id="query-wrapper">
SELECT "items".* FROM "items" WHERE (public = true AND name= '
<span id="query"><%= params[:query] %></span>
')
</h6>
<!-- ... -->
<script>
let query = document.getElementById('query')
let input
document.getElementById('search').addEventListener('input', (e) => {
input = e.target.value
query.innerHTML = input
})
</script>
<style>
#query-wrapper {
width: fit-content;
margin-top: 10px;
background: #f4f4f4;
border: 1px solid #ddd;
border-left: 3px solid #f36d33;
color: #666;
page-break-inside: avoid;
font-family: monospace;
font-size: 15px;
line-height: 1.6;
margin-bottom: 1.6em;
max-width: 100%;
overflow: auto;
padding: 1em 1.5em;
display: block;
word-wrap: break-word;
}
</style>
4. Insecure design
Step one is to create the controller with its model. For that we type rails g model insecure_user email:string password:string
and rails g controller insecure_user index
. Then we migrate with rails db:migrate
. Next we change get 'insecure_user/index'
to get 'insecure-login', to: 'insecure_user#index'
and add post 'insecure-login', to: 'insecure_user#create'
in our routes.rb
file. Next we go into our newly created insecure_user/index.html.erb
where we create our login form. I want to keep it simple so I just used to bootstrap form helper to not make it look awful:
<%= form_with url: "/insecure-login", method: :post, class: "w-25 m-3" do |form| %>
<div class="mb-3">
<%= form.label :email, "Your Email", class: "form-label" %>
<%= form.text_field :email, class: "form-control" %>
</div>
<div>
<%= form.label :password, "Password", class: "form-label" %>
<%= form.password_field :password, class: "form-control" %>
</div>
<%= form.submit "Sign in", class: "btn btn-primary mt-2" %>
<% end %>
<% if flash[:error] %>
<div class="alert alert-danger mt-3" role="alert">
<%= flash[:error] %>
</div>
<% end %>
And then we need to connect that with a post method in our backend. That function needs to take in the params, check if someone with that email exists and then check if the password matches the email. The messages we display in the flash messages:
def create
user = InsecureUser.find_by(email: params[:email])
if user
if user.password == params[:password]
flash[:error] = "Logged in successfully"
else
flash[:error] = "Password does not match the email"
end
else
flash[:error] = "Email does not exist"
end
redirect_to insecure_login_path
end
Lastly we create our seeds.rb
:
InsecureUser.create(email: 'insecure1@example.com', password: '123456')
InsecureUser.create(email: 'insecure2@example.com', password: '123456')
InsecureUser.create(email: 'insecure3@example.com', password: '123456')
InsecureUser.create(email: 'insecure4@example.com', password: '123456')
InsecureUser.create(email: 'insecure5@example.com', password: '123456')
And add that data with rails db:seed
.
5. Security Misconfiguration
The only thing I did to make this work is add
puts 'Our user:'
puts 'user.inspect'
after getting the user in the insecure_user#create
function.
6. Vulnerable components
This flaw does not have a demonstration
7. Identification and Authentication failures
This demonstration takes a bit longer to prepare, which is why it will have a separate article. Stay tuned :)
8. Software and data integrity failures
This flaw does not have a demonstration
9. Security logging and monitoring failures
This flaw does not have a demonstration
10. Server-side request forgery (SSRF)
This demonstration was very easy to create. First we type rails g controller ssrf index
into the console, which creates a controller and an index function. Then we just change the index function to following:
def index
eval params[:query]
end
This makes the server execute bad code.
Top comments (0)