This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/main by this push:
     new 343890a  Improve HTTP field header value construction validation
343890a is described below

commit 343890ab8f86d778fe6f7d5ebbcfea77b08dcffb
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Oct 27 20:22:53 2025 +0000

    Improve HTTP field header value construction validation
---
 atr/routes/download.py |  8 +++-----
 atr/web.py             | 37 +++++++++++++++++++++++++++++++++++++
 2 files changed, 40 insertions(+), 5 deletions(-)

diff --git a/atr/routes/download.py b/atr/routes/download.py
index ff2365b..e7fec63 100644
--- a/atr/routes/download.py
+++ b/atr/routes/download.py
@@ -24,7 +24,6 @@ import aiofiles
 import aiofiles.os
 import asfquart.base as base
 import quart
-import werkzeug.http
 import werkzeug.wrappers.response as response
 import zipstream
 
@@ -144,11 +143,10 @@ async def zip_selected(
             yield chunk
 
     headers = {
-        "Content-Disposition": f"attachment; 
filename={werkzeug.http.quote_header_value(release.name + '.zip')}",
-        "Content-Type": "application/zip",
+        "Content-Disposition": web.HeaderValue("attachment", 
filename=release.name + ".zip"),
+        "Content-Type": web.HeaderValue("application/zip"),
     }
-    # TODO: Write a type safe wrapper for quart.Response that ensures headers 
are encoded correctly
-    return quart.Response(stream_zip(files_to_zip), headers=headers, 
mimetype="application/zip")
+    return web.ZipResponse(stream_zip(files_to_zip), headers=headers)
 
 
 async def _download_or_list(project_name: str, version_name: str, file_path: 
str) -> response.Response | quart.Response:
diff --git a/atr/web.py b/atr/web.py
index d92f3db..442e9e6 100644
--- a/atr/web.py
+++ b/atr/web.py
@@ -22,6 +22,8 @@ from typing import TYPE_CHECKING, Any, Protocol, TypeVar
 import asfquart.base as base
 import asfquart.session as session
 import quart
+import werkzeug.datastructures.headers
+import werkzeug.http
 
 import atr.config as config
 import atr.db as db
@@ -177,6 +179,30 @@ class ElementResponse(quart.Response):
         super().__init__(str(element), status=status, mimetype="text/html")
 
 
+class HeaderValue:
+    # TODO: There does not appear to be a general HTTP header construction 
package in Python
+    # The existence of one would help us and others to adhere to the HTTP 
component of ASVS v5 1.2.1
+    # Our validation is slightly more strict than that of Werkzeug
+
+    def __init__(self, value: str, /, **kwargs: str) -> None:
+        for text in (value, *kwargs.values()):
+            if '"' in text:
+                raise ValueError(f"Header value cannot contain double quotes: 
{text}")
+            if "\x00" in text:
+                raise ValueError(f"Header value cannot contain null bytes: 
{text}")
+
+        headers = werkzeug.datastructures.headers.Headers()
+        headers.add("X-Header-Value", value, **kwargs)
+        werkzeug_value = headers.get("X-Header-Value")
+        if werkzeug_value is None:
+            raise ValueError("Header value should not be None after 
validation")
+
+        self.__value = werkzeug_value
+
+    def __str__(self) -> str:
+        return self.__value
+
+
 class RouteFunction(Protocol[R]):
     """Protocol for @app_route decorated functions."""
 
@@ -191,6 +217,17 @@ class TextResponse(quart.Response):
         super().__init__(text, status=status, mimetype="text/plain")
 
 
+class ZipResponse(quart.Response):
+    def __init__(
+        self,
+        response: Any,
+        headers: dict[str, HeaderValue],
+        status: int = 200,
+    ) -> None:
+        raw_headers = {name: str(value) for name, value in headers.items()}
+        super().__init__(response, status=status, headers=raw_headers, 
mimetype="application/zip")
+
+
 async def redirect[R](
     route: RouteFunction[R], success: str | None = None, error: str | None = 
None, **kwargs: Any
 ) -> response.Response:


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to