VYPR
Unrated severityNVD Advisory· Published Jun 1, 2026

CVE-2026-42360

CVE-2026-42360

Description

Apache Airflow's rendered-template field masking bypasses nested sensitive keys when the field exceeds max_templated_field_length, exposing secrets to authenticated users.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Apache Airflow's rendered-template field masking bypasses nested sensitive keys when the field exceeds `max_templated_field_length`, exposing secrets to authenticated users.

Vulnerability

A bug in Apache Airflow's rendered-template field handling causes nested sensitive-key masking (e.g., nested password, token, secret, api_key keys inside a JSON template structure) to be bypassed when the rendered field exceeds [core] max_templated_field_length. Airflow stringifies the structure before redaction, losing the nested key context, and persists the plaintext value into rendered_fields. This affects deployments where DAG authors pass structured JSON to operators with nested sensitive keys. This is a variant of CWE-200 previously addressed for user-registered mask_secret() patterns in CVE-2025-68438; that fix did not cover the nested sensitive-keyword allowlist. Versions prior to apache-airflow 3.2.2 are vulnerable.

Exploitation

An authenticated UI/API user with permission to read rendered template fields can harvest secret values intended to be masked. The attacker needs network access to the Airflow instance and valid credentials with at least read access to rendered template fields. The condition is that the rendered template field exceeds the configured max_templated_field_length. When a DAG author passes structured JSON containing nested sensitive keys, and the rendered field is long enough to trigger truncation, Airflow stringifies the structure before applying redaction, thus exposing the plaintext nested keys in the stored rendered field.

Impact

Successful exploitation results in information disclosure of sensitive credentials (passwords, tokens, secrets, API keys) that were intended to be masked. The attacker gains access to these secrets, potentially enabling further compromise of connected systems or data. The privilege level required is that of an authenticated user with read access to rendered template fields.

Mitigation

Upgrade to apache-airflow 3.2.2 or later, which redacts rendered template fields while still structured to preserve nested-key masking on truncation [1]. Users who already upgraded for CVE-2025-68438 should additionally upgrade to cover the nested-key path. No workaround is documented; the fix is included in the referenced pull request [1].

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

1

Patches

2
4ceb0db321e2

Redact rendered template fields while still structured to preserve nested-key masking on truncation (#65906)

https://github.com/apache/airflowJarek PotiukMay 18, 2026via nvd-ref
4 files changed · +103 2
  • airflow-core/src/airflow/serialization/helpers.py+4 1 modified
    @@ -106,7 +106,10 @@ def serialize_object(obj):
         serialized = serialize_object(template_field)
     
         if len(str(serialized)) > max_length:
    -        rendered = redact(str(serialized), name)
    +        # Redact while still structured to preserve nested-key context (so values under
    +        # documented sensitive keys such as `password`, `token`, `secret`, `api_key`
    +        # are masked recursively); only stringify the redacted result for truncation.
    +        rendered = redact(serialized, name)
             return truncate_rendered_value(str(rendered), max_length)
     
         return serialized
    
  • airflow-core/tests/unit/serialization/test_helpers.py+24 0 modified
    @@ -657,3 +657,27 @@ def make_value():
         assert all(isinstance(k, str) for k in inner[float_key])
         assert "at 0x" not in str(r1)
         json.dumps(r1, sort_keys=True)
    +
    +
    +@pytest.mark.enable_redact
    +def test_serialize_template_field_masks_nested_sensitive_keys_on_truncation(monkeypatch):
    +    """Nested sensitive-key masking applies consistently across the truncation path.
    +
    +    A value under a documented sensitive key (``password``, ``token``, ``secret``,
    +    ``api_key``) is masked recursively by ``redact()`` when the structured value
    +    is walked. The oversized branch must redact while still structured so that
    +    nested-key context is preserved before stringification — otherwise the post-
    +    stringify ``redact()`` call only sees the outer field name and the recursive
    +    walker cannot reach the inner key.
    +    """
    +    monkeypatch.setenv("AIRFLOW__CORE__MAX_TEMPLATED_FIELD_LENGTH", "200")
    +
    +    nested_value = "REGRESSION-FIXTURE-NESTED-PASSWORD-VALUE"
    +    payload = {"nested": {"password": nested_value, "zz_pad": "A" * 500}}
    +
    +    result = serialize_template_field(payload, "templates_dict")
    +
    +    assert isinstance(result, str)
    +    assert "Truncated. You can change this behaviour" in result
    +    assert nested_value not in result
    +    assert "***" in result
    
  • task-sdk/src/airflow/sdk/execution_time/task_runner.py+4 1 modified
    @@ -1084,7 +1084,10 @@ def serialize_object(obj):
         serialized = serialize_object(template_field)
     
         if len(str(serialized)) > max_length:
    -        rendered = redact(str(serialized), name)
    +        # Redact while still structured to preserve nested-key context (so values under
    +        # documented sensitive keys such as `password`, `token`, `secret`, `api_key`
    +        # are masked recursively); only stringify the redacted result for truncation.
    +        rendered = redact(serialized, name)
             return truncate_rendered_value(str(rendered), max_length)
     
         return serialized
    
  • task-sdk/tests/task_sdk/execution_time/test_task_runner.py+71 0 modified
    @@ -3002,6 +3002,77 @@ def execute(self, context):
             assert env_vars_value.endswith("...")
             assert "***" in env_vars_value  # secrets are redacted before truncation
     
    +    @pytest.mark.enable_redact
    +    def test_rendered_templates_mask_nested_keys_with_truncation(
    +        self, create_runtime_ti, mock_supervisor_comms, monkeypatch
    +    ):
    +        """Nested sensitive-key masking applies consistently across the truncation path.
    +
    +        A value under a documented sensitive key (``password``, ``token``, ``secret``,
    +        ``api_key``) is masked recursively by ``redact()`` when the structured value
    +        is walked. The oversized branch must redact while still structured so that
    +        nested-key context is preserved before stringification — otherwise the post-
    +        stringify ``redact()`` call only sees the outer field name and the recursive
    +        walker cannot reach the inner key.
    +        """
    +        from airflow.sdk._shared.secrets_masker import _secrets_masker
    +
    +        # Earlier tests in this file (e.g. test_get_connection_from_context) call
    +        # mask_secret(conn.password) where the fixture's password value is the literal
    +        # "password"; that registers "password" as a regex pattern in the singleton
    +        # masker. Without isolation, str(redacted) gets that regex applied and the
    +        # dict KEY name "password" itself becomes "***", obscuring whether the
    +        # structured nested-key walk fired. Reset the regex patterns for this test
    +        # (monkeypatch restores them on teardown) so the assertion can distinguish
    +        # value-masking (what we are testing) from key-token replacement.
    +        masker = _secrets_masker()
    +        monkeypatch.setattr(masker, "patterns", set())
    +        monkeypatch.setattr(masker, "replacer", None)
    +        # The SDK masker starts with an empty sensitive-fields list in the test runtime
    +        # (settings.py has not run); register `password` explicitly so the structured
    +        # walker has something to match. Production workers get this from settings.py.
    +        monkeypatch.setattr(
    +            masker,
    +            "sensitive_variables_fields",
    +            list(masker.sensitive_variables_fields) + ["password"],
    +        )
    +
    +        nested_value = "REGRESSION-FIXTURE-NESTED-PASSWORD-VALUE"
    +
    +        class CustomOperator(BaseOperator):
    +            template_fields = ("env_vars",)
    +
    +            def __init__(self, env_vars, *args, **kwargs):
    +                super().__init__(*args, **kwargs)
    +                self.env_vars = env_vars
    +
    +            def execute(self, context):
    +                pass
    +
    +        # Nested 'password' key under enough padding to exceed default 4096-char limit.
    +        env_vars = {
    +            "DB": {"password": nested_value, "host": "db.internal", "zz_pad": "A" * 5000},
    +        }
    +
    +        task = CustomOperator(task_id="test_nested_truncation_masking", env_vars=env_vars)
    +
    +        runtime_ti = create_runtime_ti(task=task, dag_id="test_nested_truncation_masking_dag")
    +        run(runtime_ti, context=runtime_ti.get_template_context(), log=mock.MagicMock())
    +
    +        msg = next(
    +            c.kwargs["msg"]
    +            for c in mock_supervisor_comms.send.mock_calls
    +            if c.kwargs.get("msg") and getattr(c.kwargs["msg"], "type", None) == "SetRenderedFields"
    +        )
    +        env_vars_value = msg.rendered_fields["env_vars"]
    +
    +        assert isinstance(env_vars_value, str)
    +        assert env_vars_value.startswith(
    +            "Truncated. You can change this behaviour in [core]max_templated_field_length. "
    +        )
    +        assert nested_value not in env_vars_value
    +        assert "'password': '***'" in env_vars_value
    +
         @pytest.mark.enable_redact
         def test_rendered_templates_masks_secrets_in_complex_objects(
             self, create_runtime_ti, mock_supervisor_comms
    
cde4885818be

Updating release notes for 3.2.2rc3

https://github.com/apache/airflowvatsrahul1001May 26, 2026Fixed in 3.2.2via release-tag
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

2

News mentions

0

No linked articles in our index yet.