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