From 113908f820b65499397fef4fb4ca3bea36ca9d10 Mon Sep 17 00:00:00 2001 From: "byte[]" Date: Sun, 23 Aug 2020 18:30:58 -0400 Subject: [PATCH] allow CSP customization on a per-controller basis --- .../plugs/content_security_policy_plug.ex | 59 ++++++++++++++++--- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/lib/philomena_web/plugs/content_security_policy_plug.ex b/lib/philomena_web/plugs/content_security_policy_plug.ex index ab26e399..27c81bed 100644 --- a/lib/philomena_web/plugs/content_security_policy_plug.ex +++ b/lib/philomena_web/plugs/content_security_policy_plug.ex @@ -1,5 +1,11 @@ defmodule PhilomenaWeb.ContentSecurityPolicyPlug do - alias Plug.Conn + import Plug.Conn + + @allowed_sources [ + :script_src, + :frame_src, + :style_src + ] def init(opts) do opts @@ -9,18 +15,57 @@ defmodule PhilomenaWeb.ContentSecurityPolicyPlug do cdn_uri = cdn_uri() camo_uri = camo_uri() - csp_value = - "default-src 'self' #{cdn_uri}; object-src 'none'; " <> - "frame-ancestors 'none'; frame-src 'none'; form-action 'self'; " <> - "manifest-src 'self'; img-src 'self' data: #{cdn_uri} #{camo_uri}; " <> - "block-all-mixed-content" + register_before_send(conn, fn conn -> + config = get_config(conn) - Conn.put_resp_header(conn, "content-security-policy", csp_value) + script_src = Keyword.get(config, :script_src, []) + style_src = Keyword.get(config, :style_src, []) + frame_src = Keyword.get(config, :frame_src, nil) + + csp_config = [ + {:default_src, ["'self'", cdn_uri]}, + {:script_src, ["'self'", cdn_uri | script_src]}, + {:style_src, ["'self'", cdn_uri | style_src]}, + {:object_src, ["'none'"]}, + {:frame_ancestors, ["'none'"]}, + {:frame_src, frame_src || ["'none'"]}, + {:form_action, ["'self'"]}, + {:manifest_src, ["'self'"]}, + {:img_src, ["'self'", "data:", cdn_uri, camo_uri]}, + {:block_all_mixed_content, []} + ] + + csp_value = + csp_config + |> Enum.map(&cspify_element/1) + |> Enum.join("; ") + + put_resp_header(conn, "content-security-policy", csp_value) + end) end + def permit_source(conn, key, value) when key in @allowed_sources do + conn + |> get_config() + |> Keyword.update(key, [], &[value | &1]) + |> set_config(conn) + end + + defp get_config(conn), do: conn.private[:csp] || [] + defp set_config(value, conn), do: put_private(conn, :csp, value) + defp cdn_uri, do: Application.get_env(:philomena, :cdn_host) |> to_uri() defp camo_uri, do: Application.get_env(:philomena, :camo_host) |> to_uri() defp to_uri(host) when host in [nil, ""], do: "" defp to_uri(host), do: URI.to_string(%URI{scheme: "https", host: host}) + + defp cspify_element({key, value}) do + key = + key + |> Atom.to_string() + |> String.replace("_", "-") + + Enum.join([key | value], " ") + end end