Elixir/Phoenix Security: Introduction to Cross Site Request Forgery (CSRF)

Michael Lubas, 2022-12-05

Cross site request forgery (CSRF) is a type of vulnerability in web applications, where an attacker is able to forge commands from a victim user. For example, consider a social media website that is vulnerable to CSRF. An attacker creates a malicious website aimed at legitimate users. When a victim visits the malicious site, it triggers a POST request in the victim’s browser, sending a message that was written by the attacker. This results in the victim’s account making a post written by the attacker.

How does this work in practice? This article will use a basic Phoenix application, vulnerable to CSRF, to show what the attack looks like. Phoenix provides you with excellent defaults to prevent CSRF, which have been removed for this exercise. After the vulnerability has been covered, we will examine how Phoenix prevents CSRF by default.

A Basic CSRF Vulnerability

Consider a basic application, where users login and submit posts:

csrf0

Kat decides to create a new post:

csrf1

Below is the form Kat used to create a post. Notice that Phoenix’s default CSRF protection has been removed.

<form action="/posts" method="post">

  <label for="post_title">Title</label>
  <input id="post_title" name="post[title]" type="text">

  <label for="post_body">Body</label>
  <textarea id="post_body" name="post[body]">
  </textarea>

  <div>
    <button type="submit">Save</button>
  </div>
</form>

csrf2

This action, the creation of a new post, is vulnerable to CSRF. This is because web browsers allow a site, such as attackerexample.com, to initiate a POST request to a different site, such as example.com. That POST request is made with the current session on example.com, meaning the user’s cookies are sent with the POST, allowing the attacker to preform a privileged action on behalf of the user.

For this example, the attacker wants to post the message “Hacked by Dogs” from the account of kat@paraxial.io. The attacker creates a fake website, ostensibly for photos of cats, but in reality it will trigger a malicious POST request on behalf of kat.

csrf3

Notice that our normal site in on localhost port 4000, while the malicious site is on port 4001. In a real attack, these two sites would be on different domains. The malicious page contains the form:


<form action="http://localhost:4000/posts" method="post" name="csrf_attack" style="">

  <input id="post_title" name="post[title]" value="Hacked">

  <input id="post_body" name="post[body]" value="Hacked by Dogs">

  <div>
    <button type="submit">Save</button>
  </div>
</form>

Clicking the “Save” button will result in the malicious site trigger a POST request in the victim’s browser, to http://localhost:4000/posts, to create a new post, “Hacked by Dogs”.

csrf4

In a real attack, an attacker would edit the HTML with <body onload="document.forms['csrf_attack'].submit()">, to simply submit the form as soon as the user visits the page. We use the example with a submit button to better illustrate what is happening.

CSRF protection in Phoenix is handled by the protect_from_forgery plug, typically found in your router’s :browser pipeline. In our example application, it has been commented out:

defmodule BellsWeb.Router do
  use BellsWeb, :router

  import BellsWeb.UserAuth

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, {BellsWeb.LayoutView, :root}
    #plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug :fetch_current_user
  end

Phoenix automatically generates a CSRF token when you use the proper helper functions. This token cannot be read by other sites, but is required for the POST request to succeed. The malicious site can still initiate the same POST request from the victim’s browser, but it will fail, because it lacks the secret value. Here’s the form with the CSRF token:


<form action="/posts" method="post">
  
  <input name="_csrf_token" type="hidden" value="FXFWJhU3CAIxEAY-dWdGdTkCNmcBCi4ZMC2aJRetkjvRGRtBSmu3KiMa"> 
  
  <label for="post_title">Title</label>
  <input id="post_title" name="post[title]" type="text">
  
  <label for="post_body">Body</label>
  <textarea id="post_body" name="post[body]">
  </textarea>

  <div>
    <button type="submit">Save</button>
  </div>

</form>

This is generated by Phoenix automatically.

lib/bells_web/templates/post/new.html.heex

<h1>New Post</h1>

<%= render "form.html", Map.put(assigns, :action, Routes.post_path(@conn, :create)) %>
<span><%= link "Back", to: Routes.post_path(@conn, :index) %></span>

lib/bells_web/templates/post/form.html.heex

<.form let={f} for={@changeset} action={@action}>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

  <%= label f, :title %>
  <%= text_input f, :title %>
  <%= error_tag f, :title %>

  <%= label f, :body %>
  <%= textarea f, :body %>
  <%= error_tag f, :body %>

  <div>
    <%= submit "Save" %>
  </div>
</.form>

Now that the CSRF token is added, does this mean the vulnerability is fixed? The answer is no, because even though the token is being submitted, the backend is not verifying the token is correct. This is a critical step, many applications were thought secure due to the presence of the CSRF token, until an attacker realized it was not being checked on the backend. Enabling this in Phoenix is done through the :protect_from_forgery plug:

defmodule BellsWeb.Router do
  use BellsWeb, :router

  import BellsWeb.UserAuth

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, {BellsWeb.LayoutView, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug :fetch_current_user
  end

Now what happens when the victim visits the attacker’s page?

csrf5

Phoenix raises an error, because the CSRF token is not valid, meaning the site is not vulnerable to CSRF. Even if the attacker were to place their own CSRF token in the malicious form, the attack would still fail, because the token verification is tied to the user’s current session cookie. Phoenix strongly encourages developers to write secure code by default, because every new project’s browser pipeline has the protect_from_forgery plug, and the form generators provide a CSRF token.

Paraxial.io stops data breaches by helping developers ship secure applications. Get a demo or start for free.

Subscribe to stay up to date on new posts.