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

potiuk pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-4-test by this push:
     new 4cd33408d9a [v3-4-test] Match camelCase keys against sensitive field 
names in masker (#74840) (#74897)
4cd33408d9a is described below

commit 4cd33408d9aa4f16ac275759d31772f374d8e368
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sat Oct 17 01:37:11 2026 +0100

    [v3-4-test] Match camelCase keys against sensitive field names in masker 
(#74840) (#74897)
    
    * Match camelCase keys against sensitive field names in masker
    
    should_hide_value_for_key() only lowercased the key and replaced
    non-word runs with "_", so camelCase keys such as privateKey,
    connectionString or webhookUrl never matched multi-word entries like
    private_key. Keys are now also split on camelCase/PascalCase word
    boundaries (including acronym runs such as APIKey) before matching.
    
    Generated-by: Claude Opus 5 following the guidelines at 
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
    
    * Rename newsfragment to the PR number
    
    Generated-by: Claude Opus 5 following the guidelines at 
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
    
    * Pin prefix, digit and nested redact cases for camelCase key masking
    (cherry picked from commit 20ccfcd7457a6a5032eefce0d18c09f9db9bcb27)
    
    
    Generated-by: Claude Opus 5
    
    Co-authored-by: Jarek Potiuk <[email protected]>
---
 airflow-core/newsfragments/74840.bugfix.rst        |  1 +
 .../secrets_masker/secrets_masker.py               | 17 ++++++--
 .../tests/secrets_masker/test_secrets_masker.py    | 49 +++++++++++++++++++++-
 3 files changed, 62 insertions(+), 5 deletions(-)

diff --git a/airflow-core/newsfragments/74840.bugfix.rst 
b/airflow-core/newsfragments/74840.bugfix.rst
new file mode 100644
index 00000000000..dff5b9aa5e7
--- /dev/null
+++ b/airflow-core/newsfragments/74840.bugfix.rst
@@ -0,0 +1 @@
+Redact values of camelCase and PascalCase keys (for example ``privateKey``, 
``connectionString`` or ``webhookUrl``) that match multi-word sensitive field 
names such as ``private_key``, ``connection_string`` or ``webhook_url``.
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 05c86073492..6de7a125534 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
@@ -79,6 +79,13 @@ DEFAULT_SENSITIVE_FIELDS = frozenset(
 SECRETS_TO_SKIP_MASKING = {"airflow"}
 """Common terms that should be excluded from masking in both production and 
tests"""
 
+# Word boundaries inside camelCase / PascalCase names: "privateKey" -> 
"private_Key",
+# "APIKey" -> "API_Key". Applied before lowercasing so multi-word sensitive 
field names
+# (``private_key``, ``connection_string``...) also match camelCase keys.
+_CAMEL_LOWER_UPPER_RE = re.compile(r"([a-z0-9])([A-Z])")
+_CAMEL_ACRONYM_RE = re.compile(r"([A-Z]+)([A-Z][a-z])")
+_NON_WORD_RE = re.compile(r"\W+")
+
 
 def should_hide_value_for_key(name):
     """
@@ -588,9 +595,13 @@ class SecretsMasker(logging.Filter):
         Name might be a Variable name, or key in conn.extra_dejson, for 
example.
         """
         if isinstance(name, str) and self.hide_sensitive_var_conn_fields:
-            name = name.strip().lower()
-            normalized = re.sub(r"\W+", "_", name)
-            return any(s in normalized for s in 
self.sensitive_variables_fields)
+            name = name.strip()
+            normalized = _NON_WORD_RE.sub("_", name.lower())
+            split = _CAMEL_ACRONYM_RE.sub(r"\1_\2", 
_CAMEL_LOWER_UPPER_RE.sub(r"\1_\2", name))
+            split = _NON_WORD_RE.sub("_", split.lower())
+            # Check both forms so fields configured without separators (e.g. 
``mysensitivekey``)
+            # keep matching camelCase keys such as ``mySensitiveKey``.
+            return any(s in normalized or s in split for s in 
self.sensitive_variables_fields)
         return False
 
     def add_mask(self, secret: JsonValue, name: str | None = None):
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 b3b40475a20..f9f880891c7 100644
--- a/shared/secrets_masker/tests/secrets_masker/test_secrets_masker.py
+++ b/shared/secrets_masker/tests/secrets_masker/test_secrets_masker.py
@@ -832,9 +832,10 @@ class TestShouldHideValueForKey:
             ("GOOGLE_APIKEY", True),
             (1, False),
             # webhook_url / bearer / dsn / auth_header / service_key in 
DEFAULT_SENSITIVE_FIELDS.
-            # Matching is case-insensitive substring on the lowercased key, so
+            # Matching is case-insensitive substring on the normalised key, so
             # snake_case variants (and underscore-bearing prefixes/suffixes) 
are
-            # covered; PascalCase / camelCase variants without underscores are 
not.
+            # covered; camelCase / PascalCase keys are split on word boundaries
+            # first (see the camelCase cases below).
             ("webhook_url", True),
             ("WEBHOOK_URL", True),
             ("slack_webhook_url", True),
@@ -855,6 +856,32 @@ class TestShouldHideValueForKey:
             ("auth.example.com/token", True),
             ("spark.executor.memory", False),
             ("spark.driver.cores", False),
+            # camelCase / PascalCase keys matching multi-word default fields.
+            ("privateKey", True),
+            ("PrivateKey", True),
+            ("privateKEY", True),
+            ("sshPrivateKey", True),
+            ("connectionString", True),
+            ("webhookUrl", True),
+            ("webhookURL", True),
+            ("slackWebhookUrl", True),
+            ("accessKey", True),
+            ("accessKeyId", True),
+            ("authHeader", True),
+            ("keyfileDict", True),
+            ("serviceAccount", True),
+            ("serviceAccountName", True),
+            ("serviceKey", True),
+            ("apiKey", True),
+            ("APIKey", True),
+            ("accessToken", True),
+            ("oauth2Token", True),
+            ("proxyPassword", True),
+            # camelCase keys that do not contain a sensitive field stay 
visible.
+            ("primaryKey", False),
+            ("connectionTimeout", False),
+            ("serviceName", False),
+            ("webhookName", False),
         ],
     )
     def test_hiding_defaults(self, key, expected_result):
@@ -870,6 +897,9 @@ class TestShouldHideValueForKey:
             ("key", "GITHUB_APIKEY", True),
             ("key, token", "TRELLO_TOKEN", True),
             ("mysecretword, mysensitivekey", "GITHUB_mysecretword", True),
+            ("mysensitivekey", "mySensitiveKey", True),
+            ("my_sensitive_key", "mySensitiveKey", True),
+            ("my_sensitive_key", "MySensitiveKey", True),
             (None, "TRELLO_API", False),
             ("token", "TRELLO_KEY", False),
             ("token, mysecretword", "TRELLO_KEY", False),
@@ -1240,6 +1270,21 @@ class TestMixedDataScenarios:
             assert redacted_data["nested"]["token"] == "***"
             assert redacted_data["nested"]["info"] == "No secrets here"
 
+    def test_nested_camel_case_keys(self):
+        secrets_masker = SecretsMasker()
+        configure_secrets_masker_for_test(secrets_masker)
+
+        data = {"connection": {"privateKey": "pk", "connectionString": "cs", 
"serviceName": "svc"}}
+
+        with patch(
+            "airflow_shared.secrets_masker.secrets_masker._secrets_masker", 
return_value=secrets_masker
+        ):
+            redacted_data = redact(data)
+
+        assert redacted_data == {
+            "connection": {"privateKey": "***", "connectionString": "***", 
"serviceName": "svc"}
+        }
+
 
 class TestSecretsMaskerMerge:
     """Test the merge functionality for restoring original values from 
redacted data."""

Reply via email to