logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://anongit.hacktivis.me/git/pleroma.git/

http_security_plug.ex (8500B)


  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do
  5. import Plug.Conn
  6. require Logger
  7. @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
  8. def init(opts), do: opts
  9. def call(conn, _options) do
  10. if @config_impl.get([:http_security, :enabled]) do
  11. conn
  12. |> merge_resp_headers(headers())
  13. |> maybe_send_sts_header(@config_impl.get([:http_security, :sts]))
  14. else
  15. conn
  16. end
  17. end
  18. def primary_frontend do
  19. with %{"name" => frontend} <- @config_impl.get([:frontends, :primary]),
  20. available <- @config_impl.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_impl.get([:http_security, :referrer_policy])
  34. report_uri = @config_impl.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. [{"report-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_impl.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_impl.get([:http_security, :report_uri])
  79. img_src = "img-src 'self' data: blob:"
  80. media_src = "media-src 'self'"
  81. connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
  82. # Strict multimedia CSP enforcement only when MediaProxy is enabled
  83. {img_src, media_src, connect_src} =
  84. if @config_impl.get([:media_proxy, :enabled]) &&
  85. !@config_impl.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do
  86. sources = build_csp_multimedia_source_list()
  87. {
  88. [img_src, sources],
  89. [media_src, sources],
  90. [connect_src, sources]
  91. }
  92. else
  93. {
  94. [img_src, " https:"],
  95. [media_src, " https:"],
  96. [connect_src, " https:"]
  97. }
  98. end
  99. connect_src =
  100. if @config_impl.get([:env]) == :dev do
  101. [connect_src, " http://localhost:3035/"]
  102. else
  103. connect_src
  104. end
  105. script_src =
  106. if @config_impl.get([:http_security, :allow_unsafe_eval]) do
  107. if @config_impl.get([:env]) == :dev do
  108. "script-src 'self' 'unsafe-eval'"
  109. else
  110. "script-src 'self' 'wasm-unsafe-eval'"
  111. end
  112. else
  113. "script-src 'self'"
  114. end
  115. report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"]
  116. insecure = if scheme == "https", do: "upgrade-insecure-requests"
  117. @csp_start
  118. |> add_csp_param(img_src)
  119. |> add_csp_param(media_src)
  120. |> add_csp_param(connect_src)
  121. |> add_csp_param(script_src)
  122. |> add_csp_param(insecure)
  123. |> add_csp_param(report)
  124. |> :erlang.iolist_to_binary()
  125. end
  126. defp build_csp_from_whitelist([], acc), do: acc
  127. defp build_csp_from_whitelist([last], acc) do
  128. [build_csp_param_from_whitelist(last) | acc]
  129. end
  130. defp build_csp_from_whitelist([head | tail], acc) do
  131. build_csp_from_whitelist(tail, [[?\s, build_csp_param_from_whitelist(head)] | acc])
  132. end
  133. # TODO: use `build_csp_param/1` after removing support bare domains for media proxy whitelist
  134. defp build_csp_param_from_whitelist("http" <> _ = url) do
  135. build_csp_param(url)
  136. end
  137. defp build_csp_param_from_whitelist(url), do: url
  138. defp build_csp_multimedia_source_list do
  139. media_proxy_whitelist =
  140. [:media_proxy, :whitelist]
  141. |> @config_impl.get()
  142. |> build_csp_from_whitelist([])
  143. captcha_method = @config_impl.get([Pleroma.Captcha, :method])
  144. captcha_endpoint = @config_impl.get([captcha_method, :endpoint])
  145. base_endpoints =
  146. [
  147. [:media_proxy, :base_url],
  148. [Pleroma.Upload, :base_url],
  149. [Pleroma.Uploaders.S3, :public_endpoint]
  150. ]
  151. |> Enum.map(&@config_impl.get/1)
  152. [captcha_endpoint | base_endpoints]
  153. |> Enum.map(&build_csp_param/1)
  154. |> Enum.reduce([], &add_source(&2, &1))
  155. |> add_source(media_proxy_whitelist)
  156. end
  157. defp add_source(iodata, nil), do: iodata
  158. defp add_source(iodata, []), do: iodata
  159. defp add_source(iodata, source), do: [[?\s, source] | iodata]
  160. defp add_csp_param(csp_iodata, nil), do: csp_iodata
  161. defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata]
  162. defp build_csp_param(nil), do: nil
  163. defp build_csp_param(url) when is_binary(url) do
  164. %{host: host, scheme: scheme} = URI.parse(url)
  165. if scheme do
  166. [scheme, "://", host]
  167. end
  168. end
  169. def warn_if_disabled do
  170. unless Pleroma.Config.get([:http_security, :enabled]) do
  171. Logger.warning("
  172. .i;;;;i.
  173. iYcviii;vXY:
  174. .YXi .i1c.
  175. .YC. . in7.
  176. .vc. ...... ;1c.
  177. i7, .. .;1;
  178. i7, .. ... .Y1i
  179. ,7v .6MMM@; .YX,
  180. .7;. ..IMMMMMM1 :t7.
  181. .;Y. ;$MMMMMM9. :tc.
  182. vY. .. .nMMM@MMU. ;1v.
  183. i7i ... .#MM@M@C. .....:71i
  184. it: .... $MMM@9;.,i;;;i,;tti
  185. :t7. ..... 0MMMWv.,iii:::,,;St.
  186. .nC. ..... IMMMQ..,::::::,.,czX.
  187. .ct: ....... .ZMMMI..,:::::::,,:76Y.
  188. c2: ......,i..Y$M@t..:::::::,,..inZY
  189. vov ......:ii..c$MBc..,,,,,,,,,,..iI9i
  190. i9Y ......iii:..7@MA,..,,,,,,,,,....;AA:
  191. iIS. ......:ii::..;@MI....,............;Ez.
  192. .I9. ......:i::::...8M1..................C0z.
  193. .z9; ......:i::::,.. .i:...................zWX.
  194. vbv ......,i::::,,. ................. :AQY
  195. c6Y. .,...,::::,,..:t0@@QY. ................ :8bi
  196. :6S. ..,,...,:::,,,..EMMMMMMI. ............... .;bZ,
  197. :6o, .,,,,..:::,,,..i#MMMMMM#v................. YW2.
  198. .n8i ..,,,,,,,::,,,,.. tMMMMM@C:.................. .1Wn
  199. 7Uc. .:::,,,,,::,,,,.. i1t;,..................... .UEi
  200. 7C...::::::::::::,,,,.. .................... vSi.
  201. ;1;...,,::::::,......... .................. Yz:
  202. v97,......... .voC.
  203. izAotX7777777777777777777777777777777777777777Y7n92:
  204. .;CoIIIIIUAA666666699999ZZZZZZZZZZZZZZZZZZZZ6ov.
  205. HTTP Security is disabled. Please re-enable it to prevent users from attacking
  206. your instance and your users via malicious posts:
  207. config :pleroma, :http_security, enabled: true
  208. ")
  209. end
  210. end
  211. defp maybe_send_sts_header(conn, true) do
  212. max_age_sts = @config_impl.get([:http_security, :sts_max_age])
  213. max_age_ct = @config_impl.get([:http_security, :ct_max_age])
  214. merge_resp_headers(conn, [
  215. {"strict-transport-security", "max-age=#{max_age_sts}; includeSubDomains"},
  216. {"expect-ct", "enforce, max-age=#{max_age_ct}"}
  217. ])
  218. end
  219. defp maybe_send_sts_header(conn, _), do: conn
  220. end