Created
November 16, 2025 18:43
-
-
Save hauleth/af608e6449a32d9884a8311412ad0a64 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| defmodule LangustaWeb.Plug.CSP do | |
| import Plug.Conn | |
| @behaviour Plug | |
| @impl true | |
| def init(opts) do | |
| enable? = opts[:enable?] || (&__MODULE__.__true__/1) | |
| force = opts[:force] | |
| report_only = opts[:report_only] | |
| report_uri = opts[:report_uri] | |
| if report_only not in [nil, %{}] and report_uri in [nil, ""] do | |
| raise ArgumentError, | |
| "`:report_uri` is required when `:report_only` field is non empty" | |
| end | |
| %{ | |
| force: force, | |
| report_only: report_only, | |
| enable?: enable?, | |
| report_uri: report_uri | |
| } | |
| end | |
| @impl true | |
| def call(conn, opts) when is_map(opts) do | |
| if opts.enable?.(conn) do | |
| nonce = gen_nonce() | |
| force = header_value(opts.force, nonce, "") | |
| report_only = | |
| header_value(opts.report_only, nonce, "; report-uri #{opts.report_uri}") | |
| conn | |
| |> set_header("content-security-policy", force) | |
| |> set_header("content-security-policy-report-only", report_only) | |
| |> assign(:csp_nonce, nonce) | |
| else | |
| conn | |
| end | |
| end | |
| defp set_header(conn, _name, nil), do: conn | |
| defp set_header(conn, name, values), do: put_resp_header(conn, name, values) | |
| defp header_value(nil, _nonce, _suffix), do: nil | |
| defp header_value(resources, nonce, suffix) do | |
| resources = | |
| Enum.map_intersperse(resources, "; ", fn {key, values} -> | |
| "#{key}-src #{build_resource(values, nonce)}" | |
| end) | |
| IO.iodata_to_binary([resources, suffix]) | |
| end | |
| defp gen_nonce, | |
| do: Base.url_encode64(:crypto.strong_rand_bytes(16), padding: false) | |
| defp build_resource(:all, _nonce), do: "*" | |
| defp build_resource(values, nonce) when is_list(values) do | |
| Enum.map_intersperse(values, ?\s, &build(&1, nonce)) | |
| end | |
| defp build(:nonce, nonce), do: "'nonce-#{nonce}'" | |
| defp build(atom, _) when is_atom(atom) do | |
| name = atom |> Atom.to_string() |> String.replace("_", "-") | |
| "'#{name}'" | |
| end | |
| defp build({algo, input}, _) when algo in ~w[sha256 sha384 sha512]a do | |
| digest = Base.url_encode64(:crypto.hash(algo, input)) | |
| "'#{algo}-#{digest}'" | |
| end | |
| defp build(url, _) when is_binary(url), do: url | |
| @doc false | |
| def __true__(_conn), do: true | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment