http_security_plug.ex 6.47 KB
Newer Older
1
# Pleroma: A lightweight social networking server
2
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3
4
# SPDX-License-Identifier: AGPL-3.0-only

kaniini's avatar
kaniini committed
5
defmodule Pleroma.Plugs.HTTPSecurityPlug do
kaniini's avatar
kaniini committed
6
  alias Pleroma.Config
kaniini's avatar
kaniini committed
7
8
  import Plug.Conn

9
10
  require Logger

kaniini's avatar
kaniini committed
11
12
  def init(opts), do: opts

Maksim's avatar
Maksim committed
13
  def call(conn, _options) do
kaniini's avatar
kaniini committed
14
    if Config.get([:http_security, :enabled]) do
Maksim's avatar
Maksim committed
15
16
17
      conn
      |> merge_resp_headers(headers())
      |> maybe_send_sts_header(Config.get([:http_security, :sts]))
kaniini's avatar
kaniini committed
18
19
20
    else
      conn
    end
kaniini's avatar
kaniini committed
21
22
23
  end

  defp headers do
24
    referrer_policy = Config.get([:http_security, :referrer_policy])
25
    report_uri = Config.get([:http_security, :report_uri])
26

27
    headers = [
kaniini's avatar
kaniini committed
28
29
30
31
      {"x-xss-protection", "1; mode=block"},
      {"x-permitted-cross-domain-policies", "none"},
      {"x-frame-options", "DENY"},
      {"x-content-type-options", "nosniff"},
32
      {"referrer-policy", referrer_policy},
kaniini's avatar
kaniini committed
33
      {"x-download-options", "noopen"},
34
      {"content-security-policy", csp_string()}
kaniini's avatar
kaniini committed
35
    ]
36
37
38
39
40
41
42
43
44
45

    if report_uri do
      report_group = %{
        "group" => "csp-endpoint",
        "max-age" => 10_886_400,
        "endpoints" => [
          %{"url" => report_uri}
        ]
      }

46
      [{"reply-to", Jason.encode!(report_group)} | headers]
47
48
49
    else
      headers
    end
kaniini's avatar
kaniini committed
50
51
  end

52
53
54
55
56
57
58
59
60
61
  static_csp_rules = [
    "default-src 'none'",
    "base-uri 'self'",
    "frame-ancestors 'none'",
    "style-src 'self' 'unsafe-inline'",
    "font-src 'self'",
    "manifest-src 'self'"
  ]

  @csp_start [Enum.join(static_csp_rules, ";") <> ";"]
62

kaniini's avatar
kaniini committed
63
  defp csp_string do
64
    scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
65
    static_url = Pleroma.Web.Endpoint.static_url()
66
    websocket_url = Pleroma.Web.Endpoint.websocket_url()
67
    report_uri = Config.get([:http_security, :report_uri])
68

69
70
71
72
73
74
75
76
77
    img_src = "img-src 'self' data: blob:"
    media_src = "media-src 'self'"

    {img_src, media_src} =
      if Config.get([:media_proxy, :enabled]) &&
           !Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do
        sources = get_proxy_and_attachment_sources()
        {[img_src, sources], [media_src, sources]}
      else
78
        {[img_src, " https:"], [media_src, " https:"]}
79
80
      end

Alex Gleason's avatar
Alex Gleason committed
81
    connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
82
83

    connect_src =
84
      if Pleroma.Config.get(:env) == :dev do
85
        [connect_src, " http://localhost:3035/"]
86
      else
87
        connect_src
88
89
90
      end

    script_src =
91
      if Pleroma.Config.get(:env) == :dev do
92
93
94
95
        "script-src 'self' 'unsafe-eval'"
      else
        "script-src 'self'"
      end
96

97
98
99
100
    report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"]
    insecure = if scheme == "https", do: "upgrade-insecure-requests"

    @csp_start
101
102
    |> add_csp_param(img_src)
    |> add_csp_param(media_src)
103
104
105
106
107
108
    |> add_csp_param(connect_src)
    |> add_csp_param(script_src)
    |> add_csp_param(insecure)
    |> add_csp_param(report)
    |> :erlang.iolist_to_binary()
  end
109

110
111
112
113
114
115
  defp get_proxy_and_attachment_sources do
    media_proxy_whitelist =
      Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc ->
        add_source(acc, host)
      end)

116
    media_proxy_base_url =
117
      if Config.get([:media_proxy, :base_url]),
118
119
        do: URI.parse(Config.get([:media_proxy, :base_url])).host

120
121
122
123
124
125
126
127
128
    upload_base_url =
      if Config.get([Pleroma.Upload, :base_url]),
        do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host

    s3_endpoint =
      if Config.get([Pleroma.Upload, :uploader]) == Pleroma.Uploaders.S3,
        do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host

    []
129
    |> add_source(media_proxy_base_url)
130
131
132
133
134
135
136
137
    |> add_source(upload_base_url)
    |> add_source(s3_endpoint)
    |> add_source(media_proxy_whitelist)
  end

  defp add_source(iodata, nil), do: iodata
  defp add_source(iodata, source), do: [[?\s, source] | iodata]

138
  defp add_csp_param(csp_iodata, nil), do: csp_iodata
139

140
  defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata]
kaniini's avatar
kaniini committed
141

142
143
  def warn_if_disabled do
    unless Config.get([:http_security, :enabled]) do
minibikini's avatar
minibikini committed
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
      Logger.warn("
                                 .i;;;;i.
                               iYcviii;vXY:
                             .YXi       .i1c.
                            .YC.     .    in7.
                           .vc.   ......   ;1c.
                           i7,   ..        .;1;
                          i7,   .. ...      .Y1i
                         ,7v     .6MMM@;     .YX,
                        .7;.   ..IMMMMMM1     :t7.
                       .;Y.     ;$MMMMMM9.     :tc.
                       vY.   .. .nMMM@MMU.      ;1v.
                      i7i   ...  .#MM@M@C. .....:71i
                     it:   ....   $MMM@9;.,i;;;i,;tti
                    :t7.  .....   0MMMWv.,iii:::,,;St.
                   .nC.   .....   IMMMQ..,::::::,.,czX.
                  .ct:   ....... .ZMMMI..,:::::::,,:76Y.
                  c2:   ......,i..Y$M@t..:::::::,,..inZY
                 vov   ......:ii..c$MBc..,,,,,,,,,,..iI9i
                i9Y   ......iii:..7@MA,..,,,,,,,,,....;AA:
               iIS.  ......:ii::..;@MI....,............;Ez.
              .I9.  ......:i::::...8M1..................C0z.
             .z9;  ......:i::::,.. .i:...................zWX.
             vbv  ......,i::::,,.      ................. :AQY
            c6Y.  .,...,::::,,..:t0@@QY. ................ :8bi
           :6S. ..,,...,:::,,,..EMMMMMMI. ............... .;bZ,
          :6o,  .,,,,..:::,,,..i#MMMMMM#v.................  YW2.
         .n8i ..,,,,,,,::,,,,.. tMMMMM@C:.................. .1Wn
         7Uc. .:::,,,,,::,,,,..   i1t;,..................... .UEi
         7C...::::::::::::,,,,..        ....................  vSi.
         ;1;...,,::::::,.........       ..................    Yz:
          v97,.........                                     .voC.
           izAotX7777777777777777777777777777777777777777Y7n92:
             .;CoIIIIIUAA666666699999ZZZZZZZZZZZZZZZZZZZZ6ov.

feld's avatar
feld committed
179
180
HTTP Security is disabled. Please re-enable it to prevent users from attacking
your instance and your users via malicious posts:
181
182
183
184
185
186

      config :pleroma, :http_security, enabled: true
      ")
    end
  end

kaniini's avatar
kaniini committed
187
  defp maybe_send_sts_header(conn, true) do
kaniini's avatar
kaniini committed
188
189
    max_age_sts = Config.get([:http_security, :sts_max_age])
    max_age_ct = Config.get([:http_security, :ct_max_age])
kaniini's avatar
kaniini committed
190
191

    merge_resp_headers(conn, [
192
193
      {"strict-transport-security", "max-age=#{max_age_sts}; includeSubDomains"},
      {"expect-ct", "enforce, max-age=#{max_age_ct}"}
kaniini's avatar
kaniini committed
194
195
196
197
    ])
  end

  defp maybe_send_sts_header(conn, _), do: conn
kaniini's avatar
kaniini committed
198
end