As an Elixir developer, you are probably already familiar with Phoenix Liveview, the new library to create fast and reactive web applications without requiring JavaScript. However, despite its growing popularity, there’s an important aspect of Liveview that needs to be addressed: its security model. Lately there have been some concerns about the potential vulnerabilities that Liveview might pose, and while there have been some discussions on the matter, no clear solutions have been proposed yet.

Liveview’s security model relies heavily on the developer’s ability to properly validate user input and implement access control measures. This can be a daunting task, as even small oversights can lead to a security leak. Bram Verburg recently blogged about this, providing an overview of some LiveView security concerns.

A specific challenge with Liveview’s security model is its handle_event/3 clause. This clause is a critical component of Liveview, as it is responsible for handling the events triggered by user interaction. However, since the parameters passed to this clause are coming from the web browser, they are in the user’s control and must be checked before being processed. Failing to do this could allow an attacker to manipulate the data and compromise the system’s security.

Decorators to the rescue

One way address this issue, is to use function decorators. Function decorators are annotations to a function that change the behaviour of the function in question. In the case of Elixir code, they actually allow you to modify the code before its compilation, so that the actual decorator has no runtime overhead at all.

In the case of a LiveView, we can write decorator that hooks into handle_event/3, to perform an ACL check.

Each clause of handle_event/3 should be decorated individually, as each event possibly has a different access check to be performed. The decorator takes as arguments the object (e.g., a “note”, and the action that is to be performed (e.g., “delete”). This metadata would then be used to verify whether the user is authorized to perform the action. If authorization fails, we ignore the event, maybe showing a put_flash message to indicate that the access was denied.

The implementing code in a Liveview module could look something like this:

  use ExampleApp.Web.LiveAcl

  # more code...

  @decorate event_acl_check(note: :delete)
  def handle_event("delete", %{"note-id" => id}, socket) do
    # here we now know that we have the proper access to the note with the given ID
  end

This decorator would take a single argument, which, in this case, is a keyword list that specifies which object to check with which action (we access a :note and check for the :delete action in the above example.

This LiveAcl module then of course needs to implement this decorator. The decorator library has more information on writing decorators, but an implementation example could be done like this:

defmodule ExampleApp.Web.LiveAcl do
  use Decorator.Define, event_acl_check: 1

  def event_acl_check(arg, body, context) do
    %{args: [_event, params, socket]} = context

    quote do
      socket = unquote(socket)
      user = unquote(socket).assigns.current_user

      if ExampleApp.Web.LiveAcl.acl_check(unquote(arg), unquote(params), user) == :ok do
        unquote(body)
      else
        {:noreply, socket |> put_flash(:error, "Access denied")}
      end
    end
  end

  def acl_check([{object, action}], params, user) when is_atom(object) and is_atom(action) do
    # perform the actual ACL check here!
    :ok
  end
end

It’s important to know that this event_acl_check/3 function is called at compile time: it returns a bit of code which will be inserted into the to-be-decorated function body.

In this case, we use the arguments from the context to retrieve the params and socket variables, and then return a function body which calls out to an acl_check/3 function to do the actual check.

When this function returns :ok, the unquote(body) part causes the actual function to be executed; but in the case of any other return value, nothing more is executed and only a flash message is displayed to the user, stating that the access is denied.

I have gist’ed the full source code of the helper module, hopefully useful to someone. The module also has a on_mount helper function, which can be used to provide access control check while mounting of the socket.

That’s it for this post, thanks for reading. You can find me on Twitter, genserver.social or the Elixir forum. Hope to keep you updated with more LiveView insights in the future!

Arjan Scherpenisse

Arjan Scherpenisse

Co-founder Botsquad

email me