Elixir/Phoenix Security: What is CSRF via Action Reuse?

Michael Lubas, 2023-03-28

The typical description of cross site request forgery (CSRF) involves a POST request being triggered without a secure token. If there’s a state changing HTML form making a POST, with a CSRF token, that’s validated by the server, that should be secure. Consider a web application where both a GET and POST request can perform the same state changing action. This is likely not the developer’s intention, but that is the root of most security problems in software.

Is CSRF possible with a GET request? Yes, in fact it’s worse than a POST request, because GET requests should never be used to perform state changing actions in a web application (place an order, transfer money, etc). The Sobelow finding Config.CSRFRoute: CSRF via Action Reuse states:

  This type of CSRF is flagged by `sobelow` when state-changing
  routes share an action with GET-based routes. For example:

      get "/users", UserController, :new
      post "/users", UserController, :new

  In this instance, it may be possible to trigger the POST
  functionality with a GET request and query parameters.

A Google search for “Action Reuse CSRF” does not return any useful links, and the above text may be confusing if you are thinking of CSRF in terms of POST requests and CSRF tokens. Why does the router pattern above, the:

  get "/users", UserController, :new
  post "/users", UserController, :new

result in CSRF? Isn’t CSRF related to HTML forms and HTTP requests? Let’s use Potion Shop to explain.

@ potion_shop % mix sobelow
...
-----------------------------------------------

Config.CSRFRoute: CSRF via Action Reuse - High Confidence
File: lib/carafe_web/router.ex
Action: edit_bio
Line: 98

-----------------------------------------------

The relevant lines in router.ex:

  scope "/", CarafeWeb do
    pipe_through [:browser, :require_authenticated_user]

    get "/users/settings/edit_bio", UserSettingsController, :edit_bio
    post "/users/settings/edit_bio", UserSettingsController, :edit_bio

In Potion Shop, you can update your bio from the settings page:

ar0

Notice that the form to edit the bio has an action to /users/settings/edit_bio, with a method of POST, and a valid CSRF token. In router.ex:

pipeline :browser do
  plug :accepts, ["html"]
  plug :fetch_session
  plug :fetch_live_flash
  plug :put_root_layout, {CarafeWeb.LayoutView, :root}
  plug :protect_from_forgery
  plug :put_secure_browser_headers
  plug :fetch_current_user
end

The :browser pipeline has :protect_from_forgery enabled, so this form should not be vulnerable to CSRF. So why the Sobelow finding? The answer is that this form (making the POST request) is not vulnerable to CSRF, but the application is, via GET request.

What does a POST request look like to edit the user’s bio?

ar1

The body of the POST:

ar2

Notice the payload of the POST request:

_csrf_token=MQcMaRI3KRovHHwcD2pcFQACBBozXjImEpjPefcIfMND_01wk4ryT4_j&user%5Bbio%5D=This+is+some+info+about+my+profile+page

(broken up)
_csrf_token=(ignore for now)
&
user%5Bbio%5D=This+is+some+info+about+my+profile+page

In Phoenix, that POST request goes to the UserSettingsController, to the :edit_bio action function:

  scope "/", CarafeWeb do
    pipe_through [:browser, :require_authenticated_user]

    get "/users/settings/edit_bio", UserSettingsController, :edit_bio
    post "/users/settings/edit_bio", UserSettingsController, :edit_bio
def edit_bio(conn, %{"user" => params}) do
  case Accounts.update_user_bio(conn.assigns.current_user, params) do
    {:ok, _bio} ->
      conn
      |> put_flash(:info, "Bio Update Successful")
      |> redirect(to: Routes.user_settings_path(conn, :edit))
    {:error, changeset} ->
      render(conn, "edit.html", bio_changeset: changeset)
  end
end

This is the root cause of the vulnerability, %{"user" => params} can be set by a GET request. If you log into Potion Shop and visit the URL:

http://localhost:4000/users/settings/edit_bio?user%5Bbio%5D=Hacked+LOL

ar3

Your Bio is updated, without a POST request. This is because the GET request hits the same edit_bio/2 function in UserSettingsController as the POST request. The GET route is unnecessary, there’s no reason for it to exist. Deleting the line:

  get "/users/settings/edit_bio", UserSettingsController, :edit_bio

fixes this vulnerability. Action reuse CSRF occurs when a misconfigured router introduces state changing GET requests in the application. Sobelow’s check for this vulnerability is very smart, and all Elixir/Phoenix developers should be aware of this issue.


Paraxial.io stops data breaches by helping developers ship secure applications. Get a demo or start for free.

Subscribe to stay up to date on new posts.