Skip to content

Commit cfb9e59

Browse files
authored
Use CSP nonces for script and style tags (#61)
This pull request adds a new `:csp_nonce_assign_key` option to the `error_tracker_dashboard/2` macro. If provided, the error tracker will fetch the nonce from the given assign key and use it in the `<style>` and `<script>` tags. This allows using the ErrorTracker dashboard in environments with a restricted content security policy without requiring the usage of `unsafe-inline`, which should be avoided. This implementation is based on the [Phoenix LiveDashboard](https://hexdocs.pm/phoenix_live_dashboard/Phoenix.LiveDashboard.Router.html#live_dashboard/2) one. I've updated the `dev.exs` script to use CSP headers. If we remove the new option we will see that the ErrorTracker dashboard doesn't have any styles. Closes #58
1 parent eba2338 commit cfb9e59

File tree

4 files changed

+52
-13
lines changed

4 files changed

+52
-13
lines changed

dev.exs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ defmodule ErrorTrackerDevWeb.Router do
115115
get "/exit", ErrorTrackerDevWeb.PageController, :exit
116116

117117
scope "/dev" do
118-
error_tracker_dashboard "/errors"
118+
error_tracker_dashboard "/errors", csp_nonce_assign_key: :my_csp_nonce
119119
end
120120
end
121121
end
@@ -142,10 +142,24 @@ defmodule ErrorTrackerDevWeb.Endpoint do
142142
plug Plug.RequestId
143143
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
144144
plug :maybe_exception
145+
plug :set_csp
145146
plug ErrorTrackerDevWeb.Router
146147

147148
def maybe_exception(%Plug.Conn{path_info: ["plug-exception"]}, _), do: raise("Plug exception")
148149
def maybe_exception(conn, _), do: conn
150+
151+
defp set_csp(conn, _opts) do
152+
nonce = 10 |> :crypto.strong_rand_bytes() |> Base.encode64()
153+
154+
policies = [
155+
"script-src 'self' 'nonce-#{nonce}';",
156+
"style-src 'self' 'nonce-#{nonce}';"
157+
]
158+
159+
conn
160+
|> Plug.Conn.assign(:my_csp_nonce, "#{nonce}")
161+
|> Plug.Conn.put_resp_header("content-security-policy", Enum.join(policies, " "))
162+
end
149163
end
150164

151165
defmodule ErrorTrackerDev.Telemetry do

lib/error_tracker/web/components/layouts/root.html.heex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010

1111
<title><%= assigns[:page_title] || "🐛 ErrorTracker" %></title>
1212

13-
<style>
13+
<style nonce={@csp_nonces[:style]}>
1414
<%= raw get_content(:css) %>
1515
</style>
16-
<script>
16+
<script nonce={@csp_nonces[:script]}>
1717
<%= raw get_content(:js) %>
1818
</script>
1919
</head>
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
defmodule ErrorTracker.Web.Hooks.SetAssigns do
22
@moduledoc false
33

4-
def on_mount({:set_dashboard_path, path}, _params, _session, socket) do
5-
{:cont, %{socket | private: Map.put(socket.private, :dashboard_path, path)}}
4+
import Phoenix.Component, only: [assign: 2]
5+
6+
def on_mount({:set_dashboard_path, path}, _params, session, socket) do
7+
socket = %{socket | private: Map.put(socket.private, :dashboard_path, path)}
8+
9+
{:cont, assign(socket, csp_nonces: session["csp_nonces"])}
610
end
711
end

lib/error_tracker/web/router.ex

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@ defmodule ErrorTracker.Web.Router do
33
ErrorTracker UI integration into your application's router.
44
"""
55

6+
alias ErrorTracker.Web.Hooks.SetAssigns
7+
68
@doc """
79
Creates the routes needed to use the `ErrorTracker` web interface.
810
911
It requires a path in which you are going to serve the web interface.
1012
1113
## Security considerations
1214
13-
Errors may contain sensitive information so it is recommended to use the `on_mount`
14-
option to provide a custom hook that implements authentication and authorization
15-
for access control.
15+
The dashboard inlines both the JS and CSS assets. This means that, if your
16+
application has a Content Security Policy, you need to specify the
17+
`csp_nonce_assign_key` option, which is explained below.
1618
1719
## Options
1820
@@ -21,6 +23,10 @@ defmodule ErrorTracker.Web.Router do
2123
2224
* `as`: a session name to use for the dashboard LiveView session. By default
2325
it uses `:error_tracker_dashboard`.
26+
27+
* `csp_nonce_assign_key`: an assign key to find the CSP nonce value used for assets.
28+
Supports either `atom()` or a map of type
29+
`%{optional(:img) => atom(), optional(:script) => atom(), optional(:style) => atom()}`
2430
"""
2531
defmacro error_tracker_dashboard(path, opts \\ []) do
2632
quote bind_quoted: [path: path, opts: opts] do
@@ -45,17 +51,32 @@ defmodule ErrorTracker.Web.Router do
4551
@doc false
4652
def parse_options(opts, path) do
4753
custom_on_mount = Keyword.get(opts, :on_mount, [])
48-
49-
on_mount =
50-
[{ErrorTracker.Web.Hooks.SetAssigns, {:set_dashboard_path, path}}] ++ custom_on_mount
51-
5254
session_name = Keyword.get(opts, :as, :error_tracker_dashboard)
5355

56+
csp_nonce_assign_key =
57+
case opts[:csp_nonce_assign_key] do
58+
nil -> nil
59+
key when is_atom(key) -> %{img: key, style: key, script: key}
60+
keys when is_map(keys) -> Map.take(keys, [:img, :style, :script])
61+
end
62+
5463
session_opts = [
55-
on_mount: on_mount,
64+
session: {__MODULE__, :__session__, [csp_nonce_assign_key]},
65+
on_mount: [{SetAssigns, {:set_dashboard_path, path}}] ++ custom_on_mount,
5666
root_layout: {ErrorTracker.Web.Layouts, :root}
5767
]
5868

5969
{session_name, session_opts}
6070
end
71+
72+
@doc false
73+
def __session__(conn, csp_nonce_assign_key) do
74+
%{
75+
"csp_nonces" => %{
76+
img: conn.assigns[csp_nonce_assign_key[:img]],
77+
style: conn.assigns[csp_nonce_assign_key[:style]],
78+
script: conn.assigns[csp_nonce_assign_key[:script]]
79+
}
80+
}
81+
end
6182
end

0 commit comments

Comments
 (0)