From 6b9e4fbd06b2e7f1570c8a80ee5afac537d2cefe Mon Sep 17 00:00:00 2001 From: Ho-Yon Mak Date: Fri, 5 Apr 2024 10:06:06 +0100 Subject: [PATCH 1/5] Allow passing csp_nonce_assign_key into router --- README.md | 20 +++++++++++++++++++ lib/fun_with_flags/ui/router.ex | 15 +++++++++++++- lib/fun_with_flags/ui/templates.ex | 5 +++++ .../ui/templates/_head.html.eex | 4 ++-- .../ui/templates/details.html.eex | 2 +- test/fun_with_flags/ui/templates_test.exs | 8 ++++++-- 6 files changed, 48 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7481047..c23f6fa 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,26 @@ defmodule MyPhoenixAppWeb.Router do end ``` +### Content Security Policy + +Content security policy nonces can be passed into the router to allow usage of strict Content Security Policies throughout an application. + +This can be achieved by passing in a `csp_nonce_assign_key` to the `FunWithFlags.UI.Router` forward. Values for the nonces should be set in the Conn assigns before reaching this router. + +This an either be a single nonce value, or separate values for script and style tags. + +For example: + +``` elixir +forward "/", FunWithFlags.UI.Router, namespace: "feature-flags", csp_nonce_assign_key: :my_csp_nonce +``` + +Or: + +``` elixir +forward "/", FunWithFlags.UI.Router, namespace: "feature-flags", csp_nonce_assign_key: %{style: :my_style_nonce, script: :my_script_nonce} +``` + ## Caveats While the base `fun_with_flags` library is quite relaxed in terms of valid flag names, group names and actor identifers, this web dashboard extension applies some more restrictive rules. diff --git a/lib/fun_with_flags/ui/router.ex b/lib/fun_with_flags/ui/router.ex index 3251925..61413f8 100644 --- a/lib/fun_with_flags/ui/router.ex +++ b/lib/fun_with_flags/ui/router.ex @@ -30,7 +30,10 @@ defmodule FunWithFlags.UI.Router do @doc false def call(conn, opts) do - conn = extract_namespace(conn, opts) + conn = + conn + |> extract_namespace(opts) + |> extract_csp_nonce_key(opts) super(conn, opts) end @@ -286,6 +289,16 @@ defmodule FunWithFlags.UI.Router do Plug.Conn.assign(conn, :namespace, "/" <> ns) end + defp extract_csp_nonce_key(conn, opts) do + csp_nonce_assign_key = + case opts[:csp_nonce_assign_key] do + nil -> nil + key when is_atom(key) -> %{style: key, script: key} + %{} = keys -> Map.take(keys, [:style, :script]) + end + + Plug.Conn.put_private(conn, :csp_nonce_assign_key, csp_nonce_assign_key) + end defp assign_csrf_token(conn, _opts) do csrf_token = Plug.CSRFProtection.get_csrf_token() diff --git a/lib/fun_with_flags/ui/templates.ex b/lib/fun_with_flags/ui/templates.ex index 8b0f6cb..81932d1 100644 --- a/lib/fun_with_flags/ui/templates.ex +++ b/lib/fun_with_flags/ui/templates.ex @@ -82,4 +82,9 @@ defmodule FunWithFlags.UI.Templates do |> to_string() |> URI.encode() end + + def csp_nonce(conn, type) do + csp_nonce_assign_key = conn.private.csp_nonce_assign_key[type] + conn.assigns[csp_nonce_assign_key] + end end diff --git a/lib/fun_with_flags/ui/templates/_head.html.eex b/lib/fun_with_flags/ui/templates/_head.html.eex index 3422e90..80ec632 100644 --- a/lib/fun_with_flags/ui/templates/_head.html.eex +++ b/lib/fun_with_flags/ui/templates/_head.html.eex @@ -2,6 +2,6 @@ FunWithFlags - <%= @title %> - "> - "> + "> + "> diff --git a/lib/fun_with_flags/ui/templates/details.html.eex b/lib/fun_with_flags/ui/templates/details.html.eex index 04e0bb6..df9a35d 100644 --- a/lib/fun_with_flags/ui/templates/details.html.eex +++ b/lib/fun_with_flags/ui/templates/details.html.eex @@ -140,6 +140,6 @@ - + diff --git a/test/fun_with_flags/ui/templates_test.exs b/test/fun_with_flags/ui/templates_test.exs index de31887..915b236 100644 --- a/test/fun_with_flags/ui/templates_test.exs +++ b/test/fun_with_flags/ui/templates_test.exs @@ -12,8 +12,12 @@ defmodule FunWithFlags.UI.TemplatesTest do end setup do - conn = Plug.Conn.assign(%Plug.Conn{}, :namespace, "/pear") - conn = Plug.Conn.assign(conn, :csrf_token, Plug.CSRFProtection.get_csrf_token()) + conn = + %Plug.Conn{} + |> Plug.Conn.assign(:namespace, "/pear") + |> Plug.Conn.put_private(:csp_nonce_assign_key, %{style: :style_key, script: :script_key}) + |> Plug.Conn.assign(:csrf_token, Plug.CSRFProtection.get_csrf_token()) + {:ok, conn: conn} end From 5dabf27464ab72d9f6d639788431c7039062a537 Mon Sep 17 00:00:00 2001 From: Ho-Yon Mak Date: Fri, 5 Apr 2024 12:58:35 +0100 Subject: [PATCH 2/5] Add router tests for csp_nonce_assign_key option --- test/fun_with_flags/ui/router_test.exs | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/fun_with_flags/ui/router_test.exs b/test/fun_with_flags/ui/router_test.exs index 3863acd..6842446 100644 --- a/test/fun_with_flags/ui/router_test.exs +++ b/test/fun_with_flags/ui/router_test.exs @@ -198,6 +198,39 @@ defmodule FunWithFlags.UI.RouterTest do end end + describe "CSP nonce assign key option" do + test "if csp_nonce_assign_key is set, the CSP nonce is rendered in the script and link tags" do + {:ok, true} = FunWithFlags.enable :coconut + + csp_opts = [csp_nonce_assign_key: :pineapple] + + conn = + conn(:get, "/flags/coconut") + |> Plug.Conn.assign(:pineapple, "mango") + |> Router.call(Router.init(csp_opts)) + + assert 200 = conn.status + assert conn.resp_body =~ ~r| + diff --git a/test/fun_with_flags/ui/templates_test.exs b/test/fun_with_flags/ui/templates_test.exs index 915b236..5408cf5 100644 --- a/test/fun_with_flags/ui/templates_test.exs +++ b/test/fun_with_flags/ui/templates_test.exs @@ -12,12 +12,8 @@ defmodule FunWithFlags.UI.TemplatesTest do end setup do - conn = - %Plug.Conn{} - |> Plug.Conn.assign(:namespace, "/pear") - |> Plug.Conn.put_private(:csp_nonce_assign_key, %{style: :style_key, script: :script_key}) - |> Plug.Conn.assign(:csrf_token, Plug.CSRFProtection.get_csrf_token()) - + conn = Plug.Conn.assign(%Plug.Conn{}, :namespace, "/pear") + conn = Plug.Conn.assign(conn, :csrf_token, Plug.CSRFProtection.get_csrf_token()) {:ok, conn: conn} end @@ -190,4 +186,20 @@ defmodule FunWithFlags.UI.TemplatesTest do assert String.contains?(out, ~s{The flag watermelon doesn't exist.}) end end + + describe "CSP nonce" do + test "it includes a CSP nonce if provided", %{conn: conn} do + flag = %Flag{name: :avocado, gates: []} + + conn = + conn + |> Plug.Conn.put_private(:csp_nonce_assign_key, %{script: :script_nonce, style: :style_nonce}) + |> Plug.Conn.assign(:script_nonce, "honeydew") + |> Plug.Conn.assign(:style_nonce, "watermelon") + + out = Templates.details(conn: conn, flag: flag) + assert String.contains?(out, ~s{