From d38dc9ef75c06b84501532694842f0bc124712d8 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Thu, 22 Aug 2024 18:13:12 +0200 Subject: [PATCH 1/3] Add CSP headers to dev server --- dev.exs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/dev.exs b/dev.exs index 20e6814..c5954e9 100644 --- a/dev.exs +++ b/dev.exs @@ -115,7 +115,7 @@ defmodule ErrorTrackerDevWeb.Router do get "/exit", ErrorTrackerDevWeb.PageController, :exit scope "/dev" do - error_tracker_dashboard "/errors" + error_tracker_dashboard "/errors", csp_nonce_assign_key: :my_csp_nonce end end end @@ -142,10 +142,24 @@ defmodule ErrorTrackerDevWeb.Endpoint do plug Plug.RequestId plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] plug :maybe_exception + plug :set_csp plug ErrorTrackerDevWeb.Router def maybe_exception(%Plug.Conn{path_info: ["plug-exception"]}, _), do: raise("Plug exception") def maybe_exception(conn, _), do: conn + + defp set_csp(conn, _opts) do + nonce = 10 |> :crypto.strong_rand_bytes() |> Base.encode64() + + policies = [ + "script-src 'self' 'nonce-#{nonce}';", + "style-src 'self' 'nonce-#{nonce}';" + ] + + conn + |> Plug.Conn.assign(:my_csp_nonce, "#{nonce}") + |> Plug.Conn.put_resp_header("content-security-policy", Enum.join(policies, " ")) + end end defmodule ErrorTrackerDev.Telemetry do From b38e217167429ee56802291424898d9735c63906 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Thu, 22 Aug 2024 18:18:22 +0200 Subject: [PATCH 2/3] Use nonce in script and style tags --- .../web/components/layouts/root.html.heex | 4 +-- lib/error_tracker/web/hooks/set_assigns.ex | 8 ++++-- lib/error_tracker/web/router.ex | 27 +++++++++++++++---- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/lib/error_tracker/web/components/layouts/root.html.heex b/lib/error_tracker/web/components/layouts/root.html.heex index 8ecd539..e1b694a 100644 --- a/lib/error_tracker/web/components/layouts/root.html.heex +++ b/lib/error_tracker/web/components/layouts/root.html.heex @@ -10,10 +10,10 @@ <%= assigns[:page_title] || "🐛 ErrorTracker" %> - - diff --git a/lib/error_tracker/web/hooks/set_assigns.ex b/lib/error_tracker/web/hooks/set_assigns.ex index dc5cf54..a71f8bd 100644 --- a/lib/error_tracker/web/hooks/set_assigns.ex +++ b/lib/error_tracker/web/hooks/set_assigns.ex @@ -1,7 +1,11 @@ defmodule ErrorTracker.Web.Hooks.SetAssigns do @moduledoc false - def on_mount({:set_dashboard_path, path}, _params, _session, socket) do - {:cont, %{socket | private: Map.put(socket.private, :dashboard_path, path)}} + import Phoenix.Component, only: [assign: 2] + + def on_mount({:set_dashboard_path, path}, _params, session, socket) do + socket = %{socket | private: Map.put(socket.private, :dashboard_path, path)} + + {:cont, assign(socket, csp_nonces: session["csp_nonces"])} end end diff --git a/lib/error_tracker/web/router.ex b/lib/error_tracker/web/router.ex index 541d17a..cfc4ffb 100644 --- a/lib/error_tracker/web/router.ex +++ b/lib/error_tracker/web/router.ex @@ -3,6 +3,8 @@ defmodule ErrorTracker.Web.Router do ErrorTracker UI integration into your application's router. """ + alias ErrorTracker.Web.Hooks.SetAssigns + @doc """ Creates the routes needed to use the `ErrorTracker` web interface. @@ -45,17 +47,32 @@ defmodule ErrorTracker.Web.Router do @doc false def parse_options(opts, path) do custom_on_mount = Keyword.get(opts, :on_mount, []) - - on_mount = - [{ErrorTracker.Web.Hooks.SetAssigns, {:set_dashboard_path, path}}] ++ custom_on_mount - session_name = Keyword.get(opts, :as, :error_tracker_dashboard) + csp_nonce_assign_key = + case opts[:csp_nonce_assign_key] do + nil -> nil + key when is_atom(key) -> %{img: key, style: key, script: key} + keys when is_map(keys) -> Map.take(keys, [:img, :style, :script]) + end + session_opts = [ - on_mount: on_mount, + session: {__MODULE__, :__session__, [csp_nonce_assign_key]}, + on_mount: [{SetAssigns, {:set_dashboard_path, path}}] ++ custom_on_mount, root_layout: {ErrorTracker.Web.Layouts, :root} ] {session_name, session_opts} end + + @doc false + def __session__(conn, csp_nonce_assign_key) do + %{ + "csp_nonces" => %{ + img: conn.assigns[csp_nonce_assign_key[:img]], + style: conn.assigns[csp_nonce_assign_key[:style]], + script: conn.assigns[csp_nonce_assign_key[:script]] + } + } + end end From 70e9d14d2dc6d6fbccf7dcf1346bc3d4a3cc975b Mon Sep 17 00:00:00 2001 From: crbelaus Date: Thu, 22 Aug 2024 18:24:46 +0200 Subject: [PATCH 3/3] Update docs --- lib/error_tracker/web/router.ex | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/error_tracker/web/router.ex b/lib/error_tracker/web/router.ex index cfc4ffb..7e6c7b0 100644 --- a/lib/error_tracker/web/router.ex +++ b/lib/error_tracker/web/router.ex @@ -12,9 +12,9 @@ defmodule ErrorTracker.Web.Router do ## Security considerations - Errors may contain sensitive information so it is recommended to use the `on_mount` - option to provide a custom hook that implements authentication and authorization - for access control. + The dashboard inlines both the JS and CSS assets. This means that, if your + application has a Content Security Policy, you need to specify the + `csp_nonce_assign_key` option, which is explained below. ## Options @@ -23,6 +23,10 @@ defmodule ErrorTracker.Web.Router do * `as`: a session name to use for the dashboard LiveView session. By default it uses `:error_tracker_dashboard`. + + * `csp_nonce_assign_key`: an assign key to find the CSP nonce value used for assets. + Supports either `atom()` or a map of type + `%{optional(:img) => atom(), optional(:script) => atom(), optional(:style) => atom()}` """ defmacro error_tracker_dashboard(path, opts \\ []) do quote bind_quoted: [path: path, opts: opts] do