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
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..7e6c7b0 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.
@@ -10,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
@@ -21,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
@@ -45,17 +51,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