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 8400915021a Mask the whole secret when another secret is its prefix 
(#76142)
8400915021a is described below

commit 8400915021ab0ef1c6cb0a369d737ccd8f4865ed
Author: Aaron Thomas <[email protected]>
AuthorDate: Sun Nov 1 18:15:51 2026 +0530

    Mask the whole secret when another secret is its prefix (#76142)
    
    The masker joins every registered secret into one regex alternation, which
    takes the first alternative that matches rather than the longest. When one
    secret is a prefix of another, the order of the set-backed patterns decided
    whether the longer secret was fully masked or had its tail left in the
    output, so the leak varied from process to process with the hash seed.
    
    Compile the alternation longest-first so the longest registered secret
    always wins.
---
 .../src/airflow_shared/secrets_masker/secrets_masker.py       |  4 +++-
 .../tests/secrets_masker/test_secrets_masker.py               | 11 +++++++++++
 2 files changed, 14 insertions(+), 1 deletion(-)

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 178b451c10b..c2d2a744d59 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
@@ -638,7 +638,9 @@ class SecretsMasker(logging.Filter):
                         self.patterns.add(pattern)
                         new_mask = True
             if new_mask:
-                self.replacer = re.compile("|".join(self.patterns))
+                # Regex alternation takes the first alternative that matches, 
not the longest, so
+                # longer secrets must come first or a secret that is a prefix 
of another leaves its tail.
+                self.replacer = re.compile("|".join(sorted(self.patterns, 
key=len, reverse=True)))
 
         elif isinstance(secret, collections.abc.Iterable):
             for v in secret:
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 f993a723eba..01d9142da1a 100644
--- a/shared/secrets_masker/tests/secrets_masker/test_secrets_masker.py
+++ b/shared/secrets_masker/tests/secrets_masker/test_secrets_masker.py
@@ -638,6 +638,17 @@ class TestSecretsMasker:
 
         assert filt.redact(value, name) == expected
 
+    def test_redact_masks_whole_secret_when_another_secret_is_its_prefix(self):
+        secret = "hunter2-production-key"
+        filt = SecretsMasker()
+        configure_secrets_masker_for_test(filt)
+        # ``patterns`` is a set, so every prefix is registered to make the 
result independent of its
+        # iteration order.
+        for length in range(5, len(secret) + 1):
+            filt.add_mask(secret[:length])
+
+        assert filt.redact(f"token={secret};") == "token=***;"
+
     @pytest.mark.parametrize(
         ("name", "value", "expected"),
         [

Reply via email to