From 12216196f5953a2778cb5350f3d2d36a547f5664 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Thu, 6 Aug 2026 23:40:46 +0800 Subject: [PATCH] emrg: Extend _redact to mask inline credentials in string values (sk-/ghp_/Bearer/JWT/base64-JSON) --- Agent.md | 2 +- README.md | 2 +- emrg/server/daemon.py | 39 ++++++++++++++++++++++++++++++++++++++- tests/test_daemon.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/Agent.md b/Agent.md index 871c8b76..ac3fc70e 100644 --- a/Agent.md +++ b/Agent.md @@ -93,7 +93,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design: pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg ``` -Python: `uv run pytest tests/ -v` (484) — import check: `uv run python -c "from emrg.client.app import run_client"` +Python: `uv run pytest tests/ -v` (488) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (86: 22 daemon_client + 22 app-commands + 17 renderer smoke + 15 i18n + 7 integration + 3 commands) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js` CI: `uv run pytest` + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文) diff --git a/README.md b/README.md index 1043c6fe..3133e4bb 100644 --- a/README.md +++ b/README.md @@ -276,7 +276,7 @@ EMRG doesn't just keep up — it catches up on its own. git clone https://github.com/argszero/emrg.git cd emrg uv sync # install deps -uv run pytest tests/ -v # run tests (currently 484 items) +uv run pytest tests/ -v # run tests (currently 488 items) uv run python -m emrg # launch TUI # CI includes actionlint workflow gate (#444): workflow parse errors fail PR CI diff --git a/emrg/server/daemon.py b/emrg/server/daemon.py index 068432e6..e8e16ecd 100644 --- a/emrg/server/daemon.py +++ b/emrg/server/daemon.py @@ -43,14 +43,49 @@ # ── 日志脱敏(rant 2026-08-06T10:21:26)──────────────────────────── # tool call 参数可能含 api_key/token/authorization/password 等敏感字段, # 递归替换值为 ***,避免 emrgd.log 泄露凭据。 +# 除按键名脱敏外,字符串值内联的凭据模式(sk-*/ghp_*/Bearer/JWT/base64-JSON) +# 也会被遮蔽——防止 bash command 里 `export OPENAI_API_KEY=sk-...` 或 +# base64 编码的 access_token 整体泄露(跨项目教训:明文正则匹配不到编码形式)。 _SENSITIVE_KEY_SUBSTRINGS = ( "api_key", "token", "authorization", "password", "secret", "api-key", "auth", "credential", "key", ) +import re as _re + +# 字符串值内联凭据模式(保守匹配,宁多勿漏) +_INLINE_SECRET_PATTERNS = ( + _re.compile(r"(sk-[A-Za-z0-9_\-]{8,})"), # OpenAI/DeepSeek/Anthropic 密钥 + _re.compile(r"(gh[pousr]_[A-Za-z0-9]{20,})"), # GitHub PAT / OAuth / gist token + _re.compile(r"(xox[baprs]-[A-Za-z0-9\-]{10,})"), # Slack token + _re.compile(r"(AKIA[0-9A-Z]{16})"), # AWS access key id + _re.compile(r"(Bearer\s+[A-Za-z0-9\-._~+/]+=*)", _re.IGNORECASE), # Bearer 令牌 + _re.compile(r"(Authorization\s*[:=]\s*[A-Za-z0-9\-._~+/]+=*)", _re.IGNORECASE), + _re.compile(r"((api[_-]?key|apikey|password|passwd|secret|token)\s*[:=]\s*[^\s,;\"']+)", _re.IGNORECASE), + # JWT:三段 base64url(eyJ... 开头)—— 一段即泄露签名密钥 + _re.compile(r"(eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+)"), +) + + +def _redact_string(s: str) -> str: + """遮蔽字符串值内联的凭据模式;base64-JSON 含敏感键时整段遮蔽。""" + out = s + for pat in _INLINE_SECRET_PATTERNS: + out = pat.sub("***", out) + # base64 编码的 JSON(跨项目教训:access_token 以编码形式进日志,明文正则匹配不到) + for b64 in _re.findall(r"[A-Za-z0-9+/]{40,}={0,2}", out): + try: + import base64 + decoded = base64.b64decode(b64, validate=True) + if any(k in decoded for k in (b"access_token", b"api_key", b"apikey", b"authorization", b"password", b"secret")): + out = out.replace(b64, "***") + except Exception: + continue + return out + def _redact(value): - """递归脱敏 dict/list 中的敏感字段值(就地不修改原对象)。""" + """递归脱敏 dict/list 中的敏感字段值与字符串内联凭据(不修改原对象)。""" if isinstance(value, dict): return { k: ("***" if any(s in k.lower() for s in _SENSITIVE_KEY_SUBSTRINGS) @@ -59,6 +94,8 @@ def _redact(value): } if isinstance(value, list): return [_redact(v) for v in value] + if isinstance(value, str): + return _redact_string(value) return value from emrg.tools import ToolRegistry diff --git a/tests/test_daemon.py b/tests/test_daemon.py index 5e22f457..0f784332 100644 --- a/tests/test_daemon.py +++ b/tests/test_daemon.py @@ -514,3 +514,46 @@ def test_rant_field_order(tmp_path, monkeypatch): assert entry["project"] == "emrg" assert entry["status"] == "pending" assert entry["message"] == "test rant message" + + +# ── _redact 日志脱敏(rant 10:21 + 跨项目 base64 教训)────────────── + + +def test_redact_masks_sensitive_keys(): + """按键名脱敏(原有行为)。""" + from emrg.server.daemon import _redact + assert _redact({"api_key": "sk-abc", "model": "m"}) == {"api_key": "***", "model": "m"} + assert _redact([{"token": "x"}, "plain"]) == [{"token": "***"}, "plain"] + + +def test_redact_inline_secrets_in_strings(): + """字符串值内联凭据(sk-/ghp_/Bearer/JWT)被遮蔽。""" + from emrg.server.daemon import _redact + # bash command 内联 API key + assert "sk-1234567890abcdef" not in str(_redact({"command": "export OPENAI_API_KEY=sk-1234567890abcdef; curl x"})) + # GitHub token 嵌在 URL + assert "ghp_" not in str(_redact({"command": "git push https://x-access-token:ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890@github.com/r.git"})) + # Bearer + JWT + jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U" + assert "eyJ" not in str(_redact({"text": f"Authorization: Bearer {jwt}"})) + + +def test_redact_base64_encoded_token_blob(): + """base64 编码的 access_token JSON 被整段遮蔽(跨项目教训:明文正则匹配不到编码形式)。""" + import base64 + import json as _json + from emrg.server.daemon import _redact + blob = base64.b64encode(_json.dumps({"access_token": "super-secret"}).encode()).decode() + out = _redact({"command": f"curl -d {blob} http://x"}) + assert "***" in out["command"] + assert "super-secret" not in out["command"] + assert blob not in out["command"] + + +def test_redact_no_false_positive_on_normal_strings(): + """普通字符串/短 sk- 前缀不被误伤。""" + from emrg.server.daemon import _redact + assert _redact({"command": "echo hello world", "path": "/tmp/a.txt"}) == { + "command": "echo hello world", "path": "/tmp/a.txt"} + # 短密钥(<8 位)不匹配 sk- 模式 → 保留 + assert "sk-abc" in _redact({"command": "echo sk-abc"})["command"]