diff options
| -rw-r--r-- | config/config.exs | 7 | ||||
| -rw-r--r-- | config/config.md | 7 | ||||
| -rw-r--r-- | installation/caddyfile-pleroma.example | 22 | ||||
| -rw-r--r-- | installation/pleroma-apache.conf | 9 | ||||
| -rw-r--r-- | installation/pleroma.nginx | 11 | ||||
| -rw-r--r-- | installation/pleroma.vcl | 10 | ||||
| -rw-r--r-- | lib/mix/tasks/sample_config.eex | 4 | ||||
| -rw-r--r-- | lib/pleroma/plugs/http_security_plug.ex | 59 | ||||
| -rw-r--r-- | lib/pleroma/web/endpoint.ex | 1 | ||||
| -rw-r--r-- | test/plugs/http_security_plug_test.exs | 77 | 
10 files changed, 155 insertions, 52 deletions
diff --git a/config/config.exs b/config/config.exs index e82c490e3..9cc558564 100644 --- a/config/config.exs +++ b/config/config.exs @@ -176,6 +176,13 @@ config :pleroma, :suggestions,    limit: 23,    web: "https://vinayaka.distsn.org/?{{host}}+{{user}}" +config :pleroma, :http_security, +  enabled: true, +  sts: false, +  sts_max_age: 31_536_000, +  ct_max_age: 2_592_000, +  referrer_policy: "same-origin" +  config :cors_plug,    max_age: 86_400,    methods: ["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"], diff --git a/config/config.md b/config/config.md index 51172fc4d..5b4110646 100644 --- a/config/config.md +++ b/config/config.md @@ -80,3 +80,10 @@ This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:i  * ``unfollow_blocked``: Whether blocks result in people getting unfollowed  * ``outgoing_blocks``: Whether to federate blocks to other instances  * ``deny_follow_blocked``: Whether to disallow following an account that has blocked the user in question + +## :http_security +* ``enabled``: Whether the managed content security policy is enabled +* ``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"`. diff --git a/installation/caddyfile-pleroma.example b/installation/caddyfile-pleroma.example index 305f2aa79..03ff000b6 100644 --- a/installation/caddyfile-pleroma.example +++ b/installation/caddyfile-pleroma.example @@ -21,28 +21,6 @@ example.tld  {      ciphers ECDHE-ECDSA-WITH-CHACHA20-POLY1305 ECDHE-RSA-WITH-CHACHA20-POLY1305 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-GCM-SHA256    } -  header / { -    X-XSS-Protection "1; mode=block" -    X-Frame-Options "DENY" -    X-Content-Type-Options "nosniff" -    Referrer-Policy "same-origin" -    Strict-Transport-Security "max-age=31536000; includeSubDomains;" -    Expect-CT "enforce, max-age=2592000" -    Content-Security-Policy "default-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self' wss://{host}; upgrade-insecure-requests;" -  } - -  # If you do not want remote frontends to be able to access your Pleroma backend server, remove these lines. -  # If you want to allow all origins access, remove the origin lines. -  # To use this directive, you need the http.cors plugin for Caddy. -  cors / { -    origin https://halcyon.example.tld -    origin https://pinafore.example.tld -    methods POST,PUT,DELETE,GET,PATCH,OPTIONS -    allowed_headers Authorization,Content-Type,Idempotency-Key -    exposed_headers Link,X-RateLimit-Reset,X-RateLimit-Limit,X-RateLimit-Remaining,X-Request-Id -  } -  # Stop removing lines here. -    # If you do not want to use the mediaproxy function, remove these lines.    # To use this directive, you need the http.cache plugin for Caddy.    cache { diff --git a/installation/pleroma-apache.conf b/installation/pleroma-apache.conf index fb777983e..d5e75044f 100644 --- a/installation/pleroma-apache.conf +++ b/installation/pleroma-apache.conf @@ -34,15 +34,6 @@ CustomLog ${APACHE_LOG_DIR}/access.log combined      SSLCompression          off      SSLSessionTickets       off -    Header always set X-Xss-Protection "1; mode=block" -    Header always set X-Frame-Options "DENY" -    Header always set X-Content-Type-Options "nosniff" -    Header always set Referrer-Policy same-origin -    Header always set Content-Security-Policy "default-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self' wss://${servername}; upgrade-insecure-requests;" - -    # Uncomment this only after you get HTTPS working. -    # Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" -      RewriteEngine On      RewriteCond %{HTTP:Connection} Upgrade [NC]      RewriteCond %{HTTP:Upgrade} websocket [NC] diff --git a/installation/pleroma.nginx b/installation/pleroma.nginx index 9b7419497..f0e684f2c 100644 --- a/installation/pleroma.nginx +++ b/installation/pleroma.nginx @@ -60,17 +60,6 @@ server {      client_max_body_size 16m;      location / { -        add_header X-XSS-Protection "1; mode=block" always; -        add_header X-Permitted-Cross-Domain-Policies "none" always; -        add_header X-Frame-Options "DENY" always; -        add_header X-Content-Type-Options "nosniff" always; -        add_header Referrer-Policy "same-origin" always; -        add_header X-Download-Options "noopen" always; -        add_header Content-Security-Policy "default-src 'none'; base-uri 'self'; form-action *; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self' wss://$server_name; upgrade-insecure-requests;" always; - -        # Uncomment this only after you get HTTPS working. -        # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; -          proxy_http_version 1.1;          proxy_set_header Upgrade $http_upgrade;          proxy_set_header Connection "upgrade"; diff --git a/installation/pleroma.vcl b/installation/pleroma.vcl index 74490be2a..63c1cb74d 100644 --- a/installation/pleroma.vcl +++ b/installation/pleroma.vcl @@ -119,13 +119,3 @@ sub vcl_pipe {          set bereq.http.connection = req.http.connection;      }  } - -sub vcl_deliver { -  set resp.http.X-Frame-Options = "DENY"; -  set resp.http.X-XSS-Protection = "1; mode=block"; -  set resp.http.X-Content-Type-Options = "nosniff"; -  set resp.http.Referrer-Policy = "same-origin"; -  set resp.http.Content-Security-Policy = "default-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self' wss://" + req.http.host + "; upgrade-insecure-requests;"; -  # Uncomment this only after you get HTTPS working. -  # set resp.http.Strict-Transport-Security= "max-age=31536000; includeSubDomains"; -} diff --git a/lib/mix/tasks/sample_config.eex b/lib/mix/tasks/sample_config.eex index 3881ead26..462c34636 100644 --- a/lib/mix/tasks/sample_config.eex +++ b/lib/mix/tasks/sample_config.eex @@ -25,6 +25,10 @@ config :pleroma, Pleroma.Repo,    hostname: "localhost",    pool_size: 10 +# Enable Strict-Transport-Security once SSL is working: +# config :pleroma, :http_security, +#   sts: true +  # Configure S3 support if desired.  # The public S3 endpoint is different depending on region and provider,  # consult your S3 provider's documentation for details on what to use. diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex new file mode 100644 index 000000000..960c7f6bf --- /dev/null +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -0,0 +1,59 @@ +defmodule Pleroma.Plugs.HTTPSecurityPlug do +  alias Pleroma.Config +  import Plug.Conn + +  def init(opts), do: opts + +  def call(conn, options) do +    if Config.get([:http_security, :enabled]) do +      conn = +        merge_resp_headers(conn, headers()) +        |> maybe_send_sts_header(Config.get([:http_security, :sts])) +    else +      conn +    end +  end + +  defp headers do +    referrer_policy = Config.get([:http_security, :referrer_policy]) + +    [ +      {"x-xss-protection", "1; mode=block"}, +      {"x-permitted-cross-domain-policies", "none"}, +      {"x-frame-options", "DENY"}, +      {"x-content-type-options", "nosniff"}, +      {"referrer-policy", referrer_policy}, +      {"x-download-options", "noopen"}, +      {"content-security-policy", csp_string() <> ";"} +    ] +  end + +  defp csp_string do +    [ +      "default-src 'none'", +      "base-uri 'self'", +      "form-action *", +      "frame-ancestors 'none'", +      "img-src 'self' data: https:", +      "media-src 'self' https:", +      "style-src 'self' 'unsafe-inline'", +      "font-src 'self'", +      "script-src 'self'", +      "connect-src 'self' " <> String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"), +      "upgrade-insecure-requests" +    ] +    |> Enum.join("; ") +  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]) + +    merge_resp_headers(conn, [ +      {"strict-transport-security", "max-age=#{max_age_sts}; includeSubDomains"}, +      {"expect-ct", "enforce, max-age=#{max_age_ct}"} +    ]) +  end + +  defp maybe_send_sts_header(conn, _), do: conn +end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index cb5de087b..7783b8e5c 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -12,6 +12,7 @@ defmodule Pleroma.Web.Endpoint do    # You should set gzip to true if you are running phoenix.digest    # when deploying your static files in production.    plug(CORSPlug) +  plug(Pleroma.Plugs.HTTPSecurityPlug)    plug(Plug.Static, at: "/media", from: Pleroma.Uploaders.Local.upload_path(), gzip: false) diff --git a/test/plugs/http_security_plug_test.exs b/test/plugs/http_security_plug_test.exs new file mode 100644 index 000000000..55040a108 --- /dev/null +++ b/test/plugs/http_security_plug_test.exs @@ -0,0 +1,77 @@ +defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do +  use Pleroma.Web.ConnCase +  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 + +  test "it does not send CSP headers when disabled", %{conn: conn} do +    Config.put([:http_security, :enabled], false) + +    conn = +      conn +      |> get("/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 + +  test "it sends STS headers when enabled", %{conn: conn} do +    Config.put([:http_security, :enabled], true) +    Config.put([:http_security, :sts], true) + +    conn = +      conn +      |> get("/api/v1/instance") + +    refute Conn.get_resp_header(conn, "strict-transport-security") == [] +    refute Conn.get_resp_header(conn, "expect-ct") == [] +  end + +  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 = +      conn +      |> get("/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 = +      conn +      |> get("/api/v1/instance") + +    assert Conn.get_resp_header(conn, "referrer-policy") == ["same-origin"] + +    Config.put([:http_security, :referrer_policy], "no-referrer") + +    conn = +      build_conn() +      |> get("/api/v1/instance") + +    assert Conn.get_resp_header(conn, "referrer-policy") == ["no-referrer"] +  end +end  | 
