Add configurable tag guardrails
This commit is contained in:
@@ -6,6 +6,7 @@ defmodule Parrhesia.Protocol.EventValidator do
|
||||
@required_fields ~w[id pubkey created_at kind tags content sig]
|
||||
@max_kind 65_535
|
||||
@default_max_event_future_skew_seconds 900
|
||||
@default_max_tags_per_event 256
|
||||
@supported_mls_ciphersuites MapSet.new(~w[0x0001 0x0002 0x0003 0x0004 0x0005 0x0006 0x0007])
|
||||
@required_mls_extensions MapSet.new(["0xf2ee", "0x000a"])
|
||||
@supported_keypackage_ref_sizes [32, 48, 64]
|
||||
@@ -17,6 +18,7 @@ defmodule Parrhesia.Protocol.EventValidator do
|
||||
| :invalid_created_at
|
||||
| :created_at_too_far_in_future
|
||||
| :invalid_kind
|
||||
| :too_many_tags
|
||||
| :invalid_tags
|
||||
| :invalid_content
|
||||
| :invalid_sig
|
||||
@@ -87,6 +89,7 @@ defmodule Parrhesia.Protocol.EventValidator do
|
||||
created_at_too_far_in_future:
|
||||
"invalid: event creation date is too far off from the current time",
|
||||
invalid_kind: "invalid: kind must be an integer between 0 and 65535",
|
||||
too_many_tags: "invalid: event tags exceed configured limit",
|
||||
invalid_tags: "invalid: tags must be an array of non-empty string arrays",
|
||||
invalid_content: "invalid: content must be a string",
|
||||
invalid_sig: "invalid: sig must be 64-byte lowercase hex",
|
||||
@@ -169,16 +172,25 @@ defmodule Parrhesia.Protocol.EventValidator do
|
||||
defp validate_kind(kind) when is_integer(kind) and kind >= 0 and kind <= @max_kind, do: :ok
|
||||
defp validate_kind(_kind), do: {:error, :invalid_kind}
|
||||
|
||||
defp validate_tags(tags) when is_list(tags) do
|
||||
if Enum.all?(tags, &valid_tag?/1) do
|
||||
:ok
|
||||
else
|
||||
{:error, :invalid_tags}
|
||||
end
|
||||
end
|
||||
defp validate_tags(tags) when is_list(tags), do: validate_tags(tags, max_tags_per_event(), 0)
|
||||
|
||||
defp validate_tags(_tags), do: {:error, :invalid_tags}
|
||||
|
||||
defp validate_tags([], _max_tags, _count), do: :ok
|
||||
|
||||
defp validate_tags([tag | rest], max_tags, count) do
|
||||
cond do
|
||||
count + 1 > max_tags ->
|
||||
{:error, :too_many_tags}
|
||||
|
||||
valid_tag?(tag) ->
|
||||
validate_tags(rest, max_tags, count + 1)
|
||||
|
||||
true ->
|
||||
{:error, :invalid_tags}
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_content(content) when is_binary(content), do: :ok
|
||||
defp validate_content(_content), do: {:error, :invalid_content}
|
||||
|
||||
@@ -510,4 +522,11 @@ defmodule Parrhesia.Protocol.EventValidator do
|
||||
|> Application.get_env(:limits, [])
|
||||
|> Keyword.get(:max_event_future_skew_seconds, @default_max_event_future_skew_seconds)
|
||||
end
|
||||
|
||||
defp max_tags_per_event do
|
||||
case Application.get_env(:parrhesia, :limits, []) |> Keyword.get(:max_tags_per_event) do
|
||||
value when is_integer(value) and value > 0 -> value
|
||||
_other -> @default_max_tags_per_event
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,7 @@ defmodule Parrhesia.Protocol.Filter do
|
||||
|
||||
@max_kind 65_535
|
||||
@default_max_filters_per_req 16
|
||||
@default_max_tag_values_per_filter 128
|
||||
|
||||
@type validation_error ::
|
||||
:invalid_filters
|
||||
@@ -19,6 +20,7 @@ defmodule Parrhesia.Protocol.Filter do
|
||||
| :invalid_until
|
||||
| :invalid_limit
|
||||
| :invalid_search
|
||||
| :too_many_tag_values
|
||||
| :invalid_tag_filter
|
||||
|
||||
@allowed_keys MapSet.new(["ids", "authors", "kinds", "since", "until", "limit", "search"])
|
||||
@@ -36,6 +38,7 @@ defmodule Parrhesia.Protocol.Filter do
|
||||
invalid_until: "invalid: until must be a non-negative integer",
|
||||
invalid_limit: "invalid: limit must be a positive integer",
|
||||
invalid_search: "invalid: search must be a non-empty string",
|
||||
too_many_tag_values: "invalid: tag filters exceed configured value limit",
|
||||
invalid_tag_filter:
|
||||
"invalid: tag filters must use #<single-letter> with non-empty string arrays"
|
||||
}
|
||||
@@ -178,19 +181,33 @@ defmodule Parrhesia.Protocol.Filter do
|
||||
filter
|
||||
|> Enum.filter(fn {key, _value} -> valid_tag_filter_key?(key) end)
|
||||
|> Enum.reduce_while(:ok, fn {_key, values}, :ok ->
|
||||
if valid_tag_filter_values?(values) do
|
||||
{:cont, :ok}
|
||||
else
|
||||
{:halt, {:error, :invalid_tag_filter}}
|
||||
case validate_tag_filter_values(values) do
|
||||
:ok -> {:cont, :ok}
|
||||
{:error, reason} -> {:halt, {:error, reason}}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp valid_tag_filter_values?(values) when is_list(values) do
|
||||
values != [] and Enum.all?(values, &is_binary/1)
|
||||
end
|
||||
defp validate_tag_filter_values(values) when is_list(values),
|
||||
do: validate_tag_filter_values(values, max_tag_values_per_filter(), 0)
|
||||
|
||||
defp valid_tag_filter_values?(_values), do: false
|
||||
defp validate_tag_filter_values(_values), do: {:error, :invalid_tag_filter}
|
||||
|
||||
defp validate_tag_filter_values([], _max_values, 0), do: {:error, :invalid_tag_filter}
|
||||
defp validate_tag_filter_values([], _max_values, _count), do: :ok
|
||||
|
||||
defp validate_tag_filter_values([value | rest], max_values, count) do
|
||||
cond do
|
||||
count + 1 > max_values ->
|
||||
{:error, :too_many_tag_values}
|
||||
|
||||
is_binary(value) ->
|
||||
validate_tag_filter_values(rest, max_values, count + 1)
|
||||
|
||||
true ->
|
||||
{:error, :invalid_tag_filter}
|
||||
end
|
||||
end
|
||||
|
||||
defp filter_predicates(event, filter) do
|
||||
[
|
||||
@@ -278,4 +295,12 @@ defmodule Parrhesia.Protocol.Filter do
|
||||
|> Application.get_env(:limits, [])
|
||||
|> Keyword.get(:max_filters_per_req, @default_max_filters_per_req)
|
||||
end
|
||||
|
||||
defp max_tag_values_per_filter do
|
||||
case Application.get_env(:parrhesia, :limits, [])
|
||||
|> Keyword.get(:max_tag_values_per_filter) do
|
||||
value when is_integer(value) and value > 0 -> value
|
||||
_other -> @default_max_tag_values_per_filter
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -414,6 +414,7 @@ defmodule Parrhesia.Web.Connection do
|
||||
:invalid_until,
|
||||
:invalid_limit,
|
||||
:invalid_search,
|
||||
:too_many_tag_values,
|
||||
:invalid_tag_filter
|
||||
] ->
|
||||
Filter.error_message(reason)
|
||||
@@ -462,6 +463,27 @@ defmodule Parrhesia.Web.Connection do
|
||||
restricted_count_notice(state, subscription_id, EventPolicy.error_message(reason))
|
||||
end
|
||||
|
||||
defp handle_count_error(state, subscription_id, reason)
|
||||
when reason in [
|
||||
:invalid_filters,
|
||||
:empty_filters,
|
||||
:too_many_filters,
|
||||
:invalid_filter,
|
||||
:invalid_filter_key,
|
||||
:invalid_ids,
|
||||
:invalid_authors,
|
||||
:invalid_kinds,
|
||||
:invalid_since,
|
||||
:invalid_until,
|
||||
:invalid_limit,
|
||||
:invalid_search,
|
||||
:too_many_tag_values,
|
||||
:invalid_tag_filter
|
||||
] do
|
||||
response = Protocol.encode_relay({:closed, subscription_id, Filter.error_message(reason)})
|
||||
{:push, {:text, response}, state}
|
||||
end
|
||||
|
||||
defp handle_count_error(state, subscription_id, reason) do
|
||||
response = Protocol.encode_relay({:closed, subscription_id, inspect(reason)})
|
||||
{:push, {:text, response}, state}
|
||||
@@ -648,6 +670,7 @@ defmodule Parrhesia.Web.Connection do
|
||||
:invalid_until,
|
||||
:invalid_limit,
|
||||
:invalid_search,
|
||||
:too_many_tag_values,
|
||||
:invalid_tag_filter,
|
||||
:auth_required,
|
||||
:pubkey_not_allowed,
|
||||
@@ -701,6 +724,7 @@ defmodule Parrhesia.Web.Connection do
|
||||
:invalid_until,
|
||||
:invalid_limit,
|
||||
:invalid_search,
|
||||
:too_many_tag_values,
|
||||
:invalid_tag_filter
|
||||
],
|
||||
do: Filter.error_message(reason)
|
||||
|
||||
Reference in New Issue
Block a user