mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
fix(redact): skip env-assignment redaction for programmatic env lookups
'KEY=os.getenv(...)' / 'os.environ[...]' / 'process.env.X' values are variable-name references in code snippets, not leaked secrets. Masking them corrupted pasted code in prose/log contexts (issue #2852): ha_token=os.getenv('HOMEASSISTANT_TOKEN') -> ha_token=os.get...EN'). Skip these values inside _redact_env, which covers all three passes that share the closure (_ENV_ASSIGN_RE, _CFG_DOTTED_RE, _CFG_ANCHORED_RE). Real secret values are still masked. Salvage of PR #2852-fix #2854 — the PR's own placement (an unconditional pass before the code_file gate) would have reintroduced the code-file false-positive class; the skip is applied inside the existing gated pass instead. Tests adapted from the PR. Co-authored-by: crazywriter1 <sampiyonyus@gmail.com>
This commit is contained in:
parent
e670d9cdd6
commit
9e872db7d7
2 changed files with 59 additions and 0 deletions
|
|
@ -137,6 +137,14 @@ _ENV_ASSIGN_RE = re.compile(
|
|||
# The colon-form URL guard (skip when ``://`` present) lives at the call site.
|
||||
_SECRET_CFG_NAMES = r"(?:api[ _.\-]?key|token|secret|passwd|password|credential|auth)"
|
||||
_CFG_VALUE = r"(['\"]?)([^\s&]+?)\2(?=[\s&]|$)"
|
||||
|
||||
# Programmatic env lookups (``os.getenv(...)``, ``os.environ[...]``,
|
||||
# ``os.environ.get(...)``, ``process.env.X``, ``$ENV{X}``) reference variable
|
||||
# *names*, not secret values. When one appears as the VALUE of a KEY=... match
|
||||
# it's a code snippet, not a leaked secret — skip redaction (issue #2852).
|
||||
_ENV_LOOKUP_VALUE_RE = re.compile(
|
||||
r"^(?:os\.(?:getenv|environ)|process\.env|\$ENV\{)"
|
||||
)
|
||||
# Namespaced (dotted) key: the secret word may sit anywhere in a dotted path.
|
||||
_CFG_DOTTED_RE = re.compile(
|
||||
rf"((?:[A-Za-z0-9_\-]+\.)+[A-Za-z0-9_.\-]*{_SECRET_CFG_NAMES}[A-Za-z0-9_.\-]*"
|
||||
|
|
@ -541,6 +549,11 @@ def redact_sensitive_text(
|
|||
if "=" in text:
|
||||
def _redact_env(m):
|
||||
name, quote, value = m.group(1), m.group(2), m.group(3)
|
||||
# Programmatic env lookups reference variable *names*, not
|
||||
# secret values — masking them corrupts code snippets in
|
||||
# prose/log contexts (issue #2852): ``KEY=os.getenv('X')``.
|
||||
if _ENV_LOOKUP_VALUE_RE.match(value):
|
||||
return m.group(0)
|
||||
return f"{name}={quote}{_mask_token(value)}{quote}"
|
||||
text = _ENV_ASSIGN_RE.sub(_redact_env, text)
|
||||
# Lowercase/dotted config keys (issue #16413). Skip URLs entirely —
|
||||
|
|
|
|||
|
|
@ -124,6 +124,52 @@ class TestEnvAssignments:
|
|||
assert "mypassword" not in result
|
||||
|
||||
|
||||
class TestEnvLookupPreserved:
|
||||
"""Programmatic env var lookups must not be corrupted (issue #2852)."""
|
||||
|
||||
def test_os_getenv_single_quote_uppercase_key(self):
|
||||
text = "MY_API_KEY=os.getenv('OPENAI_API_KEY')"
|
||||
assert redact_sensitive_text(text, force=True) == text
|
||||
|
||||
def test_os_getenv_lowercase_config_key(self):
|
||||
text = "ha_token=os.getenv('HOMEASSISTANT_TOKEN')"
|
||||
assert redact_sensitive_text(text, force=True) == text
|
||||
|
||||
def test_os_getenv_double_quote(self):
|
||||
text = 'API_TOKEN=os.getenv("MY_API_TOKEN")'
|
||||
assert redact_sensitive_text(text, force=True) == text
|
||||
|
||||
def test_os_environ_get(self):
|
||||
text = "HA_TOKEN=os.environ.get('HOMEASSISTANT_TOKEN')"
|
||||
assert redact_sensitive_text(text, force=True) == text
|
||||
|
||||
def test_os_environ_bracket(self):
|
||||
text = "MY_SECRET=os.environ['MY_SECRET']"
|
||||
assert redact_sensitive_text(text, force=True) == text
|
||||
|
||||
def test_process_env(self):
|
||||
text = "api_key=process.env.API_KEY"
|
||||
assert redact_sensitive_text(text, force=True) == text
|
||||
|
||||
def test_real_env_value_still_redacted(self):
|
||||
text = "HOMEASSISTANT_TOKEN=eyJhbGciOiJIUzI1NiJ9.abc123.xyz"
|
||||
result = redact_sensitive_text(text, force=True)
|
||||
assert "eyJhbGciOiJIUzI1NiJ9" not in result
|
||||
|
||||
def test_real_lowercase_value_still_redacted(self):
|
||||
text = "password=hunter2hunter2"
|
||||
result = redact_sensitive_text(text, force=True)
|
||||
assert "hunter2hunter2" not in result
|
||||
|
||||
def test_multiline_prose_with_code_snippet(self):
|
||||
text = """Set it up like this:
|
||||
HA_TOKEN=os.getenv('HOMEASSISTANT_TOKEN')
|
||||
if not HA_TOKEN:
|
||||
raise ValueError('Missing credentials')"""
|
||||
result = redact_sensitive_text(text, force=True)
|
||||
assert "os.getenv('HOMEASSISTANT_TOKEN')" in result
|
||||
|
||||
|
||||
class TestJsonFields:
|
||||
def test_json_api_key(self):
|
||||
text = '{"apiKey": "sk-proj-abc123def456ghi789jkl012"}'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue