VAITP Dataset

← Back to the dataset

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.

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 False

Payload

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 ::