fix(channels): align write-path secret validation with read-path redaction

_validate_config_security used the narrow exact-match SECRET_FIELD_PATTERNS
while _redact_config_secrets was hardened to also catch *_token/*_key/*_secret/
*_password suffix keys. This closed the read leak but still let inline
app_token/signing_secret/client_secret values be stored on write. Route both
paths through the shared _is_secret_key helper so suffix secrets are rejected
at write time too, and add regression tests.

Co-authored-by: Mervin Praison <[email protected]>
This commit is contained in:
praisonai-triage-agent[bot]andMervin Praison authored and MervinPraison committed 2026-07-09 17:00:15 +01:00
1 parent 0626d0bb96
commit 971033b905
2 files changed
+32 -17

No files matched your search

+18 -17
View File
@@ -76,23 +76,6 @@ SECRET_FIELD_PATTERNS = {
"private_key", "access_token", "refresh_token", "webhook_secret"
}
def _validate_config_security(config: Dict[str, Any]) -> Optional[str]:
"""Validate channel config doesn't contain inline secrets.
Returns error message if secrets detected, None if safe.
"""
if not isinstance(config, dict):
return None
for key, value in config.items():
if key.lower() in SECRET_FIELD_PATTERNS:
if isinstance(value, str) and value and not value.startswith("env:"):
# Check for common secret patterns
if (value.startswith(("sk-", "xoxb-", "xoxp-", "xapp-", "am_", "bot")) or
len(value) > 20):
return (f"Detected inline secret in '{key}'. Use env reference: "
f"'{key}_ref': 'env:YOUR_ENV_VAR'")
return None
# Suffixes that identify secret-bearing config keys not covered by the
# exact-match SECRET_FIELD_PATTERNS set (e.g. app_token, signing_secret).
_SECRET_KEY_SUFFIXES = ("_token", "_key", "_secret", "_password")
@@ -104,6 +87,24 @@ def _is_secret_key(key: str) -> bool:
return lowered in SECRET_FIELD_PATTERNS or lowered.endswith(_SECRET_KEY_SUFFIXES)
def _validate_config_security(config: Dict[str, Any]) -> Optional[str]:
"""Validate channel config doesn't contain inline secrets.
Returns error message if secrets detected, None if safe.
"""
if not isinstance(config, dict):
return None
for key, value in config.items():
if _is_secret_key(key):
if isinstance(value, str) and value and not value.startswith("env:"):
# Check for common secret patterns
if (value.startswith(("sk-", "xoxb-", "xoxp-", "xapp-", "am_", "bot")) or
len(value) > 20):
return (f"Detected inline secret in '{key}'. Use env reference: "
f"'{key}_ref': 'env:YOUR_ENV_VAR'")
return None
def _redact_config_secrets(config: Dict[str, Any]) -> Dict[str, Any]:
"""Redact secret values in config for API responses.
+14
View File
@@ -18,6 +18,7 @@ from praisonaiui.features import channels as channels_mod
from praisonaiui.features.channels import (
ChannelsFeature,
_redact_config_secrets,
_validate_config_security,
)
_DISCORD_TOKEN = "MTQ" + "a1b2c3d4e5f6g7h8i9j0k1l2"
@@ -101,6 +102,19 @@ def test_redact_is_non_mutating():
assert original["bot_token"] == _DISCORD_TOKEN
# ── Write path rejects inline secrets on suffix keys ─────────────────
def test_validate_rejects_inline_suffix_secret():
err = _validate_config_security({"app_token": "xapp-1-" + "a" * 30})
assert err is not None
assert "app_token" in err
def test_validate_allows_env_reference_suffix_key():
assert _validate_config_security({"signing_secret": "env:SLACK_SIGNING_SECRET"}) is None
# ── Read handlers must not leak ──────────────────────────────────────