Merge pull request 'Merge stable 2.11.1 into develop' (#8017) from merge-stable-2.11.1 into develop

This commit is contained in:
lain 2026-10-26 19:36:51 +00:00
commit 70f9b8542f
9 changed files with 56 additions and 41 deletions

View file

@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 2.11.1
### Security
- Update Bandit to version 1.14.0, which fixes HTTP/1.1, HTTP/2 and WebSocket denial-of-service and request smuggling vulnerabilities
- Update Hackney to version 4.9.1, which fixes unbounded response body buffering and decompression, CA pinning, redirect credential leaks and other vulnerabilities
## 2.11.0
### Security

View file

@ -79,8 +79,14 @@ defmodule Pleroma.HTTP do
adapter,
adapter_opts
)
|> drop_partial_body()
end
# Hackney 4.9 returns the bytes read before the peer closed. No caller wants
# them, and callers that cache errors would keep them.
defp drop_partial_body({:error, {:closed, body}}) when is_binary(body), do: {:error, :closed}
defp drop_partial_body(result), do: result
@spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()}
def request(client, request), do: Tesla.request(client, request)

View file

@ -7,16 +7,14 @@ defmodule Pleroma.Tesla.Middleware.MaxBody do
Enforces the `:max_body` adapter option for every adapter.
`Pleroma.HTTP.Adapter.Gun` applies the limit as it accumulates a `:plain`
body, so an oversized response is abandoned mid-transfer. Hackney cannot:
since 4.0 it ignores `with_body` and always hands back a fully read binary
(`hackney:request/5`), so `Tesla.Adapter.Hackney` never takes the branch that
would honour `:max_body`. Without this middleware the option is silently
unenforced there.
body, so an oversized response is abandoned mid-transfer. Since 4.9 Hackney
does the same with the `max_body` request option. Before that it ignored
`with_body` and always handed back a fully read binary (`hackney:request/5`),
so `Tesla.Adapter.Hackney` never took the branch that would honour
`:max_body`.
The check therefore bounds what callers are handed, and refuses up front when
`content-length` already announces too much. It cannot bound peak memory on
the hackney path, because the adapter has read the body before any middleware
runs.
The check bounds what callers are handed, whatever the adapter does, and
refuses up front when `content-length` already announces too much.
Streaming bodies are left alone: neither adapter limits those, and the caller
that owns the stream is the one that has to drain and release it.

View file

@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
def project do
[
app: :pleroma,
version: version("2.11.0"),
version: version("2.11.1"),
elixir: "~> 1.17",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: Mix.compilers(),
@ -224,9 +224,7 @@ defmodule Pleroma.Mixfile do
{:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false},
{:mock, "~> 0.3.9", only: :test},
{:covertool, "~> 2.0", only: :test},
{:hackney, "~> 4.7.3", override: true},
# Hackney 4.7.3 pins certifi ~> 2.17.0; 2.18.0 drops the distrusted emSign roots.
{:certifi, "~> 2.18.0", override: true},
{:hackney, "~> 4.9.1", override: true},
{:mox, "~> 1.2", only: :test},
{:websockex, "~> 0.4.3", only: :test},
{:benchee, "~> 1.4", only: :benchmark},

View file

@ -1,7 +1,7 @@
%{
"accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"},
"argon2_elixir": {:hex, :argon2_elixir, "4.1.3", "4f28318286f89453364d7fbb53e03d4563fd7ed2438a60237eba5e426e97785f", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "7c295b8d8e0eaf6f43641698f962526cdf87c6feb7d14bd21e599271b510608c"},
"bandit": {:hex, :bandit, "1.13.1", "1ce25a7beb750ed3fa2e905962f1f9a679cb585f1da0ae45637d06e385ce2eae", [:mix], [{:hpax, "~> 1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.6", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "39028f0b5a21404661410a6c19275060a9822c9dcca70b7c3e4e4fa50b11da58"},
"bandit": {:hex, :bandit, "1.14.0", "555af34f126edb98d8b6b95bbeba4d5f4c3af21e37535d791f32db2365c532fb", [:mix], [{:hpax, "~> 1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.6", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "2d07630e4bf77b55240881386f7629bcc2f87426a299860b2d31c4ad77a48746"},
"base62": {:hex, :base62, "1.2.2", "85c6627eb609317b70f555294045895ffaaeb1758666ab9ef9ca38865b11e629", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "d41336bda8eaa5be197f1e4592400513ee60518e5b9f4dcf38f4b4dae6f377bb"},
"bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "19851074419a5fedb4ef49e1f01b30df504bb5dbb6d6adfc135238063bebd1c3"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"},
@ -58,8 +58,8 @@
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
"gun": {:hex, :gun, "2.7.0", "8dfc20ff1b70f3c78f07122f7d82ac6950c40d33a8a9b35da0134f49fb35b334", [:make, :rebar3], [{:cowlib, ">= 2.21.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "2a90d073069e72654b6c9a91fae365b746ff5ce385a4a5bbf71ef53a8bf95d82"},
"h2": {:hex, :h2, "0.11.0", "b8c9f7c86f400582c5cbed59f79c0efd6cf49ad6478c210bede91d2d15019b14", [:rebar3], [], "hexpm", "c61f24361d0dc76375eaf35c2bf0e01235d81b7685538df571db036467fac358"},
"hackney": {:hex, :hackney, "4.7.3", "42feec1a4986000411cb5d389bbc9fb61d9dff84c5eef42b11fc40ea327e6f9a", [:rebar3], [{:certifi, "~> 2.17.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:h2, "~> 0.11.0", [hex: :h2, repo: "hexpm", optional: false]}, {:idna, "~> 7.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.5", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.2", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:webtransport, "~> 0.4.4", [hex: :webtransport, repo: "hexpm", optional: false]}], "hexpm", "3f460e01d5a1abba39c32d06e9a733bb76e35d1ab87a83868b6bb29d646fe25f"},
"h2": {:hex, :h2, "0.12.5", "03d492b4cce77466c4c4a2b3fd74b2bb16c1d78f9618bbfeb50ea493c005accf", [:rebar3], [], "hexpm", "a4524bf80cafb86e599710c1d5c9f63aca0284607cb4d99fb53ae63fd55aa03f"},
"hackney": {:hex, :hackney, "4.9.1", "1c1c3d75ae0f9c35df40f56ca63ba2f8c9986d988d67ae8ebe26f96697e81485", [:rebar3], [{:certifi, "~> 2.18.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:h2, "~> 0.12.5", [hex: :h2, repo: "hexpm", optional: false]}, {:idna, "~> 7.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.5", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.2", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:quic, "~> 2.1.2", [hex: :quic, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:webtransport, "~> 0.4.8", [hex: :webtransport, repo: "hexpm", optional: false]}], "hexpm", "093e1830e523c2b1972d570dcee83d000d41f7b39e7eb3c6b5240cf0f64a1973"},
"hpax": {:hex, :hpax, "1.1.0", "782931867cc23217c68fb5f68fe1a11f5e7544c7fda82c8a7019a5df5a4a1cdf", [:mix], [], "hexpm", "0b8d0f05832f55571d65ac720f79bf8994138ffbb133209dc4685eae0ad456a8"},
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
"http_signatures": {:hex, :http_signatures, "0.1.3", "19f26aee35b4684e37efdce3ac4638605e6e41a04368186bd39d2b6138a60ea9", [:mix], [{:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "20313a65516db88006f85b090f6f76cc5b04e9609b45943657e6781eb91174f4"},
@ -130,7 +130,7 @@
"prometheus_phx": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/prometheus-phx.git", "9cd8f248c9381ffedc799905050abce194a97514", [branch: "no-logging"]},
"prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm", "0273a6483ccb936d79ca19b0ab629aef0dba958697c94782bb728b920dfc6a79"},
"quantile_estimator": {:hex, :quantile_estimator, "0.2.1", "ef50a361f11b5f26b5f16d0696e46a9e4661756492c981f7b2229ef42ff1cd15", [:rebar3], [], "hexpm", "282a8a323ca2a845c9e6f787d166348f776c1d4a41ede63046d72d422e3da946"},
"quic": {:hex, :quic, "1.8.0", "7b074fc6f8a13b4a81e4e11254bf061db61c5c07b4366d568a79a38c5e8bb5f3", [:rebar3], [], "hexpm", "6b1f8f08a45412ac503bc6771efb8edc696bb14079bc48d2ca960b92933da8a4"},
"quic": {:hex, :quic, "2.1.3", "c66861354d39b0f2b8ac0d7cde56d90db0168ac7b3ffa11d1cb8ab37237d1233", [:rebar3], [], "hexpm", "1520df50fc63093c0a4d209cb97056e132d06f0b8fd54c5cf933ddc0c0ea6fe2"},
"ranch": {:hex, :ranch, "2.2.1", "fc2bb0e800efbeab8ff82eb325450b9c08653b14c3f69e8b09bbea304973105d", [:make, :rebar3], [], "hexpm", "55f05cce20ec2da1d90de5d5981afb93dbfc01325fc7e933189aa5c62f037c24"},
"recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"},
"remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"},
@ -158,7 +158,7 @@
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
"websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"},
"webtransport": {:hex, :webtransport, "0.4.4", "714180b340fbd30a7d666f3f593a612186ff1ba444566b3e8c9ce37fb2950638", [:rebar3], [{:h2, "~> 0.11.0", [hex: :h2, repo: "hexpm", optional: false]}, {:quic, "~> 1.8.0", [hex: :quic, repo: "hexpm", optional: false]}], "hexpm", "80a6641e0f6628938ceee4c9d7a4f6d48a6089890da1ae3288ca8dab13a6005e"},
"webtransport": {:hex, :webtransport, "0.4.8", "3c340d7ea381e3f639ece90378d85fb46b1da4eece17bba3223051e833166501", [:rebar3], [{:h2, "~> 0.12", [hex: :h2, repo: "hexpm", optional: false]}, {:quic, "~> 2.0", [hex: :quic, repo: "hexpm", optional: false]}], "hexpm", "08477a2195ccbf944da0b78f85dc4e6ee4498790e3dd7fe84ee7a245938e1b26"},
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
"yaml_elixir": {:hex, :yaml_elixir, "2.12.2", "9dd1330fb4cd9a36a7b0f502e5b12486eff632792ee4a5f0eba52a4d4ec32c9c", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "e7c1b10122f973e6558462d51c39026ba0e14afbc6745318e990ea82cfe9e159"},
}

View file

@ -14,15 +14,15 @@
#
# Pleroma cannot work around this: since 4.0 Hackney ignores `with_body` and
# always returns a fully read binary, so a synchronous request never yields a
# handle to close, and a truncated read still reports `{:ok, body}`.
# handle to close. Up to 4.8.2 a truncated read also reported `{:ok, body}`;
# since 4.8.3 it is `{:error, {:closed, partial_body}}`.
#
# Media proxy warming hits this whenever an attachment fetch is cut short and
# the outbound proxy is used as a CONNECT tunnel, since tunnels are never
# pooled. That is how it was found.
#
# Fixed upstream by https://github.com/benoitc/hackney/pull/919; mix.exs pins
# that commit until it lands in a release. Should this start failing, the pin
# has been moved to a version without the fix.
# Fixed upstream by https://github.com/benoitc/hackney/pull/919. Should this
# start failing, mix.exs has moved to a Hackney version without the fix.
#
# https://github.com/benoitc/hackney/issues/918
defmodule Pleroma.HTTP.HackneyConnLeakTest do
@ -50,11 +50,9 @@ defmodule Pleroma.HTTP.HackneyConnLeakTest do
# pool: false takes hackney's connect_direct/4 path, the same unpooled shape
# a CONNECT tunnel gets. A pooled connection is reaped by the pool's own
# grace timer and would not show the leak.
assert {:ok, %Tesla.Env{status: 200, body: body}} =
request(server, pool: false)
assert {:error, {:closed, body}} = request(server, pool: false)
# The short read still reports success, which is why nothing downstream
# knows to clean up.
# The short read hands back what did arrive.
assert byte_size(body) == @sent_bytes
leaked = wait_for_cleanup(before)
@ -70,7 +68,7 @@ defmodule Pleroma.HTTP.HackneyConnLeakTest do
test "a pooled request with a truncated body does not leak either", %{server: server} do
before = conn_pids()
assert {:ok, %Tesla.Env{status: 200}} = request(server, pool: :media)
assert {:error, {:closed, _}} = request(server, pool: :media)
assert wait_for_cleanup(before) == []
end

View file

@ -4,8 +4,9 @@
# Hackney 4 ignores `with_body` and always returns a fully read binary, so
# `Tesla.Adapter.Hackney` never reaches the clause that honours `:max_body`.
# The limit is enforced by Pleroma.Tesla.Middleware.MaxBody instead; these
# exercise it against the real adapter.
# Hackney 4.9 enforces `max_body` itself while reading (CVE-2026-97200);
# Pleroma.Tesla.Middleware.MaxBody stays as the check for every adapter. These
# exercise both against the real adapter.
defmodule Pleroma.HTTP.HackneyMaxBodyRegressionTest do
use ExUnit.Case, async: false
@ -43,8 +44,8 @@ defmodule Pleroma.HTTP.HackneyMaxBodyRegressionTest do
request("#{server.base_url}/big", max_body: 1024)
end
# Without a Content-Length the header pre-check cannot fire, so this is the
# case that proves hackney's fully-read binary is measured.
# Without a Content-Length the header pre-check cannot fire, so only a size
# check on the body can refuse this.
test "a chunked body over max_body is refused on size alone", %{server: server} do
assert {:error, :body_too_large} =
request("#{server.base_url}/chunked", max_body: 1024)
@ -57,17 +58,16 @@ defmodule Pleroma.HTTP.HackneyMaxBodyRegressionTest do
assert byte_size(body) == @body_bytes
end
test "without max_body the body is returned whatever its size", %{server: server} do
test "without max_body the body is returned", %{server: server} do
assert {:ok, %Tesla.Env{status: 200, body: body}} = request("#{server.base_url}/big", [])
assert byte_size(body) == @body_bytes
end
# Pins why the middleware is needed. Tesla.Adapter.Hackney only honours
# :max_body on the branch where hackney hands back a ref, which 4.x never
# does for a synchronous request. Should this ever start failing, hackney or
# Tesla have regained streaming and the middleware can go.
test "the bare adapter does not enforce max_body", %{server: server} do
url = "#{server.base_url}/big"
# Hackney 4.0 to 4.8 read the whole body whatever `max_body` said; 4.9
# stops reading once the body goes over it (CVE-2026-97200). Should this
# start failing, the hackney pin has gone back to a version without the fix.
test "the bare adapter enforces max_body", %{server: server} do
url = "#{server.base_url}/chunked"
adapter_opts =
HackneyAdapterHelper.options(
@ -77,10 +77,8 @@ defmodule Pleroma.HTTP.HackneyMaxBodyRegressionTest do
client = Tesla.client([], Tesla.Adapter.Hackney)
assert {:ok, %Tesla.Env{status: 200, body: body}} =
assert {:error, :body_too_large} =
Tesla.request(client, method: :get, url: url, opts: [adapter: adapter_opts])
assert byte_size(body) == @body_bytes
end
defp start_server do

View file

@ -29,6 +29,10 @@ defmodule Pleroma.HTTPTest do
%{method: :post, url: "http://example.com/world"} ->
%Tesla.Env{status: 200, body: "world"}
# Hackney 4.9 hands back the bytes it read before the peer closed.
%{method: :get, url: "http://example.com/truncated"} ->
{:error, {:closed, "partial body"}}
%{method: :get, url: "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz"} ->
%Tesla.Env{status: 200, body: "emoji data"}
@ -81,6 +85,12 @@ defmodule Pleroma.HTTPTest do
end
end
describe "get/1 with a truncated body" do
test "drops the partial body from the error" do
assert HTTP.get("http://example.com/truncated") == {:error, :closed}
end
end
describe "get/2 (with headers)" do
test "returns successfully result for json content-type" do
assert HTTP.get("http://example.com/hello", [{"content-type", "application/json"}]) ==