This is a blog of AmberBit - a Elixir and Ruby web development company. Hire us for your project!

Elixir, Phoenix, CSRF tokens and gloomy foreteller

Mbielawski

Posted by Mateusz Bielawski

Mateusz, who joined AmberBit in 2017, writes Elixir for our clients ever since
@mat-bi

Gloomy foreteller

Let’s pretend you want to write a really simple foreteller in Phoenix/Elixir. You don’t want to use any helpers to write a form. It could look something like this:

<form method="POST">
    Tell me your name:
    <input type="text" name="name"/>
    <input type="submit" value="Send" />
</form>

and actions:

def index(conn, _) do
    render conn, "index.html"
end

def foretell(conn, %{"name" => name}) do
    prophecies = [
      "Nothing will be good, #{name}",
      "You don't need to think about the future, #{name}. You don't have any"
    ]
    
    text conn, Enum.random(prophecies)
end

(of course some changes in the router are needed but aren’t covered in this article).

Let’s try it out, shall we?

Ok, so it’s time to check it. Let’s execute mix phx.server and go to the index page. Then we could type the name down and send the form.

Oh no!

It just doesn’t work! Phoenix says something like:

invalid CSRF (Cross Site Request Forgery) token, make sure all requests include a valid '_csrf_token' param or 'x-csrf-token' header`.

Ugh. What’s this?!

What happened?

Every POST request in browser pipeline is checked whether a csrf token exists. If not - raises an error. When form_for is used, a hidden input _csrf_token is created and filled with a random token.

Ways to make it right

Of course there are many ways to make it “right”. It could be disenabling the token (not recommended) or adding it to the form using get_csrf_token/0 from controller like this:

def index(conn, _) do
    render conn, "index.html", token: get_csrf_token()
end
<form method="POST">
    <input type="hidden" value="<%= @token %>" name="_csrf_token"/>
    (...)

Phoenix form_for also adds it automatically and it’s the easiest way. Just do this:

<%= form_for @conn, page_path(@conn, :foretell), fn f -> %>
  Name: <%= text_input f, :name %>
  <%= submit "Tell me my future!" %>
<% end %>

Then you don’t even have to pass any argument to the template. It just works. Ok. But why is it there in the first place?

CSRF Attack (Cross-Site Request Forgery)

CSRF is an attack that tricks a user into sending an unwanted request to the server. If the user is logged in into a website, the site cannot distinguish between a “real” request from the user and a malicious one. What’s really bad is the fact that no “external data” is logged - there is only the user’s request, their ip and so on. It’s really untraceable - only with some “side data” like user’s email, history etc. can be deduced what really happened.

Example

Let’s think about a simple bank site. It allows users to transfer money with a POST request to the link: https://mybank.com/transfer. Data is sent within the request body. So when Alice wants to transfer 100$ to Bob, she fills in a form that creates a request to: https://mybank.com/transfer with data: to=0012341&amount=100. But let’s pretend Eve wants to make Alice send money to the given account. She knows that she should ‘persuade’ Alice to make the request. So she creates a hidden form with action: https://mybank.com/transfer and hidden fields to and amount:

<form action="https://mybank.com/transfer" method="POST" id="my_form">
<input type="hidden" value="0012341" name="to" />
<input type="hidden" value="100" name="amount" />
</form>

and just sends it with one line of JS: document.getElementById("my_form").submit();. Now only one point is left - persuading the victim to visit the website. It could be in a form of an email “tailored” to the person or a huge attack with many victims.

Countermeasures

There are ways how to make your webpage safe.

1. POST/GET Requests

According to RFC 7231, GET requests shouldn’t have any “side-effects”. That means that every request should end with the same result and shouldn’t change anything. POST requests could have some effects like creating a new blog post. However, it is not enough.

2. One-time keys / Temporary tokens

The most rudimentary technique to secure the app is by using random tokens. Just get some random bytes, save them in session storage, then add them to the form. When the sent form doesn’t have the key or it doesn’t match up - don’t fullfill the request, raise an error. That’s exactly the case what happened with our foreteller. Phoenix didn’t get the token so it refused to complete the request.

3. Using HTTP headers

If your web app needs a higher level of security, you can also use checking headers. It’s another option, requires checking the referer/origin header in the request and failing if something doesn’t match up.

It’s really basic stuff, if we wanted to create a really safe app, we would go with one-time keys (like our bank website - a token sent by sms) or two-factor authentication. However, temporary tokens are simple and effective enough for most webpages. And as long as we don’t need to make anything really specific and without help of our frameworks, it doesn’t need our attention.

Conclusion

Frameworks make our lives simpler, allowing us to focus on really important stuff rather than thinking about every possible part of running a webpage. However, it doesn’t mean that we shouldn’t be aware of possible attacks that are ubiquitous.

Hubert

Hi there!

I hope you enjoyed the blog post. Can we help you with Elixir or Ruby work? We are looking for new opportunities at the very moment, and we do have team available just for you.

Email me at: contact@amberbit.com or use the contact form below.

Want to get in touch about a project? Drop us a line!

When submitting the form, you are sending your personal information (including your name and e-mail as entered above) to contact@amberbit.com. AmberBit Sp. z o. o. is the receiving party, and a data controller, and will use the information you provided for the purpose of establishing relationship leading to possibly signing a services contract, and fulfillment of such contract only. We will not subscribe you to marketing lists, newsletters etc. You can read more about it in our Privacy Policy.