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]