From 2024a66a6db2387d811abf6047801fc94262c0b5 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 15 Oct 2026 01:04:21 +0400 Subject: [PATCH] =?UTF-8?q?fix(hub):=20=D0=BD=D1=83=D0=BB=D0=B5=D0=B2?= =?UTF-8?q?=D0=BE=D0=B9=20=D0=B1=D0=B0=D0=B9=D1=82=20=D0=B2=20=D0=BF=D1=83?= =?UTF-8?q?=D1=82=D0=B8=20/app/assets=20=E2=80=94=20404;=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D1=8B=20=D1=82=D0=B8=D0=BF=D0=BE=D0=B2=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA?= =?UTF-8?q?=D0=B8=20(T1.1,=20T1.4,=20T16.1,=20#29)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Haiku 5.5 --- src/jira_hub/api.py | 7 ++++++- tests/test_api.py | 3 +-- tests/test_web_app.py | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/jira_hub/api.py b/src/jira_hub/api.py index 14f57e3..ecea01a 100644 --- a/src/jira_hub/api.py +++ b/src/jira_hub/api.py @@ -194,8 +194,13 @@ def h_app(hub, conn, m, q, body): return 503, Raw("text/html; charset=utf-8", NOT_BUILT) rest = unquote(m.group(1) or "") if rest.startswith("assets/"): + if "\x00" in rest: # T1.1: нулевой байт — 404, а не 500 из resolve() + raise ApiError(404, "not_found", "/app/" + rest.replace("\x00", "%00")) assets = (hub.web_dir / "assets").resolve() - f = (hub.web_dir / rest).resolve() + try: + f = (hub.web_dir / rest).resolve() + except (OSError, ValueError): + raise ApiError(404, "not_found", "/app/" + rest) if not f.is_relative_to(assets) or not f.is_file(): raise ApiError(404, "not_found", "/app/" + rest) return 200, Raw(ASSET_TYPES.get(f.suffix, "application/octet-stream"), f.read_bytes(), ASSET_CACHE) diff --git a/tests/test_api.py b/tests/test_api.py index 4f35df8..ae3c9d6 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -158,8 +158,7 @@ def test_bad_requests(hub): with pytest.raises(HubError) as e: c.get("/issues", limit="abc") assert e.value.status == 400 - import urllib.request, urllib.error - req = urllib.request.Request(hub["base"] + "/batches", data=b"{not json", method="POST", + req =urllib.request.Request(hub["base"] + "/batches", data=b"{not json", method="POST", headers={"Authorization": "Bearer " + c.token}) with pytest.raises(urllib.error.HTTPError) as e2: urllib.request.urlopen(req) diff --git a/tests/test_web_app.py b/tests/test_web_app.py index 24c128c..bfa7007 100644 --- a/tests/test_web_app.py +++ b/tests/test_web_app.py @@ -36,6 +36,9 @@ def app_hub(tmp_path): (web / "assets" / "index-abc123.js").write_text("console.log('ok')", encoding="utf-8") (web / "assets" / "index-abc123.css").write_text("body{}", encoding="utf-8") (web / "assets" / "font.bin").write_bytes(b"\x00\x01") + for name, data in (("logo.svg", b""), ("p.png", b"\x89PNG"), ("f.ico", b"\x00"), ("f.woff2", b"wOF2"), + ("index-abc123.js.map", b"{}")): + (web / "assets" / name).write_bytes(data) (tmp_path / "secret.txt").write_text("секрет", encoding="utf-8") srv, th, base = _serve(tmp_path, web) yield base @@ -111,3 +114,16 @@ def test_old_board_addresses_redirect_to_app(app_hub, path): def test_links_page_is_gone(): assert not hasattr(api, "PAGE") assert not (api.WEB_APP.parent / "links.html").exists() + + +@pytest.mark.parametrize("name,ctype", [("logo.svg", "image/svg+xml"), ("p.png", "image/png"), ("f.ico", "image/x-icon"), + ("f.woff2", "font/woff2"), ("index-abc123.js.map", "application/json")]) +def test_asset_content_types(app_hub, name, ctype): + status, r, _ = _get(app_hub, "/app/assets/" + name) + assert status == 200 and r.getheader("Content-Type") == ctype + + +@pytest.mark.parametrize("path", ["/app/assets/%00", "/app/assets/a%00.js", "/app/assets/%00/../index-abc123.js"]) +def test_nul_in_asset_path_is_404(app_hub, path): + status, _, _ = _get(app_hub, path) + assert status == 404