Merge branch 'fix/easy-timeline-dos' into 'develop'
Cap the number of requested statuses in timelines to 40 and rate limit them See merge request pleroma/pleroma!2253
This commit is contained in:
commit
438394d404
9 changed files with 78 additions and 8 deletions
|
@ -4,6 +4,9 @@ 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/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Security
|
||||||
|
- Mastodon API: Fix being able to request enourmous amount of statuses in timelines leading to DoS. Now limited to 40 per request.
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
- **Breaking**: Removed 1.0+ deprecated configurations `Pleroma.Upload, :strip_exif` and `:instance, :dedupe_media`
|
- **Breaking**: Removed 1.0+ deprecated configurations `Pleroma.Upload, :strip_exif` and `:instance, :dedupe_media`
|
||||||
- **Breaking**: OStatus protocol support
|
- **Breaking**: OStatus protocol support
|
||||||
|
@ -56,6 +59,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Admin API: Render whole status in grouped reports
|
- Admin API: Render whole status in grouped reports
|
||||||
- Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise).
|
- Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise).
|
||||||
- Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try.
|
- Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try.
|
||||||
|
- Mastodon API: Limit timeline requests to 3 per timeline per 500ms per user/ip by default.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -599,6 +599,7 @@ config :http_signatures,
|
||||||
|
|
||||||
config :pleroma, :rate_limit,
|
config :pleroma, :rate_limit,
|
||||||
authentication: {60_000, 15},
|
authentication: {60_000, 15},
|
||||||
|
timeline: {500, 3},
|
||||||
search: [{1000, 10}, {1000, 30}],
|
search: [{1000, 10}, {1000, 30}],
|
||||||
app_account_creation: {1_800_000, 25},
|
app_account_creation: {1_800_000, 25},
|
||||||
relations_actions: {10_000, 10},
|
relations_actions: {10_000, 10},
|
||||||
|
|
|
@ -2465,6 +2465,12 @@ config :pleroma, :config_description, [
|
||||||
description: "For the search requests (account & status search etc.)",
|
description: "For the search requests (account & status search etc.)",
|
||||||
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
|
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
|
||||||
},
|
},
|
||||||
|
%{
|
||||||
|
key: :timeline,
|
||||||
|
type: [:tuple, {:list, :tuple}],
|
||||||
|
description: "For requests to timelines (each timeline has it's own limiter)",
|
||||||
|
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
|
||||||
|
},
|
||||||
%{
|
%{
|
||||||
key: :app_account_creation,
|
key: :app_account_creation,
|
||||||
type: [:tuple, {:list, :tuple}],
|
type: [:tuple, {:list, :tuple}],
|
||||||
|
|
|
@ -343,6 +343,7 @@ Means that:
|
||||||
Supported rate limiters:
|
Supported rate limiters:
|
||||||
|
|
||||||
* `:search` - Account/Status search.
|
* `:search` - Account/Status search.
|
||||||
|
* `:timeline` - Timeline requests (each timeline has it's own limiter).
|
||||||
* `:app_account_creation` - Account registration from the API.
|
* `:app_account_creation` - Account registration from the API.
|
||||||
* `:relations_actions` - Following/Unfollowing in general.
|
* `:relations_actions` - Following/Unfollowing in general.
|
||||||
* `:relation_id_action` - Following/Unfollowing for a specific user.
|
* `:relation_id_action` - Following/Unfollowing for a specific user.
|
||||||
|
|
|
@ -13,6 +13,7 @@ defmodule Pleroma.Pagination do
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
|
|
||||||
@default_limit 20
|
@default_limit 20
|
||||||
|
@max_limit 40
|
||||||
@page_keys ["max_id", "min_id", "limit", "since_id", "order"]
|
@page_keys ["max_id", "min_id", "limit", "since_id", "order"]
|
||||||
|
|
||||||
def page_keys, do: @page_keys
|
def page_keys, do: @page_keys
|
||||||
|
@ -130,7 +131,11 @@ defmodule Pleroma.Pagination do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp restrict(query, :limit, options, _table_binding) do
|
defp restrict(query, :limit, options, _table_binding) do
|
||||||
limit = Map.get(options, :limit, @default_limit)
|
limit =
|
||||||
|
case Map.get(options, :limit, @default_limit) do
|
||||||
|
limit when limit < @max_limit -> limit
|
||||||
|
_ -> @max_limit
|
||||||
|
end
|
||||||
|
|
||||||
query
|
query
|
||||||
|> limit(^limit)
|
|> limit(^limit)
|
||||||
|
|
|
@ -7,8 +7,8 @@ defmodule Pleroma.Plugs.RateLimiter.LimiterSupervisor do
|
||||||
DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
|
DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_limiter(limiter_name, expiration) do
|
def add_or_return_limiter(limiter_name, expiration) do
|
||||||
{:ok, _pid} =
|
result =
|
||||||
DynamicSupervisor.start_child(
|
DynamicSupervisor.start_child(
|
||||||
__MODULE__,
|
__MODULE__,
|
||||||
%{
|
%{
|
||||||
|
@ -28,6 +28,12 @@ defmodule Pleroma.Plugs.RateLimiter.LimiterSupervisor do
|
||||||
]}
|
]}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case result do
|
||||||
|
{:ok, _pid} = result -> result
|
||||||
|
{:error, {:already_started, pid}} -> {:ok, pid}
|
||||||
|
_ -> result
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
@ -171,7 +171,7 @@ defmodule Pleroma.Plugs.RateLimiter do
|
||||||
{:error, value}
|
{:error, value}
|
||||||
|
|
||||||
{:error, :no_cache} ->
|
{:error, :no_cache} ->
|
||||||
initialize_buckets(action_settings)
|
initialize_buckets!(action_settings)
|
||||||
check_rate(action_settings)
|
check_rate(action_settings)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -250,11 +250,16 @@ defmodule Pleroma.Plugs.RateLimiter do
|
||||||
|> String.replace_leading(":", "")
|
|> String.replace_leading(":", "")
|
||||||
end
|
end
|
||||||
|
|
||||||
defp initialize_buckets(%{name: _name, limits: nil}), do: :ok
|
defp initialize_buckets!(%{name: _name, limits: nil}), do: :ok
|
||||||
|
|
||||||
defp initialize_buckets(%{name: name, limits: limits}) do
|
defp initialize_buckets!(%{name: name, limits: limits}) do
|
||||||
LimiterSupervisor.add_limiter(anon_bucket_name(name), get_scale(:anon, limits))
|
{:ok, _pid} =
|
||||||
LimiterSupervisor.add_limiter(user_bucket_name(name), get_scale(:user, limits))
|
LimiterSupervisor.add_or_return_limiter(anon_bucket_name(name), get_scale(:anon, limits))
|
||||||
|
|
||||||
|
{:ok, _pid} =
|
||||||
|
LimiterSupervisor.add_or_return_limiter(user_bucket_name(name), get_scale(:user, limits))
|
||||||
|
|
||||||
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
defp attach_identity(base, %{mode: :user, conn_info: conn_info}),
|
defp attach_identity(base, %{mode: :user, conn_info: conn_info}),
|
||||||
|
|
|
@ -10,9 +10,20 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|
||||||
|
|
||||||
alias Pleroma.Pagination
|
alias Pleroma.Pagination
|
||||||
alias Pleroma.Plugs.OAuthScopesPlug
|
alias Pleroma.Plugs.OAuthScopesPlug
|
||||||
|
alias Pleroma.Plugs.RateLimiter
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
|
||||||
|
# TODO: Replace with a macro when there is a Phoenix release with
|
||||||
|
# https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e
|
||||||
|
# in it
|
||||||
|
|
||||||
|
plug(RateLimiter, [name: :timeline, bucket_name: :direct_timeline] when action == :direct)
|
||||||
|
plug(RateLimiter, [name: :timeline, bucket_name: :public_timeline] when action == :public)
|
||||||
|
plug(RateLimiter, [name: :timeline, bucket_name: :home_timeline] when action == :home)
|
||||||
|
plug(RateLimiter, [name: :timeline, bucket_name: :hashtag_timeline] when action == :hashtag)
|
||||||
|
plug(RateLimiter, [name: :timeline, bucket_name: :list_timeline] when action == :list)
|
||||||
|
|
||||||
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct])
|
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct])
|
||||||
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list)
|
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list)
|
||||||
|
|
||||||
|
|
|
@ -242,4 +242,35 @@ defmodule Pleroma.Plugs.RateLimiterTest do
|
||||||
refute conn_2.halted
|
refute conn_2.halted
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "doesn't crash due to a race condition when multiple requests are made at the same time and the bucket is not yet initialized" do
|
||||||
|
limiter_name = :test_race_condition
|
||||||
|
Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
|
||||||
|
Pleroma.Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
|
||||||
|
|
||||||
|
opts = RateLimiter.init(name: limiter_name)
|
||||||
|
|
||||||
|
conn = conn(:get, "/")
|
||||||
|
conn_2 = conn(:get, "/")
|
||||||
|
|
||||||
|
%Task{pid: pid1} =
|
||||||
|
task1 =
|
||||||
|
Task.async(fn ->
|
||||||
|
receive do
|
||||||
|
:process2_up ->
|
||||||
|
RateLimiter.call(conn, opts)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
task2 =
|
||||||
|
Task.async(fn ->
|
||||||
|
send(pid1, :process2_up)
|
||||||
|
RateLimiter.call(conn_2, opts)
|
||||||
|
end)
|
||||||
|
|
||||||
|
Task.await(task1)
|
||||||
|
Task.await(task2)
|
||||||
|
|
||||||
|
refute {:err, :not_found} == RateLimiter.inspect_bucket(conn, limiter_name, opts)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue