CVE-2026-45426
Airflow log server auth bypass allows reading logs from other DAGs.
- CVSS 3.1
- CWE-863
- Authentication, Authorization, and Session Management
- Local
Exploitation requires the attacker to already be an authenticated Airflow worker holding a valid Log-server JWT issued for at least one Dag. Apache Airflow's Log server authorized JWT tokens against Dag IDs by applying Python's `str.lstrip()` to the requested path segment when verifying the JWT's `sub` claim. `str.lstrip()` strips any of a *set* of characters from the left (not a prefix), so a JWT issued for a Dag named e.g. `dag_a` would authorize log access to any other Dag whose name began with any subset of the characters `{d, a, g, _}` (e.g. `dag_attacker`, `aaaa_target`, `_dag_secret`). Such an authenticated worker could enumerate and read worker logs of other Dags whose names happened to share that character-class prefix, leaking task output and error traces beyond the documented per-Dag isolation boundary. Affects deployments relying on per-Dag log-access scoping (multi-team, shared-executor, shared-worker topologies). Users are advised to upgrade to `apache-airflow` 3.2.2 or later.
- CWE
- CWE-863
- CVSS base score
- 3.1
- Published
- 2026-06-01
- OWASP
- A01 Broken Access Control
- Orthogonal defect classification
- Checking
- Code defect classification
- Incorrect Check
- Category
- Authentication, Authorization, and Session Management
- Subcategory
- Poorly Designed Access Controls
- Accessibility scope
- Local
- Impact
- Information Disclosure
- Affected component
- apache-airfl
- Fixed by upgrading
- Yes
Solution
Upgrade apache-airflow to version 3.2.2 or later.
Vulnerable code sample
def vulnerable_check_access(jwt_subject_dag_id, requested_dag_id):
"""
Simulates the vulnerable authorization logic.
The check is flawed because str.lstrip() treats its argument as a
SET of characters to remove from the left, not as a prefix.
If the requested_dag_id is composed entirely of characters from the
jwt_subject_dag_id, lstrip will return an empty string, which evaluates
to False, thereby bypassing the authorization failure check.
"""
print(f"--- Checking access for '{requested_dag_id}' ---")
print(f"User is authorized for: '{jwt_subject_dag_id}'")
# Vulnerable line: An empty string is 'falsy', so if lstrip consumes
# the entire string, the `if` condition is false and access is granted.
if requested_dag_id.lstrip(jwt_subject_dag_id):
print("Result: Access DENIED\n")
else:
print("Result: Access GRANTED\n")
# The attacker holds a valid JWT for a non-sensitive DAG named 'dag_a'.
# The set of characters for lstrip becomes {'d', 'a', 'g', '_'}.
attacker_authorized_dag = "dag_a"
# 1. Correct behavior: Access is granted for the authorized DAG.
# 'dag_a'.lstrip('dag_a') -> '' (falsy) -> Access Granted
vulnerable_check_access(attacker_authorized_dag, "dag_a")
# 2. Correct behavior: Access is denied for an unrelated DAG.
# 'secret_dag_x'.lstrip('dag_a') -> 'secret_d' (truthy) -> Access Denied
vulnerable_check_access(attacker_authorized_dag, "secret_dag_x")
# 3. VULNERABILITY: Access is incorrectly granted to another DAG.
# The target DAG 'ad_g' is composed entirely of characters from 'dag_a'.
# 'ad_g'.lstrip('dag_a') -> '' (falsy) -> Access Granted
vulnerable_check_access(attacker_authorized_dag, "ad_g")
# 4. VULNERABILITY: Another example.
# The target DAG '_gaad' is also composed entirely of characters from 'dag_a'.
# '_gaad'.lstrip('dag_a') -> '' (falsy) -> Access Granted
vulnerable_check_access(attacker_authorized_dag, "_gaad")Patched code sample
def corrected_log_access_check(requested_dag_id: str, authorized_dag_id_from_jwt: str) -> bool:
"""
Represents the fixed authorization logic for Apache Airflow log access.
This function demonstrates the fix for the vulnerability by replacing a
flawed `lstrip`-based check with a direct string equality comparison. This
ensures a token is only valid for the exact DAG ID specified in its
subject claim.
"""
# The vulnerable code used a check similar to:
# `if requested_dag_id.lstrip(authorized_dag_id_from_jwt) != requested_dag_id:`
# which was flawed because `lstrip` removes any character from a set, not a prefix.
# The corrected logic uses a strict equality check.
if authorized_dag_id_from_jwt == requested_dag_id:
return True
return FalsePayload
curl "http://<airflow-log-server>/log/_dag_secret/some_task/some_execution_date/1" -H "Authorization: Bearer <JWT_issued_for_dag_a>"
Cite this entry
@misc{vaitp:cve202645426,
title = {{Airflow log server auth bypass allows reading logs from other DAGs.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-45426},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-45426/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
