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.
Post by Mateusz Bielawski
Mateusz, who joined AmberBit in 2017, writes Elixir for our clients ever since