From d8c880e4946afa2710ae7e897082db982245b095 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 21 Oct 2026 08:21:43 +0400 Subject: [PATCH] fix(http): make Hackney trust the CAStore bundle Hackney trusts its own certifi bundle, which still has the roots of the compromised eMudhra intermediate CAs. Pass explicit ssl_options with the CAStore bundle. Hackney drops all its TLS defaults when ssl_options are given, so keep peer and hostname checks in the options. --- changelog.d/hackney-castore.security | 1 + lib/pleroma/http/adapter_helper/hackney.ex | 1 + lib/pleroma/http/tls.ex | 35 ++++++++++++++ lib/pleroma/reverse_proxy/client/hackney.ex | 6 +-- .../http/adapter_helper/hackney_test.exs | 15 ++++++ test/pleroma/http/tls_test.exs | 47 +++++++++++++++++++ 6 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 changelog.d/hackney-castore.security create mode 100644 lib/pleroma/http/tls.ex create mode 100644 test/pleroma/http/tls_test.exs diff --git a/changelog.d/hackney-castore.security b/changelog.d/hackney-castore.security new file mode 100644 index 000000000..d30a53f80 --- /dev/null +++ b/changelog.d/hackney-castore.security @@ -0,0 +1 @@ +Hackney now trusts only the CAStore certificate bundle instead of its outdated certifi bundle, which still trusted the compromised eMudhra intermediate CAs diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex index 2b1502001..fe4df1e1b 100644 --- a/lib/pleroma/http/adapter_helper/hackney.ex +++ b/lib/pleroma/http/adapter_helper/hackney.ex @@ -28,6 +28,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do |> add_scheme_opts(uri) |> maybe_add_with_body() |> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy) + |> Pleroma.HTTP.TLS.put_hackney_ssl_options() end defp add_scheme_opts(opts, _), do: opts diff --git a/lib/pleroma/http/tls.ex b/lib/pleroma/http/tls.ex new file mode 100644 index 000000000..22718835b --- /dev/null +++ b/lib/pleroma/http/tls.ex @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.TLS do + @moduledoc """ + TLS client options that trust the CAStore bundle. + + Hackney otherwise trusts its own certifi bundle, and it drops all of its + default TLS options, including certificate checks, as soon as any + `ssl_options` are given. + """ + + @spec ssl_options(keyword()) :: keyword() + def ssl_options(overrides \\ []) do + Keyword.merge( + [ + verify: :verify_peer, + cacertfile: CAStore.file_path(), + depth: 20, + customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)] + ], + overrides + ) + end + + @spec put_hackney_ssl_options(keyword()) :: keyword() + def put_hackney_ssl_options(opts) do + if opts[:insecure] do + opts + else + Keyword.put(opts, :ssl_options, ssl_options(Keyword.get(opts, :ssl_options, []))) + end + end +end diff --git a/lib/pleroma/reverse_proxy/client/hackney.ex b/lib/pleroma/reverse_proxy/client/hackney.ex index 4c81a6225..9026b4586 100644 --- a/lib/pleroma/reverse_proxy/client/hackney.ex +++ b/lib/pleroma/reverse_proxy/client/hackney.ex @@ -28,9 +28,9 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do @impl true def request(method, url, headers, body, opts \\ []) do opts = - Keyword.put_new(opts, :path_encode_fun, fn path -> - path - end) + opts + |> Keyword.put_new(:path_encode_fun, fn path -> path end) + |> Pleroma.HTTP.TLS.put_hackney_ssl_options() if opts[:follow_redirect] != false do {_state, req_opts} = Access.get_and_update(opts, :follow_redirect, fn a -> {a, false} end) diff --git a/test/pleroma/http/adapter_helper/hackney_test.exs b/test/pleroma/http/adapter_helper/hackney_test.exs index 343bdb800..5bf13e25c 100644 --- a/test/pleroma/http/adapter_helper/hackney_test.exs +++ b/test/pleroma/http/adapter_helper/hackney_test.exs @@ -39,5 +39,20 @@ defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do assert opts[:b] == 1 refute Keyword.has_key?(opts, :proxy) end + + test "trusts the CAStore bundle", %{uri: uri} do + opts = Hackney.options([], uri) + + assert opts[:ssl_options] == Pleroma.HTTP.TLS.ssl_options() + end + + test "keeps the CAStore bundle with configured ssl_options", %{uri: uri} do + clear_config([:http, :adapter], ssl_options: [versions: [:"tlsv1.2"]]) + + opts = Hackney.options([], uri) + + assert opts[:ssl_options][:versions] == [:"tlsv1.2"] + assert opts[:ssl_options][:cacertfile] == CAStore.file_path() + end end end diff --git a/test/pleroma/http/tls_test.exs b/test/pleroma/http/tls_test.exs new file mode 100644 index 000000000..7e9a85edd --- /dev/null +++ b/test/pleroma/http/tls_test.exs @@ -0,0 +1,47 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.TLSTest do + use ExUnit.Case, async: true + + alias Pleroma.HTTP.TLS + + describe "ssl_options/1" do + test "verifies peers against the CAStore bundle" do + opts = TLS.ssl_options() + + assert opts[:verify] == :verify_peer + assert opts[:cacertfile] == CAStore.file_path() + assert [match_fun: match_fun] = opts[:customize_hostname_check] + assert is_function(match_fun, 2) + end + + test "keeps the CA bundle when other options are given" do + opts = TLS.ssl_options(versions: [:"tlsv1.2"]) + + assert opts[:versions] == [:"tlsv1.2"] + assert opts[:cacertfile] == CAStore.file_path() + assert opts[:verify] == :verify_peer + end + end + + describe "put_hackney_ssl_options/1" do + test "adds the TLS options" do + opts = TLS.put_hackney_ssl_options(pool: :media) + + assert opts[:pool] == :media + assert opts[:ssl_options] == TLS.ssl_options() + end + + test "merges given ssl_options into the TLS options" do + opts = TLS.put_hackney_ssl_options(ssl_options: [versions: [:"tlsv1.2"]]) + + assert opts[:ssl_options] == TLS.ssl_options(versions: [:"tlsv1.2"]) + end + + test "leaves insecure requests alone" do + assert TLS.put_hackney_ssl_options(insecure: true) == [insecure: true] + end + end +end