Let’s see how Phoenix implements CSRF for standard HTML multipart forms and how to use CSRF tokens outside these forms in React forms or API calls.
Cross-Site Request Forgery (CSRF) is exactly what it sounds like – an ability to do make a request on behalf of a user from a different site. This is possible when the authorization of the request derives from the authentication that happened automatically by the browser. There are two common scenarios for this; session cookies and HTTP Basic scheme authentication. In both cases the browser sends the authentication details (cookie or login/password combination) with every request to the particular domain name automatically.
To make it clear, CSRF is about the malicious request itself, not about the ability of the client to get the response. That’s why we don’t talk about preventing GET request with CSRF protection (and potentially leaking sensitive content). GET requests should not be changing state of the server (e.g. requesting a transfer of money, or placing an order).
The best solution we have today to combat this vulnerability are CSRF tokens. There is also an option to use Same-Site Cookie Attribute in some cases (which will be great for cookies in future). In this post we discuss CSRF tokens that should be sent with every non GET request and validated by the server. If the validation fails or the token is not provided, server should respond with 403 Forbidden.
To see how this protection works out-of-the-box in Phoenix let’s generate a new Todo application:
$ mix phx.new csrf
$ cd csrf
$ mix phx.gen.schema Todo todos description
$ mix ecto.create
$ mix ecto.migrate
$ mix phx.gen.html Web Todo todos description
And open the form that allows to submit a new task:
<%= form_for @changeset, @action, fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<%= label f, :description %>
<%= text_input f, :description %>
<%= error_tag f, :description %>
<div>
<%= submit "Save" %>
</div>
<% end %>
Looks like a regular form definition and nothing about CSRF in there. However, let’s see the HTML output:
<form accept-charset="UTF-8" action="/todos" method="post">
<input name="_csrf_token" type="hidden" value="Cm4mDW46IzBPJRZWGwpVOxskYgAJEAAAH7tiAvuVyqW4spcKto5OfQ==">
<input name="_utf8" type="hidden" value="✓">
<label for="todo_description">Description</label>
<input id="todo_description" name="todo[description]" type="text">
<div>
<button type="submit">Save</button>
</div>
</form>
Phoenix automatically injected the CSRF token for us inside the form and it will be sent together with other data on form submission. Sending the token means nothing of course if we don’t validate it. If we open the router definition we can see there :protect_from_forgery
plug which Phoenix rightfully suggests as part of the :browser
pipeline:
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
:protect_from_forgery
is actually just a wrapper for Plug.CSRFProtection and as the description say The token may be sent by the request either via the params with key “_csrf_token” or a header with name “x-csrf-token“.
Important option of this plug is :with
which can be either :exception
or :clear_session
to either raise and stop the request or just clear the session for a given request. If we are using :protect_from_forgery
plug we can pass it the same options.
By default, it raises exception which is the correct thing for handling HTML form submission like above. If we would like to be doing AJAX request then we would prefer to just cancel the session temporarily instead.
So let us do exactly that with a React form instead of the classic HTML form generated by Phoenix. First, we need to be able to grab the token from somewhere. Since we are still generating the page with Phoenix we can use csrf_meta_tag()
function to insert a meta attribute containing the CSRF token:
<!DOCTYPE html>
<html lang="en">
<head>
<%= csrf_meta_tag() %>
This is how it will look like once the page is generated:
<head>
<meta charset="UTF-8" content="ERBqDnUmHgtgHhQWR0gOOCIMeAVXEAAASI8jZjHmVJUt/28HMG/J8Q==" csrf-param="_csrf_token" method-param="_method" name="csrf-token">
Next, let’s add React dependency and write the form:
$ npm install --save react react-dom @babel/core @babel/preset-react
// assets/js/app.js
...
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class ReactForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
event.preventDefault();
var value = this.state.value
var csrfToken = document.head.querySelector("[name~=csrf-token][content]").content;
// AJAX request
var xhr = new XMLHttpRequest();
xhr.open('POST', '/todos');
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.setRequestHeader('X-CSRF-Token', csrfToken);
xhr.onload = function() {
if (xhr.status === 200) {
alert('New todo created, refresh the page to see it');
}
else if (xhr.status !== 200) {
alert('Request failed. Returned status is ' + xhr.status);
}
};
xhr.send(encodeURI('todo[description]=' + value));
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Description:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
ReactDOM.render(<ReactForm />, document.getElementById('reactForm'))
Notice how are we grabbing the token and adding it to the AJAX request:
var csrfToken = document.head.querySelector("[name~=csrf-token][content]").content;
...
xhr.setRequestHeader('X-CSRF-Token', csrfToken);
We could also change the plug now to clear session:
# in router
plug :protect_from_forgery, with: :clear_session
This would enable us to respond with some custom JSON that we could use in the React form for example, but I will skip it now.
At last, what if we don’t have the luxury of generating the HTML page with Phoenix? Let’s pretend our React form is actually part of a separate Single Page Application (SPA). For this reason, we have to send the CSRF token as a cookie.
defmodule CsrfWeb.TodoController do
use CsrfWeb, :controller
alias Csrf.Web
alias Csrf.Web.Todo
def csrf_token(conn, _params) do
# There is also get_csrf_token_for(url)
csrf_token = get_csrf_token()
conn
|> put_resp_cookie("csrf-token", csrf_token, sign: false, same_site: "secure")
|> text("")
end
We can generate the CSRF token inside any controller if we use the CsrfWeb
module and then simply call get_csrf_token()
or get_csrf_token_for(url)
functions. The above put_resp_cookie/4
function will add a new HTTP response header set-cookie
as follows:
set-cookie: csrf-token=NgMDNCwxSHxkdQ9xNAADH2MsWXNWJgAAxmPyDwx37GHDVFU0LV/AfQ==; path=/; HttpOnly
Ideally we send this together with our front-end application which can then grab the token from the browser cookie store.
That’s it! We have protected classic forms, AJAX forms, and forms in SPAs! Please note that the JavaScript code is far from perfect :).