diff options
| -rw-r--r-- | changelog.d/3904.security | 1 | ||||
| -rw-r--r-- | config/config.exs | 3 | ||||
| -rw-r--r-- | config/test.exs | 1 | ||||
| -rw-r--r-- | docs/configuration/cheatsheet.md | 1 | ||||
| -rw-r--r-- | lib/pleroma/application.ex | 7 | ||||
| -rw-r--r-- | lib/pleroma/web/plugs/http_security_plug.ex | 49 | ||||
| -rw-r--r-- | test/pleroma/web/plugs/http_security_plug_test.exs | 210 | 
7 files changed, 207 insertions, 65 deletions
diff --git a/changelog.d/3904.security b/changelog.d/3904.security new file mode 100644 index 000000000..04836d4e8 --- /dev/null +++ b/changelog.d/3904.security @@ -0,0 +1 @@ +HTTP Security: By default, don't allow unsafe-eval. The setting needs to be changed to allow Flash emulation. diff --git a/config/config.exs b/config/config.exs index cd86816fe..956d067f3 100644 --- a/config/config.exs +++ b/config/config.exs @@ -519,7 +519,8 @@ config :pleroma, :http_security,    sts: false,    sts_max_age: 31_536_000,    ct_max_age: 2_592_000, -  referrer_policy: "same-origin" +  referrer_policy: "same-origin", +  allow_unsafe_eval: false  config :cors_plug,    max_age: 86_400, diff --git a/config/test.exs b/config/test.exs index 3345bb3a9..6c88ad3c6 100644 --- a/config/test.exs +++ b/config/test.exs @@ -154,6 +154,7 @@ config :pleroma, Pleroma.Upload, config_impl: Pleroma.UnstubbedConfigMock  config :pleroma, Pleroma.ScheduledActivity, config_impl: Pleroma.UnstubbedConfigMock  config :pleroma, Pleroma.Web.RichMedia.Helpers, config_impl: Pleroma.StaticStubbedConfigMock  config :pleroma, Pleroma.Uploaders.IPFS, config_impl: Pleroma.UnstubbedConfigMock +config :pleroma, Pleroma.Web.Plugs.HTTPSecurityPlug, config_impl: Pleroma.StaticStubbedConfigMock  peer_module =    if String.to_integer(System.otp_release()) >= 25 do diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index ca2ce6369..78997c4db 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -472,6 +472,7 @@ This will make Pleroma listen on `127.0.0.1` port `8080` and generate urls start  * ``ct_max_age``: The maximum age for the `Expect-CT` header if sent.  * ``referrer_policy``: The referrer policy to use, either `"same-origin"` or `"no-referrer"`.  * ``report_uri``: Adds the specified url to `report-uri` and `report-to` group in CSP header. +* `allow_unsafe_eval`: Adds `wasm-unsafe-eval` to the CSP header. Needed for some non-essential frontend features like Flash emulation.  ### Pleroma.Web.Plugs.RemoteIp diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index d266d1836..0d9757b44 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Application do    @name Mix.Project.config()[:name]    @version Mix.Project.config()[:version]    @repository Mix.Project.config()[:source_url] +  @compile_env Mix.env()    def name, do: @name    def version, do: @version @@ -51,7 +52,11 @@ defmodule Pleroma.Application do      Pleroma.HTML.compile_scrubbers()      Pleroma.Config.Oban.warn()      Config.DeprecationWarnings.warn() -    Pleroma.Web.Plugs.HTTPSecurityPlug.warn_if_disabled() + +    if @compile_env != :test do +      Pleroma.Web.Plugs.HTTPSecurityPlug.warn_if_disabled() +    end +      Pleroma.ApplicationRequirements.verify!()      load_custom_modules()      Pleroma.Docs.JSON.compile() diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex index a27dcd0ab..38f6c511e 100644 --- a/lib/pleroma/web/plugs/http_security_plug.ex +++ b/lib/pleroma/web/plugs/http_security_plug.ex @@ -3,26 +3,27 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do -  alias Pleroma.Config    import Plug.Conn    require Logger +  @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config) +    def init(opts), do: opts    def call(conn, _options) do -    if Config.get([:http_security, :enabled]) do +    if @config_impl.get([:http_security, :enabled]) do        conn        |> merge_resp_headers(headers()) -      |> maybe_send_sts_header(Config.get([:http_security, :sts])) +      |> maybe_send_sts_header(@config_impl.get([:http_security, :sts]))      else        conn      end    end    def primary_frontend do -    with %{"name" => frontend} <- Config.get([:frontends, :primary]), -         available <- Config.get([:frontends, :available]), +    with %{"name" => frontend} <- @config_impl.get([:frontends, :primary]), +         available <- @config_impl.get([:frontends, :available]),           %{} = primary_frontend <- Map.get(available, frontend) do        {:ok, primary_frontend}      end @@ -37,8 +38,8 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do    end    def headers do -    referrer_policy = Config.get([:http_security, :referrer_policy]) -    report_uri = Config.get([:http_security, :report_uri]) +    referrer_policy = @config_impl.get([:http_security, :referrer_policy]) +    report_uri = @config_impl.get([:http_security, :report_uri])      custom_http_frontend_headers = custom_http_frontend_headers()      headers = [ @@ -86,10 +87,10 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do    @csp_start [Enum.join(static_csp_rules, ";") <> ";"]    defp csp_string do -    scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme] +    scheme = @config_impl.get([Pleroma.Web.Endpoint, :url])[:scheme]      static_url = Pleroma.Web.Endpoint.static_url()      websocket_url = Pleroma.Web.Endpoint.websocket_url() -    report_uri = Config.get([:http_security, :report_uri]) +    report_uri = @config_impl.get([:http_security, :report_uri])      img_src = "img-src 'self' data: blob:"      media_src = "media-src 'self'" @@ -97,8 +98,8 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do      # Strict multimedia CSP enforcement only when MediaProxy is enabled      {img_src, media_src, connect_src} = -      if Config.get([:media_proxy, :enabled]) && -           !Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do +      if @config_impl.get([:media_proxy, :enabled]) && +           !@config_impl.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do          sources = build_csp_multimedia_source_list()          { @@ -115,17 +116,21 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do        end      connect_src = -      if Config.get(:env) == :dev do +      if @config_impl.get([:env]) == :dev do          [connect_src, " http://localhost:3035/"]        else          connect_src        end      script_src = -      if Config.get(:env) == :dev do -        "script-src 'self' 'unsafe-eval'" +      if @config_impl.get([:http_security, :allow_unsafe_eval]) do +        if @config_impl.get([:env]) == :dev do +          "script-src 'self' 'unsafe-eval'" +        else +          "script-src 'self' 'wasm-unsafe-eval'" +        end        else -        "script-src 'self' 'wasm-unsafe-eval'" +        "script-src 'self'"        end      report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"] @@ -161,11 +166,11 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do    defp build_csp_multimedia_source_list do      media_proxy_whitelist =        [:media_proxy, :whitelist] -      |> Config.get() +      |> @config_impl.get()        |> build_csp_from_whitelist([]) -    captcha_method = Config.get([Pleroma.Captcha, :method]) -    captcha_endpoint = Config.get([captcha_method, :endpoint]) +    captcha_method = @config_impl.get([Pleroma.Captcha, :method]) +    captcha_endpoint = @config_impl.get([captcha_method, :endpoint])      base_endpoints =        [ @@ -173,7 +178,7 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do          [Pleroma.Upload, :base_url],          [Pleroma.Uploaders.S3, :public_endpoint]        ] -      |> Enum.map(&Config.get/1) +      |> Enum.map(&@config_impl.get/1)      [captcha_endpoint | base_endpoints]      |> Enum.map(&build_csp_param/1) @@ -200,7 +205,7 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do    end    def warn_if_disabled do -    unless Config.get([:http_security, :enabled]) do +    unless Pleroma.Config.get([:http_security, :enabled]) do        Logger.warning("                                   .i;;;;i.                                 iYcviii;vXY: @@ -245,8 +250,8 @@ your instance and your users via malicious posts:    end    defp maybe_send_sts_header(conn, true) do -    max_age_sts = Config.get([:http_security, :sts_max_age]) -    max_age_ct = Config.get([:http_security, :ct_max_age]) +    max_age_sts = @config_impl.get([:http_security, :sts_max_age]) +    max_age_ct = @config_impl.get([:http_security, :ct_max_age])      merge_resp_headers(conn, [        {"strict-transport-security", "max-age=#{max_age_sts}; includeSubDomains"}, diff --git a/test/pleroma/web/plugs/http_security_plug_test.exs b/test/pleroma/web/plugs/http_security_plug_test.exs index c79170382..11a351a41 100644 --- a/test/pleroma/web/plugs/http_security_plug_test.exs +++ b/test/pleroma/web/plugs/http_security_plug_test.exs @@ -3,14 +3,52 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do -  use Pleroma.Web.ConnCase +  use Pleroma.Web.ConnCase, async: true    alias Plug.Conn +  import Mox + +  setup do +    base_config = Pleroma.Config.get([:http_security]) +    %{base_config: base_config} +  end + +  defp mock_config(config, additional \\ %{}) do +    Pleroma.StaticStubbedConfigMock +    |> stub(:get, fn +      [:http_security, key] -> config[key] +      key -> additional[key] +    end) +  end +    describe "http security enabled" do -    setup do: clear_config([:http_security, :enabled], true) +    setup %{base_config: base_config} do +      %{base_config: Keyword.put(base_config, :enabled, true)} +    end + +    test "it does not contain unsafe-eval", %{conn: conn, base_config: base_config} do +      mock_config(base_config) + +      conn = get(conn, "/api/v1/instance") +      [header] = Conn.get_resp_header(conn, "content-security-policy") +      refute header =~ ~r/unsafe-eval/ +    end + +    test "with allow_unsafe_eval set, it does contain it", %{conn: conn, base_config: base_config} do +      base_config = +        base_config +        |> Keyword.put(:allow_unsafe_eval, true) + +      mock_config(base_config) + +      conn = get(conn, "/api/v1/instance") +      [header] = Conn.get_resp_header(conn, "content-security-policy") +      assert header =~ ~r/unsafe-eval/ +    end -    test "it sends CSP headers when enabled", %{conn: conn} do +    test "it sends CSP headers when enabled", %{conn: conn, base_config: base_config} do +      mock_config(base_config)        conn = get(conn, "/api/v1/instance")        refute Conn.get_resp_header(conn, "x-xss-protection") == [] @@ -22,8 +60,10 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do        refute Conn.get_resp_header(conn, "content-security-policy") == []      end -    test "it sends STS headers when enabled", %{conn: conn} do -      clear_config([:http_security, :sts], true) +    test "it sends STS headers when enabled", %{conn: conn, base_config: base_config} do +      base_config +      |> Keyword.put(:sts, true) +      |> mock_config()        conn = get(conn, "/api/v1/instance") @@ -31,8 +71,10 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do        refute Conn.get_resp_header(conn, "expect-ct") == []      end -    test "it does not send STS headers when disabled", %{conn: conn} do -      clear_config([:http_security, :sts], false) +    test "it does not send STS headers when disabled", %{conn: conn, base_config: base_config} do +      base_config +      |> Keyword.put(:sts, false) +      |> mock_config()        conn = get(conn, "/api/v1/instance") @@ -40,19 +82,30 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do        assert Conn.get_resp_header(conn, "expect-ct") == []      end -    test "referrer-policy header reflects configured value", %{conn: conn} do -      resp = get(conn, "/api/v1/instance") +    test "referrer-policy header reflects configured value", %{ +      conn: conn, +      base_config: base_config +    } do +      mock_config(base_config) +      resp = get(conn, "/api/v1/instance")        assert Conn.get_resp_header(resp, "referrer-policy") == ["same-origin"] -      clear_config([:http_security, :referrer_policy], "no-referrer") +      base_config +      |> Keyword.put(:referrer_policy, "no-referrer") +      |> mock_config        resp = get(conn, "/api/v1/instance")        assert Conn.get_resp_header(resp, "referrer-policy") == ["no-referrer"]      end -    test "it sends `report-to` & `report-uri` CSP response headers", %{conn: conn} do +    test "it sends `report-to` & `report-uri` CSP response headers", %{ +      conn: conn, +      base_config: base_config +    } do +      mock_config(base_config) +        conn = get(conn, "/api/v1/instance")        [csp] = Conn.get_resp_header(conn, "content-security-policy") @@ -65,7 +118,11 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do                 "{\"endpoints\":[{\"url\":\"https://endpoint.com\"}],\"group\":\"csp-endpoint\",\"max-age\":10886400}"      end -    test "default values for img-src and media-src with disabled media proxy", %{conn: conn} do +    test "default values for img-src and media-src with disabled media proxy", %{ +      conn: conn, +      base_config: base_config +    } do +      mock_config(base_config)        conn = get(conn, "/api/v1/instance")        [csp] = Conn.get_resp_header(conn, "content-security-policy") @@ -73,60 +130,129 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do        assert csp =~ "img-src 'self' data: blob: https:;"      end -    test "it sets the Service-Worker-Allowed header", %{conn: conn} do -      clear_config([:http_security, :enabled], true) -      clear_config([:frontends, :primary], %{"name" => "fedi-fe", "ref" => "develop"}) - -      clear_config([:frontends, :available], %{ -        "fedi-fe" => %{ -          "name" => "fedi-fe", -          "custom-http-headers" => [{"service-worker-allowed", "/"}] -        } -      }) - +    test "it sets the Service-Worker-Allowed header", %{conn: conn, base_config: base_config} do +      base_config +      |> Keyword.put(:enabled, true) + +      additional_config = +        %{} +        |> Map.put([:frontends, :primary], %{"name" => "fedi-fe", "ref" => "develop"}) +        |> Map.put( +          [:frontends, :available], +          %{ +            "fedi-fe" => %{ +              "name" => "fedi-fe", +              "custom-http-headers" => [{"service-worker-allowed", "/"}] +            } +          } +        ) + +      mock_config(base_config, additional_config)        conn = get(conn, "/api/v1/instance")        assert Conn.get_resp_header(conn, "service-worker-allowed") == ["/"]      end    end    describe "img-src and media-src" do -    setup do -      clear_config([:http_security, :enabled], true) -      clear_config([:media_proxy, :enabled], true) -      clear_config([:media_proxy, :proxy_opts, :redirect_on_failure], false) +    setup %{base_config: base_config} do +      base_config = +        base_config +        |> Keyword.put(:enabled, true) + +      additional_config = +        %{} +        |> Map.put([:media_proxy, :enabled], true) +        |> Map.put([:media_proxy, :proxy_opts, :redirect_on_failure], false) +        |> Map.put([:media_proxy, :whitelist], []) + +      %{base_config: base_config, additional_config: additional_config}      end -    test "media_proxy with base_url", %{conn: conn} do +    test "media_proxy with base_url", %{ +      conn: conn, +      base_config: base_config, +      additional_config: additional_config +    } do        url = "https://example.com" -      clear_config([:media_proxy, :base_url], url) + +      additional_config = +        additional_config +        |> Map.put([:media_proxy, :base_url], url) + +      mock_config(base_config, additional_config) +        assert_media_img_src(conn, url)      end -    test "upload with base url", %{conn: conn} do +    test "upload with base url", %{ +      conn: conn, +      base_config: base_config, +      additional_config: additional_config +    } do        url = "https://example2.com" -      clear_config([Pleroma.Upload, :base_url], url) + +      additional_config = +        additional_config +        |> Map.put([Pleroma.Upload, :base_url], url) + +      mock_config(base_config, additional_config) +        assert_media_img_src(conn, url)      end -    test "with S3 public endpoint", %{conn: conn} do +    test "with S3 public endpoint", %{ +      conn: conn, +      base_config: base_config, +      additional_config: additional_config +    } do        url = "https://example3.com" -      clear_config([Pleroma.Uploaders.S3, :public_endpoint], url) + +      additional_config = +        additional_config +        |> Map.put([Pleroma.Uploaders.S3, :public_endpoint], url) + +      mock_config(base_config, additional_config)        assert_media_img_src(conn, url)      end -    test "with captcha endpoint", %{conn: conn} do -      clear_config([Pleroma.Captcha.Mock, :endpoint], "https://captcha.com") +    test "with captcha endpoint", %{ +      conn: conn, +      base_config: base_config, +      additional_config: additional_config +    } do +      additional_config = +        additional_config +        |> Map.put([Pleroma.Captcha.Mock, :endpoint], "https://captcha.com") +        |> Map.put([Pleroma.Captcha, :method], Pleroma.Captcha.Mock) + +      mock_config(base_config, additional_config)        assert_media_img_src(conn, "https://captcha.com")      end -    test "with media_proxy whitelist", %{conn: conn} do -      clear_config([:media_proxy, :whitelist], ["https://example6.com", "https://example7.com"]) +    test "with media_proxy whitelist", %{ +      conn: conn, +      base_config: base_config, +      additional_config: additional_config +    } do +      additional_config = +        additional_config +        |> Map.put([:media_proxy, :whitelist], ["https://example6.com", "https://example7.com"]) + +      mock_config(base_config, additional_config)        assert_media_img_src(conn, "https://example7.com https://example6.com")      end      # TODO: delete after removing support bare domains for media proxy whitelist -    test "with media_proxy bare domains whitelist (deprecated)", %{conn: conn} do -      clear_config([:media_proxy, :whitelist], ["example4.com", "example5.com"]) +    test "with media_proxy bare domains whitelist (deprecated)", %{ +      conn: conn, +      base_config: base_config, +      additional_config: additional_config +    } do +      additional_config = +        additional_config +        |> Map.put([:media_proxy, :whitelist], ["example4.com", "example5.com"]) + +      mock_config(base_config, additional_config)        assert_media_img_src(conn, "example5.com example4.com")      end    end @@ -138,8 +264,10 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do      assert csp =~ "img-src 'self' data: blob: #{url};"    end -  test "it does not send CSP headers when disabled", %{conn: conn} do -    clear_config([:http_security, :enabled], false) +  test "it does not send CSP headers when disabled", %{conn: conn, base_config: base_config} do +    base_config +    |> Keyword.put(:enabled, false) +    |> mock_config      conn = get(conn, "/api/v1/instance")  | 
