Session csrf_token versus LiveView csrf_token

Does anyone know what is the point of:

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})

when mount/3 in the LiveView receives a session containing another "_csrf_token" ?
And why are those tokens not the same, I am confused.

inspect Plug.Conn.get_session(assigns.conn, "_csrf_token")
>>> "Xebn1wL063P33DzixqzBBT6I"

document.querySelector("meta[name='csrf-token']").getAttribute("content")
>>> "EyMMBGcPHAMEez8BBCsKEA4HNjsNZncfKFnjVxP32Ho27opyvvLyO2AV"

def mount(%{} = _params, %{"_csrf_token" => csrf_token}, socket) do
>>> "Xebn1wL063P33DzixqzBBT6I"

socket |> Phoenix.LiveView.get_connect_params |> Map.get("_csrf_token")
>>> "EyMMBGcPHAMEez8BBCsKEA4HNjsNZncfKFnjVxP32Ho27opyvvLyO2AV"

What’s going on here?

2 Likes

Related to:

Anything in the session cannot be accessed by javascript in the browser, because the session cookie as the Secure and HttpOnly flags set on them, and you can read more about it here:

Restrict access to cookies

There are a couple of ways to ensure that cookies are sent securely and are not accessed by unintended parties or scripts.

A cookie with the Secure attribute is sent to the server only with an encrypted request over the HTTPS protocol, never with unsecured HTTPS. Even with Secure , sensitive information should never be stored in cookies, as they are inherently insecure and this attribute can’t offer real protection. Insecure sites (with http: in the URL) can’t set cookies with the Secure attribute (since Chrome 52 and Firefox 52).

A cookie with the HttpOnly attribute is inaccessible to the JavaScript Document.cookie API; it is sent only to the server. For example, cookies that persist server-side sessions don’t need to be available to JavaScript, and should have the HttpOnly attribute. This precaution helps mitigate cross-site scripting (XSS) attacks.

Here is an example:

Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2021 07:28:00 GMT; Secure; HttpOnly

So this means that in order to protect the fist call from LiveView to the server we need to give it another CSRF token, otherwise you would be leaking the session CSRF token to the html document, thus compromising the security of the session cookie, because any javascript on your page would be able to read it, just as the LiveView javascript is able to do.

3 Likes

It makes sense that you don’t want any javascript to be able to access session (or any other) cookies.
The csrf token that is sent to the liveview is copied from a meta tag that is rendered in the html source code just like a token would be rendered in a hidden form field.
I guess that meta tag was generated from session data right?
I would think that I would be able to validate the csrf token I get by calling Phoenix.LiveView.get_connect_params/1 against the one I get from the session argument in mount/3.
That way I could be sure the socket connection is safe just like I would know a post request came from my server.
Is that not the purpose of the csrf token?

The purpose of CSRF tokens are to give you some confidence that the request comes indeed from your browser app, but once they are used from an html tag, they are indeed a weak assurance for that purpose, but are better then nothing.

Anyway the ones used by Phoenix can be improved by using their encrypted tokens feature, and then checking in the backend that the token is correctly signed and not older then x amount of seconds, lets say 10 seconds.

With this approach you have more confidence that the request is indeed from your browser app, but 10 seconds is still plenty of time for it to be stolen and reused from malicious javascript code in your web page.

Now you can enhance the CSRF token even more by treating it as a nonce, aka it can be only used once, but this requires some logic in your backend to keep an :ets table with all CSRF tokens not expired, that you will check for in each request to see if it was already used, and if so you deny the request, despite the encrypted CSRF token be correctly signed and have not expired.

Once more this not guarantees you that the malicious javascript in your page was not the first one to use the encrypted CSRF token to make a request to your backend.

If you do all this then you are in a much safer place then the current standard approach, but remember that in the end of the day you cannot find a bullet proof solution, just one that is hard to bypass.

3 Likes

Thank you for your effort to help clearing things up.

So lets say that the purpose of CSRF tokens is to give me some confidence that the request comes from my browser app.
That’s exactly how I understand this technique so in this respect we are on the same page.

But what kind of request are we talking about?

a) mounting the liveview
b) liveview -> render form -> post stuff -> got request

I am assuming that I should use this token: get_connect_params |> Map.get("_csrf_token") to validate mount/3 (option a), something along the lines of:

case token_from_connect_params do
  ^token_from_session -> :great_stuff
  _csrf_attack        -> :bad_news
end

Suppose the actual purpose is option b then I am misunderstanding all of this completely.
Probably someone can think of some obscure use case for option b.
To me that would be an edge case, so I would guess the purpose is option a.
so… how do I get rid of :bad_news ?

I did (finally) find a relation between the two csrf tokens:

Plug.CSRFProtection.dump_state()                       |  "0Nt0DUzw_86ttI3jIcNFPV61"
x: mount/3 -> session -> "_csrf_token"                 |  "0Nt0DUzw_86ttI3jIcNFPV61"
Plug.CSRFProtection.dump_state_from_session(a)         |  "0Nt0DUzw_86ttI3jIcNFPV61"
y: socket -> get_connect_params -> "_csrf_token"       |  "ZB4uAHB4KidmSmUrOQVbUgYSenMICUx0TPZ04-PP9rS_MLh8Oq45X_zE"
Plug.CSRFProtection.valid_state_and_csrf_token?(x, y)  |  true  <-- got match!

So I can validate mounting the liveview:

@impl true
def mount(%{} = request_params, %{} = session, socket) do
  case Map.get(session, "_csrf_token") do
    nil ->
      :ok = :secret_key_base
            |> socket.endpoint.config
            |> Plug.CSRFProtection.load_state(nil)

      token   = Plug.CSRFProtection.dump_state()
      session = Map.put(session, "_csrf_token", token)

      mount(request_params, session, socket)

    session_state ->
      connect_params = Phoenix.LiveView.get_connect_params(socket) || %{}
      csrf_token     = Map.get(connect_params, "_csrf_token")
      loading?       = is_nil(csrf_token)
      valid_token?   = Plug.CSRFProtection.valid_state_and_csrf_token?(
                         session_state, csrf_token
                       )

      state = case {loading?, valid_token?} do
                {true, false} -> :loading
                {false, true} -> :complete
                _csrf_attack  -> :error
              end

      {:ok, assign(socket, mount: state)}
  end
end

However, if this is correct, why is it not in the manual?
So probably it’s not correct.
And if it’s not correct, then what is the purpose of the csrf token?

1 Like

The option a) refers to the CSRF token you mentioned here:

and my reply was base on that scope.

Now my suggested improvements may also be added to CSRF tokens for forms, but obviously there you cannot use a very short expire time for it.


I am still new to Elixir and Phoenix and just 1 month ago I decided to build a real project with it, thus I am still in the exploratory phase of all its security mechanisms, thus I welcome your findings regarding the CSRF token and I will explore them too.


To bear in mind that Phoenix Phoenix will check the Phoenix tokens for your in the incoming requests, but I have not explored the internals of it to see how it works and see if we have room for hardening it.


Well web-sockets can be attacked as anything else exposed to the internet, and if your site as some value for attackers you can bet that they will try to hack it, no matter if over web-sockets or regular http requests.

You can read more about testing web-sockets for security in this OWASP guide, that includes some examples with the OWASP Zed Attack Proxy.

You will not regret your decision to start a real project using Elixir and Phoenix :wink:
I am not new to Elixir and a huge fan, but when it comes to LiveView I’m also in the exploratory phase.
Unlike many other Elixir projects Phoenix has a lot of magic goblins walking around.
When I see stuff “pop up” (like %{"_csrf_token" => _} = session) I want to know all about it.
Sometimes it’s not easy to follow the money.
I think Phoenix and LiveView are works of genius.
Some things are a little unfortunate in my opinion, like for example non-specific variable names (“params”) and aliases imported by macros.
Using Phoenix seems simple, but there’s lots of stuff you need to know to make sense of the docs.
Anyway, Phoenix is absolutely awesome, so it’s worth it.

3 Likes

And if you haven’t already, please setup ci/cd and create tests for every public function.
Maybe it’s a lot of overhead but you will be very happy you did once your project becomes more complex.

1 Like

I’m also curious about this. Not sure if I’ve completely understood, either.

My understanding that by default Phoenix does this:

  1. The get_csrf_token() call in root.html.heex causes the server to generate a CSRF token, which it stores in the process dictionary.
  2. As there is now a CSRF token in the process dictionary, Plug.CSRFProtection will also put the CSRF token in the session.
  3. Subsequent responses will use the CSRF token from the session (cookie).

The result is that the HTML response contains a session cookie with the CSRF token (the first time), and also in the response body (every time), so it can be read by JS.

For LiveView, I’m also unsure. It looks like most of the components handle generating CSRF tokens themselves. The docs say:

The mount callback receives three arguments: the request parameters, the session, and the socket.
…
The session retrieves information from a signed (or encrypted) cookie.

But the CSRF token inside the session the mount call receives is not the one from the session cookie, it’s one generated by the live view :face_with_raised_eyebrow:

I had some time to look into this more.

It turns out this was all a misunderstanding, caused by getting confused by how Plug.CSRFProtection handles CSRF tokens internally.

How Plug.CSRFProtection handles CSRF tokens internally

Plug.CSRFProtection has an internal concept of “masked” (long) and “unmasked” (short) CSRF token. The one you get when you call get_csrf_token/0 is the masked one, but the one it stores in the session (which you see if manually getting the session or looking at the mount call, or inspecting the cookie) is the unmasked one. However, these are the same and you can check that they match as ingmar already did above:

I guess the masking is something to do with preventing timing attacks.

LiveView Components add the CSRF token automatically

form will automatically add the CSRF token (which it gets from the process dictionary, which is populated from the session cookie, as described in the previous post) to the form if it contains an action parameter. A form generated by mix phx.gen.live doesn’t contain action by default, as it’s assumed the form will be submitted via LiveView, but if you add it, the CSRF token will be added. For example

<.form for={@form} action={~p"/example/path"}>...</.form>

This form will include the masked/long CSRF token in a _csrf_token field by default, due to the presence of the action param.

Additionally, while looking into this, I also found this post which confused me, because what it described is all unnecessary - maybe it was an issue on older versions:

Now the issue described in that article also works out of the box with a link like this:

<.link method="delete", href={~p"/example/path"}>Delete</.link>

This generates a link with the relevant CSRF token included automatically.


To go full circle, the only thing I’m not sure about, is what the purpose of this is, considering components make their own calls to Plug.CSRFProtection to get the csrf token, so there doesn’t seem to be a need to pass it back from the browser:

1 Like

The masking is needed to avoid the BREACH attack, a variant of the CRIME compression attack. CRIME was an attack on compressed headers and was fixed in HTTP2 with the HPACK algorithm which compresses headers in such a way that they are not vulnerable. Unfortunately the CSRF token is present in the body and does not change between requests, meaning it can slowly be revealed if an attacker can tweak the page contents and observe the compression ratio.

Masking literally just generates another token’s worth of bytes as a mask, xors that with the token, and returns the concatenation of the two. This essentially hides (masks) the original token from the compression algorithm, and the mask changes on every request so it cannot be iteratively revealed.

Personally I find the complexity (and therefore bug surface) of CSRF protection quite distressing given that experts believe the token approach is no longer necessary. But of course this is brand new guidance and it will take time for everyone to adapt.

Reading the article I’m also not sure if this is any less complex tbh.

That article is a pretty thorough technical deep dive into whether this new approach is secure (and the conclusion is that it is).

In practice the new approach is just comparing a couple of headers sent by the browser, versus creating a magic token (which you have to do very carefully, see above), writing it into the session, and propagating it into every single form and checking it on submit.

The CSRF token approach is a gross hack that has to touch an unreasonable amount of the stack just to check that the request came from the correct origin, which is something that the browser just, like, tells you for free now.

Not to mention the only reason this topic even came up in the Go community is that they found a serious security bug in their CSRF library that had been there for years lol.

1 Like

Obviously this is security-sensitive code and I am just typing into a forum box so DO NOT USE THIS FOR ANYTHING, but here is a port of the Go implementation.

def check(conn) when conn.method in ["GET", "HEAD", "OPTIONS"] do
  :ok
end

def check(conn) do
  case get_req_header(conn, "sec-fetch-site") do
    [value] when value in ["same-origin", "none"] -> :ok
    [_other] -> {:error, :cross_origin}
    [] -> check_origin(conn)
  end
end

# Fallback for 2020-2023 browsers with Origin but without Sec-Fetch-Site
defp check_origin(conn) do
  case get_req_header(conn, "origin") do
    [origin] ->
      %URI{host: origin_host} = URI.new!(origin)
      case origin_host == conn.host do
        true -> :ok
        false -> {:error, :cross_origin_older_browser}
      end
    [] ->
      # No Sec-Fetch-Site or Origin headers were present
      # This is not a browser (or is a pre-2020 browser)
      :ok
  end
end

Note that the entire Origin fallback section isn’t even necessary if you’re willing to ditch support for pre-2023 browsers (which I personally would tbh), at which point this is like 5 lines of actual code.

Contrast that with using cryptography to generate, sign, and mask a token, write it to a session cookie, and propagate the value into every single form in your application, which is hundreds of lines at least and leaks all the way into the high-level abstractions (forms) of the framework.

I think the path forward is pretty clear. If you’re writing a new framework there is no reason to even think about the token approach. Existing frameworks will adapt over time.

1 Like

@garrison that is super interesting. Thanks for going into the details. I had read Filippo’s post before and mentally bookmarked it to come back to. My objective in this thread is to understand Phoenix’s current default behaviour, although the explanation of CRIME has raised another question:

  1. The fix seems to depend on the way the headers are compressed (or not). As Plug stores the unmasked token in the cookie, does this mean the attack is still possible in some cases? Maybe this is mitigated by the way the session is serialised (external term format, cryptographically signed, base64)?

  2. The original question @i-n-g-m-a-r asked 5 years ago that I’m re-asking: what is the purpose of sending the (masked) CSRF token back to LV via the socket via JS, considering web components can and do fetch it themselves directly on the server anyway?

1 Like

Answering my own question in the hope of activating Cunningham’s Law.

The reason that the (masked) CSRF token is passed to the LiveSocket via JS is because this is a new HTTP request, so CSRF is possible (somebody could be serving up a page that loads JS that tries to connect to our web socket using our cookie). As the socket connections are handled in the Endpoint, before the :protect_from_forgery plug in the Router, the socket performs its own validation of the CSRF token:

In this code, csrf_token is the masked CSRF token passed to the LiveSocket connection request, and csrf_state is the unmasked one from the cookie.

1 Like

Headers are not vulnerable to CRIME in HTTP2 because they are compressed with an algorithm called HPACK which is part of the standard.

Originally HTTP headers were not compressed at all (in 1.1), but then people started trying to compress them naively (in SPDY) to save on ingress costs. As shown in that CF blog post it actually saves like half of ingress costs because headers are most of a request (not so much for a response).

Unfortunately it was shown that naively compressing headers with e.g. DEFLATE can leak them to an attacker (in an unlikely but plausible scenario), so everyone turned off compression. But compression was nice (made things cheaper), so they designed a special compression algorithm that is not vulnerable, and that’s HPACK.

On a modern stack you can put whatever you want in the headers and it won’t be leaked because they are compressed with HPACK. The reason you have to mask the CSRF token is that it is sent in the body (with the forms).

As a side note, I believe this also means that you should never put any sort of confidential secret into a compressed body alongside variable content or it could theoretically leak. Try not to think about the implications there.

Again the reason the attack is not possible is because HTTP headers are not compressed this way, but if they were then the entire session would be vulnerable to exfiltration.

I have not read the code but this makes perfect sense and I think it’s correct. Note that the token has to be masked because it is again in the body and could be compressed naively.

I know it’s not what you’re asking about but I can’t help but point out that it would be a whole lot easier to just check Sec-Fetch-Site and be done with it rather than pass a token through like four levels of abstraction. Of course this was not possible when the code was written!

1 Like

A summary for anyone else landing here.

  1. Phoenix uses Plug.CSRFProtection to generate the CSRF token. A token has two forms: a short/unmasked form, which is what is stored in the session, and a long/masked form, which is what is returned by Plug.CSRFProtection.get_csrf_token/0 and injected into the HTML responses. The long form in the HTML is necessary to guard against BREACH attacks.
  2. This is not mentioned in the thread, but it’s also possible for a single HTML page to have multiple masked CSRF tokens that correspond to the same unmasked one in the session. This is because the masked tokens are generated (from the token in the session) per process on the server. The process that returns the initial HTML response from the controller is different to the one that updates the DOM from LiveView.
  3. The reason the masked/long CSRF token is passed to the web socket is to prevent a CSRF request occurring when the websocket is created. Normally the masked CSRF token is validated against the one in the session by the :protect_from_forgery plug, but websocket requests are set up in the Endpoint and don’t use :protect_from_forgery, so the web socket code has its own code to validate the token (which ultimately uses Plug.CSRFProtection under the hood, the same as :protect_from_forgery). Additionally, the purpose of this is not because LiveView needs to otherwise know the long/masked token - it’s just to protect against CSRF when establishing the socket.
  4. As explained in this article by Filippo Valsorda, in 2025 CSRF tokens may not be the best way to protect against CSRF, instead the Sec-Fetch-Site header can be used - but this method is relatively new and is not currently used by Phoenix out of the box.
4 Likes