logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma
commit: 54923c2e55e2d241b53a62a4abbd2501abdcae32
parent: e7d6f133ebaf7f7515456b44ee700e5ea7527b50
Author: kaniini <nenolod@gmail.com>
Date:   Mon, 12 Nov 2018 15:30:42 +0000

Merge branch 'feature/csp-plug' into 'develop'

migrate CSP management to CSPPlug

See merge request pleroma/pleroma!441

Diffstat:

Mconfig/config.exs7+++++++
Mconfig/config.md7+++++++
Minstallation/caddyfile-pleroma.example22----------------------
Minstallation/pleroma-apache.conf9---------
Minstallation/pleroma.nginx11-----------
Minstallation/pleroma.vcl10----------
Mlib/mix/tasks/sample_config.eex4++++
Alib/pleroma/plugs/http_security_plug.ex59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/pleroma/web/endpoint.ex1+
Atest/plugs/http_security_plug_test.exs77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 155 insertions(+), 52 deletions(-)

diff --git 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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