Web-app security risks demonstrated
Tags: webdev, security, tutorial, beginners
Published: 2022-11-01 12:49:26

## 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](https://github.com/aneshodza/security-risks-demo). 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:
```ruby
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](https://github.com/CanCanCommunity/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](https://dev.to/aneshodza/how-not-to-store-passwords-20ib) 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:
```sql
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.
![A screenshot of the sql injection](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4a4nj78afrmon2pjd7pc.png)
To fix this we sanitise the queries. Rails does that for us, if we query like this:
```ruby
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.
![Form showing error: Email does not match password](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7ohd17migcz9yk5f0x6o.png)
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s4dp9vex9k720cqtpwny.png)
This (existing) user has its details printed onto the console:
![The user data printed onto the console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/js6qy217unm8hjrd04pn.png)
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](https://github.com/aneshodza/rails-bruter.py). 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](https://www.youtube.com/watch?v=jwzeJU_62IQ&ab_channel=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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t1i8y127ekr6wcl37v5f.png)
## 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](https://dev.to/aneshodza/how-not-to-store-passwords-44cd). 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:
```ruby
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:
```erb
<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`:
```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:
```erb
<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`:
```html
<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:
```ruby
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](https://dev.to/aneshodza/how-not-to-store-passwords-20ib).
### 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:
```ruby
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:
```ruby
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:
```erb
<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:
```erb
<!-- ... -->
<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:
```erb
<%= 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:
```ruby
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`:
```ruby
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
```ruby
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:
```ruby
def index
  eval params[:query]
end
```
This makes the server execute bad code.