diff options
| -rw-r--r-- | CHANGELOG.md | 3 | ||||
| -rw-r--r-- | config/test.exs | 2 | ||||
| -rw-r--r-- | docs/config.md | 3 | ||||
| -rw-r--r-- | lib/pleroma/plugs/http_security_plug.ex | 31 | ||||
| -rw-r--r-- | test/plugs/http_security_plug_test.exs | 133 | 
5 files changed, 107 insertions, 65 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index b0e849285..ea1f29304 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).  - Configuration: `fetch_initial_posts` option  - Configuration: `notify_email` option  - Configuration: Media proxy `whitelist` option +- Configuration: `report_uri` option  - Pleroma API: User subscriptions  - Pleroma API: Healthcheck endpoint  - Admin API: Endpoints for listing/revoking invite tokens @@ -98,7 +99,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).  - Mastodon API: Make `irreversible` field default to `false` [`POST /api/v1/filters`]  ## Removed -- Configuration: `config :pleroma, :fe` in favor of the more flexible `config :pleroma, :frontend_configurations`  +- Configuration: `config :pleroma, :fe` in favor of the more flexible `config :pleroma, :frontend_configurations`  ## [0.9.9999] - 2019-04-05  ### Security diff --git a/config/test.exs b/config/test.exs index a0c90c371..40db66170 100644 --- a/config/test.exs +++ b/config/test.exs @@ -61,6 +61,8 @@ config :pleroma, Pleroma.ScheduledActivity,  config :pleroma, :app_account_creation, max_requests: 5 +config :pleroma, :http_security, report_uri: "https://endpoint.com" +  try do    import_config "test.secret.exs"  rescue diff --git a/docs/config.md b/docs/config.md index 470f71b7c..c2af5c012 100644 --- a/docs/config.md +++ b/docs/config.md @@ -286,7 +286,8 @@ This will make Pleroma listen on `127.0.0.1` port `8080` and generate urls start  * ``sts``: Whether to additionally send a `Strict-Transport-Security` header  * ``sts_max_age``: The maximum age for the `Strict-Transport-Security` header if sent  * ``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"`. +* ``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.  ## :mrf_user_allowlist diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index a476f1d49..485ddfbc7 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -20,8 +20,9 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do    defp headers do      referrer_policy = Config.get([:http_security, :referrer_policy]) +    report_uri = Config.get([:http_security, :report_uri]) -    [ +    headers = [        {"x-xss-protection", "1; mode=block"},        {"x-permitted-cross-domain-policies", "none"},        {"x-frame-options", "DENY"}, @@ -30,12 +31,27 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do        {"x-download-options", "noopen"},        {"content-security-policy", csp_string() <> ";"}      ] + +    if report_uri do +      report_group = %{ +        "group" => "csp-endpoint", +        "max-age" => 10_886_400, +        "endpoints" => [ +          %{"url" => report_uri} +        ] +      } + +      headers ++ [{"reply-to", Jason.encode!(report_group)}] +    else +      headers +    end    end    defp csp_string do      scheme = Config.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])      connect_src = "connect-src 'self' #{static_url} #{websocket_url}" @@ -53,7 +69,7 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do          "script-src 'self'"        end -    [ +    main_part = [        "default-src 'none'",        "base-uri 'self'",        "frame-ancestors 'none'", @@ -63,11 +79,14 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do        "font-src 'self'",        "manifest-src 'self'",        connect_src, -      script_src, -      if scheme == "https" do -        "upgrade-insecure-requests" -      end +      script_src      ] + +    report = if report_uri, do: ["report-uri #{report_uri}; report-to csp-endpoint"], else: [] + +    insecure = if scheme == "https", do: ["upgrade-insecure-requests"], else: [] + +    (main_part ++ report ++ insecure)      |> Enum.join("; ")    end diff --git a/test/plugs/http_security_plug_test.exs b/test/plugs/http_security_plug_test.exs index 0cbb7e4b1..7dfd50c1f 100644 --- a/test/plugs/http_security_plug_test.exs +++ b/test/plugs/http_security_plug_test.exs @@ -7,77 +7,96 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do    alias Pleroma.Config    alias Plug.Conn -  test "it sends CSP headers when enabled", %{conn: conn} do -    Config.put([:http_security, :enabled], true) - -    conn = -      conn -      |> get("/api/v1/instance") - -    refute Conn.get_resp_header(conn, "x-xss-protection") == [] -    refute Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == [] -    refute Conn.get_resp_header(conn, "x-frame-options") == [] -    refute Conn.get_resp_header(conn, "x-content-type-options") == [] -    refute Conn.get_resp_header(conn, "x-download-options") == [] -    refute Conn.get_resp_header(conn, "referrer-policy") == [] -    refute Conn.get_resp_header(conn, "content-security-policy") == [] -  end +  describe "http security enabled" do +    setup do +      enabled = Config.get([:http_securiy, :enabled]) -  test "it does not send CSP headers when disabled", %{conn: conn} do -    Config.put([:http_security, :enabled], false) +      Config.put([:http_security, :enabled], true) -    conn = -      conn -      |> get("/api/v1/instance") +      on_exit(fn -> +        Config.put([:http_security, :enabled], enabled) +      end) -    assert Conn.get_resp_header(conn, "x-xss-protection") == [] -    assert Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == [] -    assert Conn.get_resp_header(conn, "x-frame-options") == [] -    assert Conn.get_resp_header(conn, "x-content-type-options") == [] -    assert Conn.get_resp_header(conn, "x-download-options") == [] -    assert Conn.get_resp_header(conn, "referrer-policy") == [] -    assert Conn.get_resp_header(conn, "content-security-policy") == [] -  end +      :ok +    end -  test "it sends STS headers when enabled", %{conn: conn} do -    Config.put([:http_security, :enabled], true) -    Config.put([:http_security, :sts], true) +    test "it sends CSP headers when enabled", %{conn: conn} do +      conn = get(conn, "/api/v1/instance") -    conn = -      conn -      |> get("/api/v1/instance") +      refute Conn.get_resp_header(conn, "x-xss-protection") == [] +      refute Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == [] +      refute Conn.get_resp_header(conn, "x-frame-options") == [] +      refute Conn.get_resp_header(conn, "x-content-type-options") == [] +      refute Conn.get_resp_header(conn, "x-download-options") == [] +      refute Conn.get_resp_header(conn, "referrer-policy") == [] +      refute Conn.get_resp_header(conn, "content-security-policy") == [] +    end -    refute Conn.get_resp_header(conn, "strict-transport-security") == [] -    refute Conn.get_resp_header(conn, "expect-ct") == [] -  end +    test "it sends STS headers when enabled", %{conn: conn} do +      Config.put([:http_security, :sts], true) -  test "it does not send STS headers when disabled", %{conn: conn} do -    Config.put([:http_security, :enabled], true) -    Config.put([:http_security, :sts], false) +      conn = get(conn, "/api/v1/instance") -    conn = -      conn -      |> get("/api/v1/instance") +      refute Conn.get_resp_header(conn, "strict-transport-security") == [] +      refute Conn.get_resp_header(conn, "expect-ct") == [] +    end -    assert Conn.get_resp_header(conn, "strict-transport-security") == [] -    assert Conn.get_resp_header(conn, "expect-ct") == [] -  end +    test "it does not send STS headers when disabled", %{conn: conn} do +      Config.put([:http_security, :sts], false) + +      conn = get(conn, "/api/v1/instance") + +      assert Conn.get_resp_header(conn, "strict-transport-security") == [] +      assert Conn.get_resp_header(conn, "expect-ct") == [] +    end + +    test "referrer-policy header reflects configured value", %{conn: conn} do +      conn = get(conn, "/api/v1/instance") + +      assert Conn.get_resp_header(conn, "referrer-policy") == ["same-origin"] -  test "referrer-policy header reflects configured value", %{conn: conn} do -    Config.put([:http_security, :enabled], true) +      Config.put([:http_security, :referrer_policy], "no-referrer") -    conn = -      conn -      |> get("/api/v1/instance") +      conn = +        build_conn() +        |> get("/api/v1/instance") -    assert Conn.get_resp_header(conn, "referrer-policy") == ["same-origin"] +      assert Conn.get_resp_header(conn, "referrer-policy") == ["no-referrer"] +    end -    Config.put([:http_security, :referrer_policy], "no-referrer") +    test "it sends `report-to` & `report-uri` CSP response headers" do +      conn = +        build_conn() +        |> get("/api/v1/instance") -    conn = -      build_conn() -      |> get("/api/v1/instance") +      [csp] = Conn.get_resp_header(conn, "content-security-policy") -    assert Conn.get_resp_header(conn, "referrer-policy") == ["no-referrer"] +      assert csp =~ ~r|report-uri https://endpoint.com; report-to csp-endpoint;| + +      [reply_to] = Conn.get_resp_header(conn, "reply-to") + +      assert reply_to == +               "{\"endpoints\":[{\"url\":\"https://endpoint.com\"}],\"group\":\"csp-endpoint\",\"max-age\":10886400}" +    end +  end + +  test "it does not send CSP headers when disabled", %{conn: conn} do +    enabled = Config.get([:http_securiy, :enabled]) + +    Config.put([:http_security, :enabled], false) + +    on_exit(fn -> +      Config.put([:http_security, :enabled], enabled) +    end) + +    conn = get(conn, "/api/v1/instance") + +    assert Conn.get_resp_header(conn, "x-xss-protection") == [] +    assert Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == [] +    assert Conn.get_resp_header(conn, "x-frame-options") == [] +    assert Conn.get_resp_header(conn, "x-content-type-options") == [] +    assert Conn.get_resp_header(conn, "x-download-options") == [] +    assert Conn.get_resp_header(conn, "referrer-policy") == [] +    assert Conn.get_resp_header(conn, "content-security-policy") == []    end  end  | 
