i-n-g-m-a-r

i-n-g-m-a-r

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?

Most Liked

adamu

adamu

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.
Exadra37

Exadra37

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.

Exadra37

Exadra37

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.

i-n-g-m-a-r

i-n-g-m-a-r

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.

i-n-g-m-a-r

i-n-g-m-a-r

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?

Where Next?

Popular in Questions Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
dokuzbir
Hello, I am trying to convert my lists to string without losing brackets.For start i have 3 map. They look like these buyer = %{ id: ...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
mathew4509
I have a list say x = ["23gh", "56kh", "97mh"] I would like to pass each element to Val in each iteration. Say, in iteration 1 -------...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

Other popular topics Top

AstonJ
You’re a programmer, so you don’t need spoon feeding with the conventional drivel about “this is an integer.” No. You need to know what’s...
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 27727 240
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 42633 214
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
beno
I will often find my self writing things similar to: case some_value do nil -&gt; something() "" -&gt; something() _ -&gt; someth...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
AstonJ
by Lance Halvorsen Elixir and Phoenix are generating tremendous excitement as an unbeatable platform for building modern web application...
460 27162 124
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New

We're in Beta

About us Mission Statement