logo

pleroma

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

http_security_plug.ex (8183B)


  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. 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. [{"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.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. 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.get([:media_proxy, :enabled]) &&
  85. !Config.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.get(:env) == :dev do
  101. [connect_src, " http://localhost:3035/"]
  102. else
  103. connect_src
  104. end
  105. script_src =
  106. if Config.get(:env) == :dev do
  107. "script-src 'self' 'unsafe-eval'"
  108. else
  109. "script-src 'self' 'wasm-unsafe-eval'"
  110. end
  111. report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"]
  112. insecure = if scheme == "https", do: "upgrade-insecure-requests"
  113. @csp_start
  114. |> add_csp_param(img_src)
  115. |> add_csp_param(media_src)
  116. |> add_csp_param(connect_src)
  117. |> add_csp_param(script_src)
  118. |> add_csp_param(insecure)
  119. |> add_csp_param(report)
  120. |> :erlang.iolist_to_binary()
  121. end
  122. defp build_csp_from_whitelist([], acc), do: acc
  123. defp build_csp_from_whitelist([last], acc) do
  124. [build_csp_param_from_whitelist(last) | acc]
  125. end
  126. defp build_csp_from_whitelist([head | tail], acc) do
  127. build_csp_from_whitelist(tail, [[?\s, build_csp_param_from_whitelist(head)] | acc])
  128. end
  129. # TODO: use `build_csp_param/1` after removing support bare domains for media proxy whitelist
  130. defp build_csp_param_from_whitelist("http" <> _ = url) do
  131. build_csp_param(url)
  132. end
  133. defp build_csp_param_from_whitelist(url), do: url
  134. defp build_csp_multimedia_source_list do
  135. media_proxy_whitelist =
  136. [:media_proxy, :whitelist]
  137. |> Config.get()
  138. |> build_csp_from_whitelist([])
  139. captcha_method = Config.get([Pleroma.Captcha, :method])
  140. captcha_endpoint = Config.get([captcha_method, :endpoint])
  141. base_endpoints =
  142. [
  143. [:media_proxy, :base_url],
  144. [Pleroma.Upload, :base_url],
  145. [Pleroma.Uploaders.S3, :public_endpoint]
  146. ]
  147. |> Enum.map(&Config.get/1)
  148. [captcha_endpoint | base_endpoints]
  149. |> Enum.map(&build_csp_param/1)
  150. |> Enum.reduce([], &add_source(&2, &1))
  151. |> add_source(media_proxy_whitelist)
  152. end
  153. defp add_source(iodata, nil), do: iodata
  154. defp add_source(iodata, []), do: iodata
  155. defp add_source(iodata, source), do: [[?\s, source] | iodata]
  156. defp add_csp_param(csp_iodata, nil), do: csp_iodata
  157. defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata]
  158. defp build_csp_param(nil), do: nil
  159. defp build_csp_param(url) when is_binary(url) do
  160. %{host: host, scheme: scheme} = URI.parse(url)
  161. if scheme do
  162. [scheme, "://", host]
  163. end
  164. end
  165. def warn_if_disabled do
  166. unless Config.get([:http_security, :enabled]) do
  167. Logger.warning("
  168. .i;;;;i.
  169. iYcviii;vXY:
  170. .YXi .i1c.
  171. .YC. . in7.
  172. .vc. ...... ;1c.
  173. i7, .. .;1;
  174. i7, .. ... .Y1i
  175. ,7v .6MMM@; .YX,
  176. .7;. ..IMMMMMM1 :t7.
  177. .;Y. ;$MMMMMM9. :tc.
  178. vY. .. .nMMM@MMU. ;1v.
  179. i7i ... .#MM@M@C. .....:71i
  180. it: .... $MMM@9;.,i;;;i,;tti
  181. :t7. ..... 0MMMWv.,iii:::,,;St.
  182. .nC. ..... IMMMQ..,::::::,.,czX.
  183. .ct: ....... .ZMMMI..,:::::::,,:76Y.
  184. c2: ......,i..Y$M@t..:::::::,,..inZY
  185. vov ......:ii..c$MBc..,,,,,,,,,,..iI9i
  186. i9Y ......iii:..7@MA,..,,,,,,,,,....;AA:
  187. iIS. ......:ii::..;@MI....,............;Ez.
  188. .I9. ......:i::::...8M1..................C0z.
  189. .z9; ......:i::::,.. .i:...................zWX.
  190. vbv ......,i::::,,. ................. :AQY
  191. c6Y. .,...,::::,,..:t0@@QY. ................ :8bi
  192. :6S. ..,,...,:::,,,..EMMMMMMI. ............... .;bZ,
  193. :6o, .,,,,..:::,,,..i#MMMMMM#v................. YW2.
  194. .n8i ..,,,,,,,::,,,,.. tMMMMM@C:.................. .1Wn
  195. 7Uc. .:::,,,,,::,,,,.. i1t;,..................... .UEi
  196. 7C...::::::::::::,,,,.. .................... vSi.
  197. ;1;...,,::::::,......... .................. Yz:
  198. v97,......... .voC.
  199. izAotX7777777777777777777777777777777777777777Y7n92:
  200. .;CoIIIIIUAA666666699999ZZZZZZZZZZZZZZZZZZZZ6ov.
  201. HTTP Security is disabled. Please re-enable it to prevent users from attacking
  202. your instance and your users via malicious posts:
  203. config :pleroma, :http_security, enabled: true
  204. ")
  205. end
  206. end
  207. defp maybe_send_sts_header(conn, true) do
  208. max_age_sts = Config.get([:http_security, :sts_max_age])
  209. max_age_ct = Config.get([:http_security, :ct_max_age])
  210. merge_resp_headers(conn, [
  211. {"strict-transport-security", "max-age=#{max_age_sts}; includeSubDomains"},
  212. {"expect-ct", "enforce, max-age=#{max_age_ct}"}
  213. ])
  214. end
  215. defp maybe_send_sts_header(conn, _), do: conn
  216. end