Skip to content

Commit de96d81

Browse files
authored
Validate multipart part headers (#1142)
1 parent 51c3269 commit de96d81

2 files changed

Lines changed: 29 additions & 0 deletions

File tree

‎src/httpx2/httpx2/_multipart.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
_HTML5_FORM_ENCODING_REPLACEMENTS = {'"': "%22", "\\": "\\\\"}
2525
_HTML5_FORM_ENCODING_REPLACEMENTS.update({chr(c): f"%{c:02X}" for c in range(0x1F + 1) if c != 0x1B})
2626
_HTML5_FORM_ENCODING_RE = re.compile(r"|".join([re.escape(c) for c in _HTML5_FORM_ENCODING_REPLACEMENTS.keys()]))
27+
_HEADER_NAME_RE = re.compile(r"[!#$%&'*+\-.^_`|~0-9A-Za-z]+")
28+
_FORBIDDEN_HEADER_VALUE_CHARS_RE = re.compile(r"[\x00-\x08\x0a-\x1f\x7f]")
2729

2830

2931
def _format_form_param(name: str, value: str) -> bytes:
@@ -176,6 +178,10 @@ def render_headers(self) -> bytes:
176178
filename = _format_form_param("filename", self.filename)
177179
parts.extend([b"; ", filename])
178180
for header_name, header_value in self.headers.items():
181+
if _HEADER_NAME_RE.fullmatch(header_name) is None:
182+
raise ValueError("Invalid multipart header name.")
183+
if _FORBIDDEN_HEADER_VALUE_CHARS_RE.search(header_value) is not None:
184+
raise ValueError("Invalid control character in multipart header value.")
179185
key, val = f"\r\n{header_name}: ".encode(), header_value.encode()
180186
parts.extend([key, val])
181187
parts.append(b"\r\n\r\n")

‎tests/httpx2/test_multipart.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,29 @@ def test_multipart_headers_include_content_type() -> None:
205205
)
206206

207207

208+
@pytest.mark.parametrize(
209+
("content_type", "file_headers"),
210+
[
211+
("text/plain\r", {}),
212+
(None, {"X-Test\t": "value"}),
213+
(None, {"X-Test": "value\n"}),
214+
],
215+
)
216+
def test_multipart_rejects_invalid_file_headers(content_type: str | None, file_headers: dict[str, str]) -> None:
217+
files = {"file": ("test.txt", b"<file content>", content_type, file_headers)}
218+
219+
with pytest.raises(ValueError, match="Invalid .*multipart header"):
220+
httpx2.Request("POST", "https://www.example.com/", files=files)
221+
222+
223+
@pytest.mark.parametrize("control_character", ["\x00", "\x01", "\x08", "\x1f", "\x7f"])
224+
def test_multipart_rejects_control_characters_in_file_header_values(control_character: str) -> None:
225+
files = {"file": ("test.txt", b"<file content>", None, {"X-Test": f"value{control_character}"})}
226+
227+
with pytest.raises(ValueError, match="Invalid control character in multipart header value"):
228+
httpx2.Request("POST", "https://www.example.com/", files=files)
229+
230+
208231
def test_multipart_encode(tmp_path: typing.Any) -> None:
209232
path = str(tmp_path / "name.txt")
210233
with open(path, "wb") as f:

0 commit comments

Comments
 (0)