Compare commits
9 commits
1804337593
...
e4ed44ba29
| Author | SHA1 | Date | |
|---|---|---|---|
| e4ed44ba29 | |||
| d6df0dd7d9 | |||
| 26cb389528 | |||
| c50afa78d2 | |||
| c397d29930 | |||
| a5f4cabf14 | |||
| c74b0ac96e | |||
| fb37688dd9 | |||
| a92e97277b |
14 changed files with 238 additions and 5 deletions
|
|
@ -4,7 +4,7 @@ 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/).
|
||||
|
||||
## Unreleased
|
||||
## 2026.05
|
||||
|
||||
### General note
|
||||
- backup restore instructions very slightly changed but in an important way.
|
||||
|
|
@ -38,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- non-federating instances now return a 405 response on inbox `POST`s, matching AP spec
|
||||
- fixed `GET /api/v1/statuses/:id/context` omitting most local-only posts for authenticated users
|
||||
- fixed nondeterministic API results in endpoints using GIN indexes; e.g. full-text search
|
||||
- enforced the host header being present on signatures, and matching our server
|
||||
|
||||
### Changed
|
||||
- our Docker container now sets a default `nofile` `ulimit` to avoid issues on some systems.
|
||||
|
|
|
|||
60
lib/pleroma/web/plugs/ensure_host_plug.ex
Normal file
60
lib/pleroma/web/plugs/ensure_host_plug.ex
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# Akkoma: Magically expressive social media
|
||||
# Copyright © 2022-2026 Akkoma Authors <https://akkoma.dev/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.EnsureHostPlug do
|
||||
@moduledoc """
|
||||
Ensures the request has a Host header, and that it matches this server
|
||||
"""
|
||||
import Plug.Conn
|
||||
|
||||
alias Pleroma.Web.Endpoint
|
||||
import Phoenix.Controller, only: [text: 2]
|
||||
|
||||
def init(options) do
|
||||
options
|
||||
end
|
||||
|
||||
def call(conn, _) do
|
||||
case get_req_header(conn, "host") do
|
||||
[host] ->
|
||||
handle_host_header(host, conn)
|
||||
|
||||
[_host | _more] ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> text("Bad host header")
|
||||
|> halt()
|
||||
|
||||
[] ->
|
||||
handle_host_header(nil, conn)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_host_header(value, conn) when is_binary(value) do
|
||||
our_uri = Endpoint.struct_url()
|
||||
default_port = URI.default_port(our_uri.scheme)
|
||||
expected_host = "#{our_uri.host}:#{our_uri.port}"
|
||||
|
||||
if case_insensitive_matches?(value, expected_host) ||
|
||||
case_insensitive_matches?("#{value}:#{default_port}", expected_host) do
|
||||
assign(conn, :host_matches, true)
|
||||
else
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> text("Host header does not match")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_host_header(_, conn) do
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> text("Host header not present")
|
||||
|> halt()
|
||||
end
|
||||
|
||||
defp case_insensitive_matches?(a, b) do
|
||||
String.downcase(a) == String.downcase(b)
|
||||
end
|
||||
end
|
||||
|
|
@ -20,7 +20,8 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
|
|||
conn
|
||||
end
|
||||
|
||||
def call(conn, _opts) do
|
||||
# ensure host check is run before this without mixing the two up here
|
||||
def call(%{assigns: %{host_matches: true}} = conn, _opts) do
|
||||
if get_format(conn) in ["json", "activity+json"] do
|
||||
conn
|
||||
|> maybe_assign_valid_signature()
|
||||
|
|
@ -29,6 +30,8 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
|
|||
end
|
||||
end
|
||||
|
||||
def call(conn, _opts), do: conn
|
||||
|
||||
def route_aliases(%{path_info: ["objects", id], query_string: query_string, method: method}) do
|
||||
ap_id = url(~p[/objects/#{id}])
|
||||
method = String.downcase(method)
|
||||
|
|
|
|||
|
|
@ -146,11 +146,13 @@ defmodule Pleroma.Web.Router do
|
|||
end
|
||||
|
||||
pipeline :optional_http_signature do
|
||||
plug(Pleroma.Web.Plugs.EnsureHostPlug)
|
||||
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
|
||||
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
|
||||
end
|
||||
|
||||
pipeline :http_signature do
|
||||
plug(Pleroma.Web.Plugs.EnsureHostPlug)
|
||||
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
|
||||
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
|
||||
plug(Pleroma.Web.Plugs.EnsureHTTPSignaturePlug)
|
||||
|
|
|
|||
2
mix.exs
2
mix.exs
|
|
@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
|
|||
def project do
|
||||
[
|
||||
app: :pleroma,
|
||||
version: version("3.18.1"),
|
||||
version: version("3.19.0"),
|
||||
elixir: "~> 1.15",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
compilers: Mix.compilers(),
|
||||
|
|
|
|||
2
mix.lock
2
mix.lock
|
|
@ -59,7 +59,7 @@
|
|||
"hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"},
|
||||
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
|
||||
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
|
||||
"http_signatures": {:git, "https://akkoma.dev/AkkomaGang/http_signatures.git", "fa3891074c33b5cb87964450f1226357f44549ae", [branch: "main"]},
|
||||
"http_signatures": {:git, "https://akkoma.dev/AkkomaGang/http_signatures.git", "a4791e3e8fdf25ed2d7e519548c2a5ade3f9302c", [branch: "main"]},
|
||||
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"igniter": {:hex, :igniter, "0.5.52", "18777a36918e3bb91c70f07b69f6a6589d8fa5547a7d210b228d410a2453923f", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "8d75f0f2307e21b53ad96bd746f1806da91859ec0d4a68b203b763da4d5ae567"},
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
|
||||
setup do: clear_config([:instance, :federating], true)
|
||||
setup do: clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
|
||||
setup :request_host_header
|
||||
|
||||
describe "/relay" do
|
||||
setup do: clear_config([:instance, :allow_relay], true)
|
||||
|
|
@ -1854,6 +1855,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
|
||||
build_conn()
|
||||
|> put_req_header("accept", "application/activity+json")
|
||||
|> with_request_host_header()
|
||||
|> assign(:user, other_user)
|
||||
|> get(object_path)
|
||||
|> json_response(200)
|
||||
|
|
@ -1878,6 +1880,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
|
||||
build_conn()
|
||||
|> put_req_header("accept", "application/activity+json")
|
||||
|> with_request_host_header()
|
||||
|> assign(:user, other_user)
|
||||
|> get(activity_path)
|
||||
|> json_response(200)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
|
|||
clear_config([:mrf_simple, :reject], [])
|
||||
end
|
||||
|
||||
setup :request_host_header
|
||||
|
||||
describe "gather_webfinger_links/1" do
|
||||
test "it returns links" do
|
||||
user = insert(:user)
|
||||
|
|
@ -449,12 +451,14 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
|
|||
|
||||
build_conn()
|
||||
|> put_req_header("accept", "application/activity+json")
|
||||
|> with_request_host_header()
|
||||
|> assign(:user, fetcher)
|
||||
|> get(object_path)
|
||||
|> json_response(200)
|
||||
|
||||
build_conn()
|
||||
|> put_req_header("accept", "application/activity+json")
|
||||
|> with_request_host_header()
|
||||
|> assign(:user, another_fetcher)
|
||||
|> get(activity_path)
|
||||
|> json_response(200)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ defmodule Pleroma.Web.Feed.UserControllerTest do
|
|||
alias Pleroma.Web.Feed.FeedView
|
||||
|
||||
setup do: clear_config([:static_fe, :enabled], false)
|
||||
setup :request_host_header
|
||||
|
||||
describe "feed" do
|
||||
setup do: clear_config([:feed])
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
|
|||
end
|
||||
|
||||
setup do: clear_config([:static_fe, :enabled], false)
|
||||
setup :request_host_header
|
||||
|
||||
describe "Mastodon compatibility routes" do
|
||||
setup %{conn: conn} do
|
||||
|
|
|
|||
118
test/pleroma/web/plugs/ensure_host_plug_test.exs
Normal file
118
test/pleroma/web/plugs/ensure_host_plug_test.exs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
defmodule Pleroma.Web.Plugs.EnsureHostPlugTest do
|
||||
use Pleroma.Web.ConnCase, async: false
|
||||
alias Pleroma.Web.Plugs.EnsureHostPlug
|
||||
|
||||
import Plug.Conn
|
||||
import Mock
|
||||
|
||||
defp put_host_header(conn, host) do
|
||||
%{
|
||||
conn
|
||||
| req_headers: [
|
||||
{"host", host} | conn.req_headers
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
describe "requires a host header that matches our server" do
|
||||
setup do
|
||||
conn = build_conn(:get, "/doesntmatter")
|
||||
|
||||
[conn: conn]
|
||||
end
|
||||
|
||||
test "rejects a request where no host value is present", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_host_header(nil)
|
||||
|> EnsureHostPlug.call(%{})
|
||||
|
||||
assert conn.halted == true
|
||||
assert conn.status == 400
|
||||
assert conn.state == :sent
|
||||
assert conn.resp_body == "Host header not present"
|
||||
end
|
||||
|
||||
test "rejects a request where the host value does not match", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_host_header("oops-not-us.info")
|
||||
|> EnsureHostPlug.call(%{})
|
||||
|
||||
assert conn.halted == true
|
||||
assert conn.status == 400
|
||||
assert conn.state == :sent
|
||||
assert conn.resp_body == "Host header does not match"
|
||||
end
|
||||
|
||||
test "rejects a request where the port value does not match", %{conn: conn} do
|
||||
host = Pleroma.Web.Endpoint.host()
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_host_header("#{host}:9")
|
||||
|> EnsureHostPlug.call(%{})
|
||||
|
||||
assert conn.halted == true
|
||||
assert conn.status == 400
|
||||
assert conn.state == :sent
|
||||
assert conn.resp_body == "Host header does not match"
|
||||
end
|
||||
|
||||
test "accepts a request where the hostname matches and there is no port", %{conn: conn} do
|
||||
# this test actually needs a mock as our test server does not run on the default http port
|
||||
|
||||
url = Pleroma.Web.Endpoint.struct_url()
|
||||
|
||||
with_mock Pleroma.Web.Endpoint, struct_url: fn -> %{url | port: 80} end do
|
||||
conn =
|
||||
conn
|
||||
|> put_host_header(url.host)
|
||||
|> EnsureHostPlug.call(%{})
|
||||
|
||||
assert conn.halted == false
|
||||
end
|
||||
end
|
||||
|
||||
test "rejects a request where the hostname matches and there is no port, and we do not run on the default",
|
||||
%{conn: conn} do
|
||||
url = Pleroma.Web.Endpoint.struct_url()
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_host_header(url.host)
|
||||
|> EnsureHostPlug.call(%{})
|
||||
|
||||
assert conn.halted == true
|
||||
assert conn.status == 400
|
||||
assert conn.state == :sent
|
||||
assert conn.resp_body == "Host header does not match"
|
||||
end
|
||||
|
||||
test "accepts a request where the hostname matches, with a port", %{conn: conn} do
|
||||
%{host: host, port: port} = Pleroma.Web.Endpoint.struct_url()
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_host_header("#{host}:#{port}")
|
||||
|> EnsureHostPlug.call(%{})
|
||||
|
||||
assert conn.halted == false
|
||||
end
|
||||
|
||||
test "rejects a request with multiple host headers", %{conn: conn} do
|
||||
url = Pleroma.Web.Endpoint.struct_url()
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_host_header(url.host)
|
||||
|> put_host_header("example.com")
|
||||
|> EnsureHostPlug.call(%{})
|
||||
|
||||
assert conn.halted == true
|
||||
assert conn.status == 400
|
||||
assert conn.state == :sent
|
||||
assert conn.resp_body == "Bad host header"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -53,6 +53,7 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
|||
|
||||
conn =
|
||||
conn
|
||||
|> assign(:host_matches, true)
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"#{user.signing_key.key_id}\""
|
||||
|
|
@ -71,7 +72,11 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
|||
clear_config([:activitypub, :authorized_fetch_mode], true)
|
||||
|
||||
params = %{"actor" => "http://mastodon.example.org/users/admin"}
|
||||
conn = build_conn(:get, "/doesntmattter", params) |> put_format("activity+json")
|
||||
|
||||
conn =
|
||||
build_conn(:get, "/doesntmattter", params)
|
||||
|> put_format("activity+json")
|
||||
|> assign(:host_matches, true)
|
||||
|
||||
[conn: conn]
|
||||
end
|
||||
|
|
@ -138,6 +143,7 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
|||
test "fakes success on gone key when receiving Delete" do
|
||||
build_conn(:post, "/inbox", %{"type" => "Delete"})
|
||||
|> put_format("activity+json")
|
||||
|> assign(:host_matches, true)
|
||||
|> assign(:gone_signature_key, true)
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
|
|
@ -150,6 +156,7 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
|||
test "fails on gone key for non-Delete" do
|
||||
conn =
|
||||
build_conn(:post, "/inbox", %{"type" => "Note"})
|
||||
|> assign(:host_matches, true)
|
||||
|> put_format("activity+json")
|
||||
|> assign(:gone_signature_key, true)
|
||||
|> put_req_header(
|
||||
|
|
@ -165,6 +172,7 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
|||
|
||||
test "fakes accept for POST on rejected keys", %{user: user} do
|
||||
build_conn(:post, "/inbox", %{"type" => "Note"})
|
||||
|> assign(:host_matches, true)
|
||||
|> put_format("activity+json")
|
||||
|> assign(:rejected_key_id, true)
|
||||
|> put_req_header(
|
||||
|
|
@ -177,6 +185,7 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
|||
|
||||
test "fakes not found for GET on rejected keys", %{user: user} do
|
||||
build_conn(:get, "/doesntmattter", %{"user" => user.ap_id})
|
||||
|> assign(:host_matches, true)
|
||||
|> put_format("activity+json")
|
||||
|> assign(:rejected_key_id, true)
|
||||
|> put_req_header(
|
||||
|
|
@ -186,4 +195,19 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
|||
|> HTTPSignaturePlug.call(%{})
|
||||
|> response(404)
|
||||
end
|
||||
|
||||
test "does not assign anything when a host match has not been run", %{user: user} do
|
||||
conn =
|
||||
build_conn(:get, "/doesntmattter", %{"user" => user.ap_id})
|
||||
|> assign(:host_matches, false)
|
||||
|> put_format("activity+json")
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"#{user.signing_key.key_id}\""
|
||||
)
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
|
||||
refute Map.has_key?(conn.assigns, :valid_signature)
|
||||
refute Map.has_key?(conn.assigns, :signature_user)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
|
|||
|
||||
setup_all do: clear_config([:static_fe, :enabled], true)
|
||||
setup do: clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
|
||||
setup :request_host_header
|
||||
|
||||
setup %{conn: conn} do
|
||||
conn = put_req_header(conn, "accept", "text/html")
|
||||
|
|
|
|||
|
|
@ -59,6 +59,21 @@ defmodule Pleroma.Web.ConnCase do
|
|||
[conn: conn]
|
||||
end
|
||||
|
||||
defp with_request_host_header(conn) do
|
||||
uri = Pleroma.Web.Endpoint.struct_url()
|
||||
|
||||
%{
|
||||
conn
|
||||
| req_headers: [
|
||||
{"host", "#{uri.host}:#{uri.port}"} | conn.req_headers
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp request_host_header(%{conn: conn}) do
|
||||
[conn: with_request_host_header(conn)]
|
||||
end
|
||||
|
||||
defp empty_json_response(conn) do
|
||||
body = response(conn, 204)
|
||||
response_content_type(conn, :json)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue