Web-app security risks demonstrated

aneshodza

anes

Posted on November 1, 2022

Web-app security risks demonstrated

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


Enter fullscreen mode Exit fullscreen mode

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')


Enter fullscreen mode Exit fullscreen mode

This selects every car, independent of its public status, because in the end it compares if '1' = '1', which is true for every car.
A screenshot of the sql injection
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


Enter fullscreen mode Exit fullscreen mode

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.
Form showing error: Email does not match password
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.
Log in form being filled out
This (existing) user has its details printed onto the console:
The user data 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.
Console output of a user being 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


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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)



Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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 %>


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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 %>


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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')


Enter fullscreen mode Exit fullscreen mode

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'


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

This makes the server execute bad code.

💖 💪 🙅 🚩
aneshodza
anes

Posted on November 1, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related