This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 4d39a855b1c Walk nested lists/tuples/sets in secrets masker key-name 
redaction (#68422)
4d39a855b1c is described below

commit 4d39a855b1c6c3072e53b49465dc66f5a38e095a
Author: Jarek Potiuk <[email protected]>
AuthorDate: Thu Jun 25 04:22:23 2026 -0400

    Walk nested lists/tuples/sets in secrets masker key-name redaction (#68422)
    
    The secrets masker walks dicts unconditionally for key-name-based
    redaction, so a sensitive key nested in dicts is masked at any depth.
    Lists, tuples, and sets were only walked below the recursion-depth
    cutoff, so a value carrying a sensitive key wrapped in an iterable
    beyond the cutoff was returned without key-name redaction.
    
    Walk nested lists/tuples/sets unconditionally too, mirroring the dict
    handling, keeping the depth cutoff only for pattern-based string
    masking. Add regression tests for sensitive keys wrapped in lists and
    tuples past MAX_RECURSION_DEPTH.
    
    Generated-by: Claude Opus 4.8 (1M context) following the guidelines at
    
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
---
 .../secrets_masker/secrets_masker.py               | 40 +++++++++++++---------
 .../tests/secrets_masker/test_secrets_masker.py    | 14 ++++++++
 2 files changed, 37 insertions(+), 17 deletions(-)

diff --git 
a/shared/secrets_masker/src/airflow_shared/secrets_masker/secrets_masker.py 
b/shared/secrets_masker/src/airflow_shared/secrets_masker/secrets_masker.py
index 3b625b7af77..9157a91a7fb 100644
--- a/shared/secrets_masker/src/airflow_shared/secrets_masker/secrets_masker.py
+++ b/shared/secrets_masker/src/airflow_shared/secrets_masker/secrets_masker.py
@@ -369,8 +369,29 @@ class SecretsMasker(logging.Filter):
                     for dict_key, subval in item.items()
                 }
                 return to_return
-            # Avoid spending too much effort on pattern-based masking of
-            # deeply nested non-dict structures.
+            # Always walk lists/tuples/sets too, mirroring the unconditional 
dict
+            # walk above, so a sensitive key wrapped in an iterable is still
+            # caught at any nesting depth. Self-referential iterables hit 
Python's
+            # own recursion limit and are caught by the except clause below, 
which
+            # fails closed.
+            if isinstance(item, (tuple, set)):
+                # Turn set in to tuple!
+                return tuple(
+                    self._redact(
+                        subval, name=None, depth=(depth + 1), 
max_depth=max_depth, replacement=replacement
+                    )
+                    for subval in item
+                )
+            if isinstance(item, list):
+                return [
+                    self._redact(
+                        subval, name=None, depth=(depth + 1), 
max_depth=max_depth, replacement=replacement
+                    )
+                    for subval in item
+                ]
+            # The depth cutoff only bounds the work of pattern-based string
+            # masking below — key-name redaction (dicts and iterables above) is
+            # unbounded so sensitive keys fail closed at any depth.
             if depth > max_depth:
                 return item
             if isinstance(item, Enum):
@@ -393,21 +414,6 @@ class SecretsMasker(logging.Filter):
                     # the structure.
                     return self.replacer.sub(replacement, str(item))
                 return item
-            if isinstance(item, (tuple, set)):
-                # Turn set in to tuple!
-                return tuple(
-                    self._redact(
-                        subval, name=None, depth=(depth + 1), 
max_depth=max_depth, replacement=replacement
-                    )
-                    for subval in item
-                )
-            if isinstance(item, list):
-                return [
-                    self._redact(
-                        subval, name=None, depth=(depth + 1), 
max_depth=max_depth, replacement=replacement
-                    )
-                    for subval in item
-                ]
             return item
         # I think this should never happen, but it does not hurt to leave it 
just in case
         # Well. It happened (see 
https://github.com/apache/airflow/issues/19816#issuecomment-983311373)
diff --git a/shared/secrets_masker/tests/secrets_masker/test_secrets_masker.py 
b/shared/secrets_masker/tests/secrets_masker/test_secrets_masker.py
index c6990602284..b3b40475a20 100644
--- a/shared/secrets_masker/tests/secrets_masker/test_secrets_masker.py
+++ b/shared/secrets_masker/tests/secrets_masker/test_secrets_masker.py
@@ -718,6 +718,20 @@ class TestSecretsMasker:
                 {"a": {"b": {"c": {"d": {"e": {"api_key": "leaked"}}}}}},
                 {"a": {"b": {"c": {"d": {"e": {"api_key": "***"}}}}}},
             ),
+            # A sensitive key wrapped in a list beyond MAX_RECURSION_DEPTH is
+            # still redacted: the list is walked unconditionally, mirroring the
+            # unbounded dict walk above. Here the list sits at depth 6 (one 
past
+            # the cutoff), reached through the unbounded dict chain a..f.
+            (
+                {"a": {"b": {"c": {"d": {"e": {"f": [{"password": 
"leaked"}]}}}}}},
+                {"a": {"b": {"c": {"d": {"e": {"f": [{"password": 
"***"}]}}}}}},
+            ),
+            # Same for a tuple-wrapped sensitive key past MAX_RECURSION_DEPTH
+            # (a set cannot hold a dict, so only list/tuple are exercised 
here).
+            (
+                {"a": {"b": {"c": {"d": {"e": {"f": ({"token": 
"leaked"},)}}}}}},
+                {"a": {"b": {"c": {"d": {"e": {"f": ({"token": "***"},)}}}}}},
+            ),
         ],
     )
     def test_redact_sensitive_key_past_max_depth(self, val, expected):

Reply via email to