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:
rinpatch 2020-02-29 23:08:14 +00:00
commit 438394d404
9 changed files with 78 additions and 8 deletions

View file

@ -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

View file

@ -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},

View file

@ -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}],

View file

@ -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.

View file

@ -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)

View file

@ -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

View file

@ -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}),

View file

@ -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)

View file

@ -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