Add configurable tag guardrails

This commit is contained in:
2026-03-18 13:36:40 +01:00
parent 8dbf05b7fe
commit 57fdb4ed85
9 changed files with 150 additions and 15 deletions

View File

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

View File

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

View File

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