CVE-2026-45426
Description
Apache Airflow log server incorrectly uses str.lstrip() for JWT authorization, allowing authenticated workers to read logs of other Dags with similar names.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Apache Airflow log server incorrectly uses str.lstrip() for JWT authorization, allowing authenticated workers to read logs of other Dags with similar names.
Vulnerability
Apache Airflow versions 3.0.0 before 3.2.2 contain an authorization bypass in the log server. The log server uses Python's str.lstrip("/log/") to extract the requested path segment when verifying a JWT's sub claim. str.lstrip() strips any combination of the characters {/, l, o, g} from the left, not the literal prefix "/log/". This allows a JWT issued for one Dag to authorize access to logs of other Dags whose names begin with a subset of those characters (e.g., dag_a grants access to dag_attacker, aaaa_target, _dag_secret). Affected deployments are those relying on per-Dag log-access scoping, such as multi-team or shared-executor topologies [1][2].
Exploitation
An attacker must be an authenticated Airflow worker holding a valid Log-server JWT for at least one Dag. With that JWT, the attacker can request log files for any other Dag whose name shares a character prefix with the authorized Dag. For example, a JWT for dag_a can be used to request logs for dag_attacker because str.lstrip() strips d, a, g, _ characters. No additional network position or user interaction is required; the attacker simply sends a crafted request to the log server [1][2].
Impact
A successful attacker can enumerate and read worker logs of other Dags, including task output and error traces. This breaks the per-Dag isolation boundary, leaking sensitive information from other workflows. The attacker gains unauthorized read access to log data at the same privilege level as the worker, with no escalation to other systems [2].
Mitigation
Users should upgrade to apache-airflow version 3.2.2 or later, which fixes the issue by replacing str.lstrip() with str.removeprefix("/log/") (Python 3.9+). The fix was merged in pull request #66749 and is included in the 3.2.2 release [1][2]. No workarounds are available for earlier versions; upgrading is the only mitigation.
AI Insight generated on Jun 1, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1Patches
2d8865dd42499Fix log server path extraction to use removeprefix (#66749)
2 files changed · +14 −1
airflow-core/src/airflow/utils/serve_logs/log_server.py+1 −1 modified@@ -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)
airflow-core/tests/unit/utils/test_serve_logs.py+13 −0 modified@@ -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"})
cde4885818beUpdating release notes for 3.2.2rc3
2 files changed · +5 −4
RELEASE_NOTES.rst+3 −2 modified@@ -24,7 +24,7 @@ .. towncrier release notes start -Airflow 3.2.2 (2026-05-27) +Airflow 3.2.2 (2026-05-29) -------------------------- Significant Changes @@ -81,7 +81,8 @@ Significant Changes Bug Fixes ^^^^^^^^^ - +- Fix ``Callback.handle_event`` triggerer crash when OpenTelemetry metrics receive dict typed tag values (#67527) (#67529) +- UI: Rewrite ``modulepreload hrefs`` to the api-server static path (#67548) (#67556) - Correctly pre-allocate ``external_executor_id`` with multiple executors on PostgreSQL (#67388) (#67458) - Return raw import-error stacktrace when a Dag file has no registered Dag (#67465) (#67478) - UI: Fix Expand/Collapse All on XComs and Audit Log JSON cells (#67316) (#67361)
reproducible_build.yaml+2 −2 modified@@ -1,2 +1,2 @@ -release-notes-hash: 6407b48d1054fe3ce68c09bf4435d91d -source-date-epoch: 1779745327 +release-notes-hash: 504288db9a9dc13a0db859232fab98d0 +source-date-epoch: 1779811737
Vulnerability mechanics
No source-code context for this CVE — mechanics is only generated when we can read the actual fix diff. Without that, the four sections (root cause, attack vector, affected code, fix) would be speculation rather than analysis.
References
3News mentions
0No linked articles in our index yet.