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."""