Search code examples
elixirphoenix-framework

Phoenix.HTML.Safe not implemented error when setting csp_nonce_assign_key


I'm trying to follow this tutorial for adding a Content-Security-Policy (CSP) header to the live dashboard route in Phoenix. It works fine until I use a Map as the csp_nonce_assign_key value instead of an atom.

Maps seem to be supported as per the documentation and it does seem to work when I set the value in my router.ex file like this:

live_dashboard "/dashboard",
  csp_nonce_assign_key: %{
    img: generate_nonce(),
    style: generate_nonce(),
    script: generate_nonce(),
  }

However, it doesn't work if I use a Plug like this:

# router.ex

live_dashboard "/dashboard",
  csp_nonce_assign_key: :csp_nonce_value
# my_plug.ex

def call(conn, _opts) do
  conn
  |> assign(:csp_nonce_value, %{
    img: generate_nonce(),
    style: generate_nonce(),
    script: generate_nonce(),
  })
end

When I use the Plug version I get the following error: protocol Phoenix.HTML.Safe not implemented for %{img: "fMIOCwnmMfsaOA", script: "m1oNHieWGoYMfw", style: "9EDcaW6JlgcfxQ"} of type Map.

What I don't understand is why the same error doesn't happen in the first version. PS. I'm new to Elixir, so I'm guessing there's something super obvious I'm missing here.


Solution

  • I have updated the blog post with an answer to your question. To make it short, you need to create 3 different assigns in the Plug, containing the 3 nonces.

    Then in the router, you give the name of these 3 assigns, not the nonce values.

    in the Plug:

      def call(conn, _opts) do
        # a random string is generated
        nonce_1 = generate_nonce()
        nonce_2 = generate_nonce()
        nonce_3 = generate_nonce()
    
        csp_headers = csp_headers(Application.fetch_env!(:my_app, :app_env), nonce_1, nonce_2, nonce_3)
    
        conn
        # the nonce is saved in the connection assigns
        |> Plug.Conn.assign(:img_src_nonce, nonce_1)
        |> Plug.Conn.assign(:img_style_nonce, nonce_2)
        |> Plug.Conn.assign(:img_script_nonce, nonce_3)
        |> Phoenix.Controller.put_secure_browser_headers(csp_headers)
      end
    

    In the router file

    live_dashboard("/phoenix-dashboard",
      metrics: Transport.PhoenixDashboardTelemetry,
      csp_nonce_assign_key: %{
        img: :img_src_nonce,
        style: :img_style_nonce,
        script: :img_script_nonce,
      })