From c16c826074c46534a00e5bb54d61dc0215ace118 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Tue, 12 May 2026 05:24:19 +0200 Subject: [PATCH] Fix log server path extraction to use removeprefix The log server uses request.url.path.lstrip("/log/") to extract the requested filename from the URL path. str.lstrip() strips any combination of the argument characters (here {/, l, o, g}) from the left of the string -- it does not remove the literal prefix "/log/". This is a documented Python pitfall. Switch to str.removeprefix("/log/") (Python 3.9+, already required by Airflow) so the filename extracted for JWT validation matches the one the underlying Starlette StaticFiles mount uses to locate the file on disk. Generated-by: Claude Opus 4.7 (1M context) following the guidelines at https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions --- .../src/airflow/utils/serve_logs/log_server.py | 2 +- airflow-core/tests/unit/utils/test_serve_logs.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/utils/serve_logs/log_server.py b/airflow-core/src/airflow/utils/serve_logs/log_server.py index 292d7bf5b167f..92178bf5a245f 100644 --- a/airflow-core/src/airflow/utils/serve_logs/log_server.py +++ b/airflow-core/src/airflow/utils/serve_logs/log_server.py @@ -67,7 +67,7 @@ async def validate_jwt_token(self, request: Request): payload = await signer.avalidated_claims(auth) token_filename = payload.get("filename") # Extract filename from url path - request_filename = request.url.path.lstrip("/log/") + request_filename = request.url.path.removeprefix("/log/") if token_filename is None: logger.warning("The payload does not contain 'filename' key: %s.", payload) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) diff --git a/airflow-core/tests/unit/utils/test_serve_logs.py b/airflow-core/tests/unit/utils/test_serve_logs.py index b6c7805386ac9..ebd8d602eb444 100644 --- a/airflow-core/tests/unit/utils/test_serve_logs.py +++ b/airflow-core/tests/unit/utils/test_serve_logs.py @@ -124,6 +124,19 @@ def test_forbidden_different_logname(self, client: TestClient, jwt_generator): ) assert response.status_code == 403 + def test_forbidden_lstrip_character_overlap(self, client: TestClient, jwt_generator): + # The request path and the JWT filename intersect on the set {/, l, o, g}: + # str.lstrip("/log/") on "/log/log_sample.log" returns "_sample.log", + # which would have matched the JWT, but StaticFiles serves "log_sample.log". + # removeprefix preserves the literal prefix so the two paths agree. + response = client.get( + "/log/log_sample.log", + headers={ + "Authorization": jwt_generator.generate({"filename": "_sample.log"}), + }, + ) + assert response.status_code == 403 + def test_forbidden_expired(self, client: TestClient, jwt_generator): with time_machine.travel("2010-01-14"): token = jwt_generator.generate({"filename": "sample.log"})