Fork of Pleroma with site-specific changes and feature branches https://git.pleroma.social/pleroma/pleroma
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

251 line
7.8KB

  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do
  5. alias Pleroma.Config
  6. import Plug.Conn
  7. require Logger
  8. def init(opts), do: opts
  9. def call(conn, _options) do
  10. if Config.get([:http_security, :enabled]) do
  11. conn
  12. |> merge_resp_headers(headers())
  13. |> maybe_send_sts_header(Config.get([:http_security, :sts]))
  14. else
  15. conn
  16. end
  17. end
  18. def primary_frontend do
  19. with %{"name" => frontend} <- Config.get([:frontends, :primary]),
  20. available <- Config.get([:frontends, :available]),
  21. %{} = primary_frontend <- Map.get(available, frontend) do
  22. {:ok, primary_frontend}
  23. end
  24. end
  25. def custom_http_frontend_headers do
  26. with {:ok, %{"custom-http-headers" => custom_headers}} <- primary_frontend() do
  27. custom_headers
  28. else
  29. _ -> []
  30. end
  31. end
  32. def headers do
  33. referrer_policy = Config.get([:http_security, :referrer_policy])
  34. report_uri = Config.get([:http_security, :report_uri])
  35. custom_http_frontend_headers = custom_http_frontend_headers()
  36. headers = [
  37. {"x-xss-protection", "1; mode=block"},
  38. {"x-permitted-cross-domain-policies", "none"},
  39. {"x-frame-options", "DENY"},
  40. {"x-content-type-options", "nosniff"},
  41. {"referrer-policy", referrer_policy},
  42. {"x-download-options", "noopen"},
  43. {"content-security-policy", csp_string()},
  44. {"permissions-policy", "interest-cohort=()"}
  45. ]
  46. headers =
  47. if custom_http_frontend_headers do
  48. custom_http_frontend_headers ++ headers
  49. else
  50. headers
  51. end
  52. if report_uri do
  53. report_group = %{
  54. "group" => "csp-endpoint",
  55. "max-age" => 10_886_400,
  56. "endpoints" => [
  57. %{"url" => report_uri}
  58. ]
  59. }
  60. [{"reply-to", Jason.encode!(report_group)} | headers]
  61. else
  62. headers
  63. end
  64. end
  65. static_csp_rules = [
  66. "default-src 'none'",
  67. "base-uri 'self'",
  68. "frame-ancestors 'none'",
  69. "style-src 'self' 'unsafe-inline'",
  70. "font-src 'self'",
  71. "manifest-src 'self'"
  72. ]
  73. @csp_start [Enum.join(static_csp_rules, ";") <> ";"]
  74. defp csp_string do
  75. scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
  76. static_url = Pleroma.Web.Endpoint.static_url()
  77. websocket_url = Pleroma.Web.Endpoint.websocket_url()
  78. report_uri = Config.get([:http_security, :report_uri])
  79. img_src = "img-src 'self' data: blob:"
  80. media_src = "media-src 'self'"
  81. # Strict multimedia CSP enforcement only when MediaProxy is enabled
  82. {img_src, media_src} =
  83. if Config.get([:media_proxy, :enabled]) &&
  84. !Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do
  85. sources = build_csp_multimedia_source_list()
  86. {[img_src, sources], [media_src, sources]}
  87. else
  88. {[img_src, " https:"], [media_src, " https:"]}
  89. end
  90. connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
  91. connect_src =
  92. if Config.get(:env) == :dev do
  93. [connect_src, " http://localhost:3035/"]
  94. else
  95. connect_src
  96. end
  97. script_src =
  98. if Config.get(:env) == :dev do
  99. "script-src 'self' 'unsafe-eval'"
  100. else
  101. "script-src 'self'"
  102. end
  103. report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"]
  104. insecure = if scheme == "https", do: "upgrade-insecure-requests"
  105. @csp_start
  106. |> add_csp_param(img_src)
  107. |> add_csp_param(media_src)
  108. |> add_csp_param(connect_src)
  109. |> add_csp_param(script_src)
  110. |> add_csp_param(insecure)
  111. |> add_csp_param(report)
  112. |> :erlang.iolist_to_binary()
  113. end
  114. defp build_csp_from_whitelist([], acc), do: acc
  115. defp build_csp_from_whitelist([last], acc) do
  116. [build_csp_param_from_whitelist(last) | acc]
  117. end
  118. defp build_csp_from_whitelist([head | tail], acc) do
  119. build_csp_from_whitelist(tail, [[?\s, build_csp_param_from_whitelist(head)] | acc])
  120. end
  121. # TODO: use `build_csp_param/1` after removing support bare domains for media proxy whitelist
  122. defp build_csp_param_from_whitelist("http" <> _ = url) do
  123. build_csp_param(url)
  124. end
  125. defp build_csp_param_from_whitelist(url), do: url
  126. defp build_csp_multimedia_source_list do
  127. media_proxy_whitelist =
  128. [:media_proxy, :whitelist]
  129. |> Config.get()
  130. |> build_csp_from_whitelist([])
  131. captcha_method = Config.get([Pleroma.Captcha, :method])
  132. captcha_endpoint = Config.get([captcha_method, :endpoint])
  133. base_endpoints =
  134. [
  135. [:media_proxy, :base_url],
  136. [Pleroma.Upload, :base_url],
  137. [Pleroma.Uploaders.S3, :public_endpoint]
  138. ]
  139. |> Enum.map(&Config.get/1)
  140. [captcha_endpoint | base_endpoints]
  141. |> Enum.map(&build_csp_param/1)
  142. |> Enum.reduce([], &add_source(&2, &1))
  143. |> add_source(media_proxy_whitelist)
  144. end
  145. defp add_source(iodata, nil), do: iodata
  146. defp add_source(iodata, []), do: iodata
  147. defp add_source(iodata, source), do: [[?\s, source] | iodata]
  148. defp add_csp_param(csp_iodata, nil), do: csp_iodata
  149. defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata]
  150. defp build_csp_param(nil), do: nil
  151. defp build_csp_param(url) when is_binary(url) do
  152. %{host: host, scheme: scheme} = URI.parse(url)
  153. if scheme do
  154. [scheme, "://", host]
  155. end
  156. end
  157. def warn_if_disabled do
  158. unless Config.get([:http_security, :enabled]) do
  159. Logger.warn("
  160. .i;;;;i.
  161. iYcviii;vXY:
  162. .YXi .i1c.
  163. .YC. . in7.
  164. .vc. ...... ;1c.
  165. i7, .. .;1;
  166. i7, .. ... .Y1i
  167. ,7v .6MMM@; .YX,
  168. .7;. ..IMMMMMM1 :t7.
  169. .;Y. ;$MMMMMM9. :tc.
  170. vY. .. .nMMM@MMU. ;1v.
  171. i7i ... .#MM@M@C. .....:71i
  172. it: .... $MMM@9;.,i;;;i,;tti
  173. :t7. ..... 0MMMWv.,iii:::,,;St.
  174. .nC. ..... IMMMQ..,::::::,.,czX.
  175. .ct: ....... .ZMMMI..,:::::::,,:76Y.
  176. c2: ......,i..Y$M@t..:::::::,,..inZY
  177. vov ......:ii..c$MBc..,,,,,,,,,,..iI9i
  178. i9Y ......iii:..7@MA,..,,,,,,,,,....;AA:
  179. iIS. ......:ii::..;@MI....,............;Ez.
  180. .I9. ......:i::::...8M1..................C0z.
  181. .z9; ......:i::::,.. .i:...................zWX.
  182. vbv ......,i::::,,. ................. :AQY
  183. c6Y. .,...,::::,,..:t0@@QY. ................ :8bi
  184. :6S. ..,,...,:::,,,..EMMMMMMI. ............... .;bZ,
  185. :6o, .,,,,..:::,,,..i#MMMMMM#v................. YW2.
  186. .n8i ..,,,,,,,::,,,,.. tMMMMM@C:.................. .1Wn
  187. 7Uc. .:::,,,,,::,,,,.. i1t;,..................... .UEi
  188. 7C...::::::::::::,,,,.. .................... vSi.
  189. ;1;...,,::::::,......... .................. Yz:
  190. v97,......... .voC.
  191. izAotX7777777777777777777777777777777777777777Y7n92:
  192. .;CoIIIIIUAA666666699999ZZZZZZZZZZZZZZZZZZZZ6ov.
  193. HTTP Security is disabled. Please re-enable it to prevent users from attacking
  194. your instance and your users via malicious posts:
  195. config :pleroma, :http_security, enabled: true
  196. ")
  197. end
  198. end
  199. defp maybe_send_sts_header(conn, true) do
  200. max_age_sts = Config.get([:http_security, :sts_max_age])
  201. max_age_ct = Config.get([:http_security, :ct_max_age])
  202. merge_resp_headers(conn, [
  203. {"strict-transport-security", "max-age=#{max_age_sts}; includeSubDomains"},
  204. {"expect-ct", "enforce, max-age=#{max_age_ct}"}
  205. ])
  206. end
  207. defp maybe_send_sts_header(conn, _), do: conn
  208. end