21
votes

I am receiving an Invalid CSRF token error when trying to update (or create) a record. I am using Elixir v1.0.3, Erlang/OTP 17 [erts-6.3], and Phoenix v0.8.0 (I think, I am not sure how to check Phoenix's version). I am creating a web app mostly following the Phoenix guides and the Elixir Dose Jobsite Example resources. However, when I try to post information from an html form, I get the Invalid CSRF token error. Following the advice given in the error, I added 'x-csrf-token': csrf_token to the action.

edit.html.eex:

<h2>Edit Directory</h2>
<form class="form-horizontal" action="<%= directory_path @conn, :update, @directory.id, 'x-csrf-token': @csrf_token %>" method="post">
  <div class="form-group">
    <label for="directory" class="col-sm-2 control-label">Directory</label>
    <div class="col-sm-10">
      <input type="hidden" name="_method" value="PATCH">
      <input type="text" class="form-control" value="<%= @directory.directory %>" name="directory" placeholder="Directory" required="required">
    </div>
  </div>
...

but I receive the following error:

[error] #PID<0.579.0> running Ainur.Endpoint terminated
Server: localhost:4000 (http)
Request: POST /config/directories/2?x-csrf-token=
** (exit) an exception was raised:
    ** (Plug.CSRFProtection.InvalidCSRFTokenError) Invalid CSRF (Cross Site Forgery Protection) token. Make sure that all your non-HEAD and non-GET requests include the csrf_token as part of form params or as a value in your request's headers with the key 'x-csrf-token'
        (plug) lib/plug/csrf_protection.ex:54: Plug.CSRFProtection.call/2
        (ainur) web/router.ex:4: Ainur.Router.browser/2
        (ainur) lib/phoenix/router.ex:2: Ainur.Router.call/2
        (plug) lib/plug/debugger.ex:104: Plug.Debugger.wrap/3
        (phoenix) lib/phoenix/endpoint/error_handler.ex:43: Phoenix.Endpoint.ErrorHandler.wrap/3
        (ainur) lib/ainur/endpoint.ex:1: Ainur.Endpoint.phoenix_endpoint_pipeline/2
        (plug) lib/plug/debugger.ex:104: Plug.Debugger.wrap/3
        (phoenix) lib/phoenix/endpoint/error_handler.ex:43: Phoenix.Endpoint.ErrorHandler.wrap/3

As far as I can tell (being new to Elixir, Phoenix, and HTML), "action" is essentially a path and any parameters I place in it will find their way back to the application. And, indeed, I find that x-csrf-token = "" is passed back to the router, so @csrf_token must not be correct. I am not sure exactly where the csrf_token comes from, so I do not know how to reference it (or perhaps I am doing this completely wrong).

Any ideas would be greatly appreciated.

6

6 Answers

21
votes

On version 0.13 of Phoenix you can do

<input type="hidden" name="_csrf_token" value="<%= get_csrf_token() %>">

because on file web/web.ex there's an import of this function.

9
votes

As another solution available since v0.10.0, you can let Phoenix inject the CSRF input for you.

Example from upgrade guide:

<%= form_tag("/hello", method: :post) %>
... your form stuff. input with csrf value is created for you.
</form>

That will output the form tag and a few input tags, including the _csrf_token one. Results will look something like this:

<form accept-charset="UTF-8" action="/hello" method="post">
    <input name="_csrf_token" value="[automatically-inserted token]" type="hidden">
    <input name="_utf8" value="✓" type="hidden">
</form>

form_tag docs: "for 'post' requests, the form tag will automatically include an input tag with name _csrf_token."

4
votes

To see the version installed, run

cat ./deps/phoenix/mix.exs | grep version

Which shows you which phoenix you have in the deps directory.

Also, if/when you upgrade to phoenix 0.9.0, things have changed (due to updates to Plug.CSRFProtection), the CSRF works differently using cookies instead of sessions.

From Phoenix changelog for v0.9.0 (2015-02-12)

[Plug] Plug.CSRFProtection now uses a cookie instead of session and expects a "_csrf_token" parameter instead of "csrf_token"

To access the token's value, grab if from the cookie, which on the server side looks like

Map.get(@conn.req_cookies, "_csrf_token")

So for your code, would look something like

<h2>Edit Directory</h2>
<form class="form-horizontal" action="<%= directory_path @conn, :update, @directory.id, 'x-csrf-token': Map.get(@conn.req_cookies, "_csrf_token") %>" method="post">
  <div class="form-group">
    <label for="directory" class="col-sm-2 control-label">Directory</label>
    <div class="col-sm-10">
      <input type="hidden" name="_method" value="PATCH">
      <input type="text" class="form-control" value="<%= @directory.directory %>" name="directory" placeholder="Directory" required="required">
    </div>
  </div>

Now, for completeness, I needed the updated CSRF for requests built purely client side, so here is how I accessed the cookie within javascript, using JQuery cookies, for easy access. You should be able to see the value in your browser by running the following

$.cookie("_csrf_token")

Which might return something like

"K9UDa23e1sacdadfmvu zzOD9VBHTSr1c/lcvWY="

Note in the above, the space, which in phoenix was being url encoded to +, which was still causing the CSRF to fail. Now is that a bug in Plug, or simply something to be handled, I am not sure, so for now I am simply handling the + explicitly

$.cookie("_csrf_token").replace(/\s/g, '+');

With access to the CSRF token, we now just need to add the x-csrf-token to your request header (thank you ilake). Here is the code to make it work with an ajax call work (fill in the url and data and response accordingly).

$.ajax({ 
  url: 'YOUR URL HERE',
  type: 'POST',
  beforeSend: function(xhr) {
    xhr.setRequestHeader('x-csrf-token', $.cookie("_csrf_token").replace(/\s/g, '+'))
  },
  data: 'someData=' + someData,
  success: function(response) {
    $('#someDiv').html(response);
  }
});

Note that you could send back the _csrf_token as parameter as well, but I prefer the above and it feels cleaner to me.

Final note, I didn't have enough reputation points to properly post a link to jquery cookie, but it should be easy to google.

2
votes

I found the answer on http://phoenix.thefirehoseproject.com. You must create a function to get the csrf token:

web/view.ex

def csrf_token(conn) do
  Plug.Conn.get_session(conn, :csrf_token)
end

Then retrieve it in the template:

web/template/directory/edit.html.eex

<form class="form-horizontal" action="<%= directory_path @conn, :update, @directory.id %>" method="post">
   <input type="hidden" name="csrf_token" value="<%= csrf_token(@conn) %>">

And that's it!

1
votes

My solution is:

  • Import Phoenix.Controller.get_csrf_token:0 to MyModule.view:0 (under apps/my_app_web/lib/my_app_web.ex):

    import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]

  • Add a hidden param in my form:

    <input type="hidden" name="_csrf_token" value="<%= get_csrf_token() %>" />

0
votes

In my case it was the line plug :scrub_params causing the problem. After commenting the line, it worked. But need to make sure to fix it as the app would be insecure without the scrub_params.