You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by sb...@apache.org on 2026/10/12 16:14:08 UTC
(tooling-trusted-releases) branch sbp updated: Limit all form lists to at most 1000 items
This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
The following commit(s) were added to refs/heads/sbp by this push:
new a1a1cb67 Limit all form lists to at most 1000 items
a1a1cb67 is described below
commit a1a1cb67d96cb344ec42c385d75e268a2a602867
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Mon Oct 12 17:13:16 2026 +0100
Limit all form lists to at most 1000 items
---
atr/form.py | 15 +++++++-
tests/unit/test_form_list_limit.py | 73 ++++++++++++++++++++++++++++++++++++++
2 files changed, 87 insertions(+), 1 deletion(-)
diff --git a/atr/form.py b/atr/form.py
index 99cb069c..14abaaa6 100644
--- a/atr/form.py
+++ b/atr/form.py
@@ -48,6 +48,7 @@ if TYPE_CHECKING:
DISCRIMINATOR_NAME: Final[str] = "variant"
DISCRIMINATOR: Final[Any] = schema.discriminator(DISCRIMINATOR_NAME)
+MAX_LIST_ITEMS: Final[int] = 1000
_CONFIRM_PATTERN = re.compile(r"^[A-Za-z0-9 _.,!?-]+$")
@@ -430,6 +431,7 @@ def to_enum[EnumType: enum.Enum](v: Any, enum_class: type[EnumType]) -> EnumType
def to_enum_set[EnumType: enum.Enum](v: Any, enum_class: type[EnumType]) -> set[EnumType]:
+ _check_list_size(v)
members: dict[str, EnumType] = {member.value: member for member in enum_class}
if isinstance(v, set):
return {item for item in v if isinstance(item, enum_class)}
@@ -464,6 +466,7 @@ def to_filestorage(v: Any) -> datastructures.FileStorage | None:
def to_filestorage_list(v: Any) -> list[datastructures.FileStorage]:
+ _check_list_size(v)
if isinstance(v, list):
result = []
for item in v:
@@ -507,6 +510,7 @@ def to_optional_url(v: Any) -> pydantic.HttpUrl | None:
def to_safe_path_list(v: Any) -> list[safe.RelPath]:
+ _check_list_size(v)
if isinstance(v, list):
result = []
for item in v:
@@ -518,6 +522,7 @@ def to_safe_path_list(v: Any) -> list[safe.RelPath]:
def to_str_list(v: Any) -> list[str]:
+ _check_list_size(v)
if isinstance(v, list):
return [str(item) for item in v if item != ""]
if isinstance(v, str):
@@ -541,13 +546,21 @@ def to_standard_uri_list(v: Any) -> list[str]:
return items
+def _check_list_size(v: Any) -> None:
+ if isinstance(v, list) and (len(v) > MAX_LIST_ITEMS):
+ raise ValueError(f"Expected at most {MAX_LIST_ITEMS} items, got {len(v)}")
+
+
def _to_uri_items(v: Any) -> list[str]:
+ _check_list_size(v)
if v is None or v == "":
return []
if isinstance(v, list):
return [str(item) for item in v if item not in (None, "")]
if isinstance(v, str):
- return [line.strip() for line in v.split("\n") if line.strip()]
+ items = [line.strip() for line in v.split("\n") if line.strip()]
+ _check_list_size(items)
+ return items
raise ValueError(f"Expected a string or list of URIs, got {type(v).__name__}")
diff --git a/tests/unit/test_form_list_limit.py b/tests/unit/test_form_list_limit.py
new file mode 100644
index 00000000..98177228
--- /dev/null
+++ b/tests/unit/test_form_list_limit.py
@@ -0,0 +1,73 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import collections.abc as abc
+import enum
+import functools
+from typing import Any
+
+import pydantic
+import pytest
+import quart.datastructures as datastructures
+
+import atr.form as form
+
+_URI = "https://example.org/x"
+
+
+class Colour(enum.Enum):
+ RED = "red"
+
+
+class ListForm(form.Form):
+ names: form.StrList = form.label("Names")
+
+
+@pytest.mark.parametrize(
+ ("convert", "valid", "invalid"),
+ [
+ (form.to_str_list, "alice", ""),
+ (form.to_safe_path_list, "a.txt", "/etc/passwd"),
+ (form.to_repository_uri_list, _URI, "javascript:alert(1)"),
+ (form.to_standard_uri_list, _URI, ""),
+ (form.to_filestorage_list, datastructures.FileStorage(filename="a.txt"), "a.txt"),
+ (functools.partial(form.to_enum_set, enum_class=Colour), "red", "red"),
+ ],
+)
+def test_list_converters_limit_items(convert: abc.Callable[[Any], Any], valid: object, invalid: object) -> None:
+ assert convert([valid] * 1000)
+ with pytest.raises(ValueError, match="Expected at most 1000 items, got 1001"):
+ convert([invalid] * 1001)
+
+
+def test_list_limit_is_a_field_error() -> None:
+ validated = form.validate(ListForm, {"csrf_token": "x", "names": ["alice"] * 1000})
+ assert isinstance(validated, ListForm)
+ assert len(validated.names) == 1000
+ with pytest.raises(pydantic.ValidationError) as exc_info:
+ form.validate(ListForm, {"csrf_token": "x", "names": ["alice"] * 1001})
+ errors = exc_info.value.errors()
+ assert [(error["loc"], error["type"]) for error in errors] == [(("names",), "value_error")]
+ flash_data = form.flash_error_data(ListForm, errors, {})
+ assert flash_data["names"]["msg"] == "Expected at most 1000 items, got 1001"
+
+
+@pytest.mark.parametrize("convert", [form.to_repository_uri_list, form.to_standard_uri_list])
+def test_uri_text_limits_entries(convert: abc.Callable[[Any], list[str]]) -> None:
+ assert convert("\n\n".join([_URI] * 1000)) == [_URI] * 1000
+ with pytest.raises(ValueError, match="Expected at most 1000 items, got 1001"):
+ convert("\n".join(["javascript:alert(1)"] * 1001))
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org