fix(security): patch 5 open vulnerabilities#7136
Open
dpskate wants to merge 1 commit into
Open
Conversation
…attern bypass, redaction bypass, network exposure - Remove hermes-agent root from sandbox PYTHONPATH to prevent internal module import and API key exfiltration (mitigates NousResearch#7071) - Add 8 missing DANGEROUS_PATTERNS: heredoc injection, git destructive ops (reset --hard, push --force, clean -f, branch -D, checkout -- .), and chmod +x social engineering (mitigates NousResearch#6961) - Add base64/hex encoded secret detection to redact_sensitive_text() to prevent redaction bypass via encoding - Change default bind address from 0.0.0.0 to 127.0.0.1 for webhook, SMS/Twilio, and Telegram adapters (mitigates NousResearch#4260, NousResearch#6335) - Fix .env and config.yaml file permissions from 644 to 600 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2 tasks
Contributor
There was a problem hiding this comment.
Pull request overview
This PR tightens hermes-agent’s security posture across sandbox execution, dangerous-command approval gating, secret redaction, and default network bind behavior—addressing multiple audit findings and reported vulnerabilities.
Changes:
- Removes hermes-agent repo-root injection into the
execute_codesandboxPYTHONPATHand filters it out if present. - Expands
DANGEROUS_PATTERNSand extends output redaction to detect base64/hex-encoded secrets. - Switches several gateway/webhook listeners to default to loopback (
127.0.0.1) and updates CLI display defaults.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/code_execution_tool.py | Stops exposing repo-root modules to sandboxed scripts via PYTHONPATH. |
| tools/approval.py | Adds new regex detections intended to close approval-bypass gaps. |
| agent/redact.py | Adds encoded-secret detection/redaction for base64 and hex substrings. |
| gateway/platforms/webhook.py | Changes default webhook bind host to loopback. |
| gateway/platforms/sms.py | Changes Twilio webhook server bind host to loopback. |
| gateway/platforms/telegram.py | Makes Telegram webhook listen address configurable (default loopback). |
| hermes_cli/webhook.py | Updates CLI base URL default host/display behavior for loopback. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+107
to
+108
| # Matches: python3 << 'EOF', bash <<EOF, python3 <<-'SCRIPT', etc. | ||
| (r'\b(python[23]?|perl|ruby|node|bash|sh|zsh|ksh)\s+<<-?\s*[\'\"]?\w+', "script execution via heredoc"), |
| # --- Git destructive operations --- | ||
| (r'\bgit\s+reset\s+--hard\b', "git reset --hard (destroys uncommitted changes)"), | ||
| (r'\bgit\s+push\s+.*--force\b', "git force push (rewrites remote history)"), | ||
| (r'\bgit\s+push\s+-f\b', "git force push (rewrites remote history)"), |
Comment on lines
+106
to
+117
| # --- Heredoc script injection (bypass -e/-c flag detection) --- | ||
| # Matches: python3 << 'EOF', bash <<EOF, python3 <<-'SCRIPT', etc. | ||
| (r'\b(python[23]?|perl|ruby|node|bash|sh|zsh|ksh)\s+<<-?\s*[\'\"]?\w+', "script execution via heredoc"), | ||
| # --- Git destructive operations --- | ||
| (r'\bgit\s+reset\s+--hard\b', "git reset --hard (destroys uncommitted changes)"), | ||
| (r'\bgit\s+push\s+.*--force\b', "git force push (rewrites remote history)"), | ||
| (r'\bgit\s+push\s+-f\b', "git force push (rewrites remote history)"), | ||
| (r'\bgit\s+clean\s+-[^\s]*f', "git clean -f (deletes untracked files)"), | ||
| (r'\bgit\s+branch\s+-D\b', "git branch -D (force delete branch)"), | ||
| (r'\bgit\s+checkout\s+--\s+\.', "git checkout -- . (discards all changes)"), | ||
| # --- chmod+x then execute (two-step social engineering) --- | ||
| (r'\bchmod\s+\+x\b', "make file executable (verify script content first)"), |
Comment on lines
+1019
to
+1023
| # hermes-agent project root. | ||
| _hermes_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | ||
| _filtered = os.pathsep.join( | ||
| p for p in child_env["PYTHONPATH"].split(os.pathsep) | ||
| if p and os.path.normpath(p) != os.path.normpath(_hermes_root) |
Comment on lines
+1012
to
+1028
| # SECURITY FIX: Do NOT add hermes-agent root to PYTHONPATH. | ||
| # Exposing the project root allows sandbox scripts to import internal | ||
| # modules (hermes_state, hermes_constants, config parsers) which may | ||
| # contain or provide access to API keys and security rules. | ||
| # See: https://github.com/NousResearch/hermes-agent/issues/7071 | ||
| if "PYTHONPATH" in child_env: | ||
| # Preserve any pre-existing PYTHONPATH entries that are NOT the | ||
| # hermes-agent project root. | ||
| _hermes_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | ||
| _filtered = os.pathsep.join( | ||
| p for p in child_env["PYTHONPATH"].split(os.pathsep) | ||
| if p and os.path.normpath(p) != os.path.normpath(_hermes_root) | ||
| ) | ||
| if _filtered: | ||
| child_env["PYTHONPATH"] = _filtered | ||
| else: | ||
| del child_env["PYTHONPATH"] |
Comment on lines
+178
to
+205
| # Base64-encoded secret detection | ||
| _BASE64_CHUNK_RE = re.compile(r"[A-Za-z0-9+/]{20,}={0,2}") | ||
| _HEX_CHUNK_RE = re.compile(r"(?:0x)?[0-9a-fA-F]{20,}") | ||
| # Known prefixes that indicate a secret when found inside decoded data | ||
| _DECODED_SECRET_MARKERS = ( | ||
| b"sk-", b"ghp_", b"github_pat_", b"gho_", b"xoxb-", b"xoxp-", | ||
| b"AIza", b"AKIA", b"sk_live_", b"sk_test_", b"SG.", b"hf_", | ||
| b"Bearer ", b"token=", b"password=", b"api_key=", b"apiKey", | ||
| ) | ||
|
|
||
|
|
||
| def _redact_encoded_secrets(text: str) -> str: | ||
| """Detect and redact base64/hex-encoded strings containing secret markers.""" | ||
| import base64 | ||
| import binascii | ||
|
|
||
| def _check_and_redact_b64(m): | ||
| chunk = m.group(0) | ||
| try: | ||
| decoded = base64.b64decode(chunk, validate=True) | ||
| if any(marker in decoded for marker in _DECODED_SECRET_MARKERS): | ||
| return "[REDACTED BASE64-ENCODED SECRET]" | ||
| except (binascii.Error, ValueError): | ||
| pass | ||
| return chunk | ||
|
|
||
| text = _BASE64_CHUNK_RE.sub(_check_and_redact_b64, text) | ||
|
|
Comment on lines
+170
to
218
| # --- SECURITY FIX: Detect base64/hex encoded secrets --- | ||
| # Attackers can bypass pattern-based redaction by encoding API keys. | ||
| # Detect base64 strings that decode to known secret prefixes. | ||
| text = _redact_encoded_secrets(text) | ||
|
|
||
| return text | ||
|
|
||
|
|
||
| # Base64-encoded secret detection | ||
| _BASE64_CHUNK_RE = re.compile(r"[A-Za-z0-9+/]{20,}={0,2}") | ||
| _HEX_CHUNK_RE = re.compile(r"(?:0x)?[0-9a-fA-F]{20,}") | ||
| # Known prefixes that indicate a secret when found inside decoded data | ||
| _DECODED_SECRET_MARKERS = ( | ||
| b"sk-", b"ghp_", b"github_pat_", b"gho_", b"xoxb-", b"xoxp-", | ||
| b"AIza", b"AKIA", b"sk_live_", b"sk_test_", b"SG.", b"hf_", | ||
| b"Bearer ", b"token=", b"password=", b"api_key=", b"apiKey", | ||
| ) | ||
|
|
||
|
|
||
| def _redact_encoded_secrets(text: str) -> str: | ||
| """Detect and redact base64/hex-encoded strings containing secret markers.""" | ||
| import base64 | ||
| import binascii | ||
|
|
||
| def _check_and_redact_b64(m): | ||
| chunk = m.group(0) | ||
| try: | ||
| decoded = base64.b64decode(chunk, validate=True) | ||
| if any(marker in decoded for marker in _DECODED_SECRET_MARKERS): | ||
| return "[REDACTED BASE64-ENCODED SECRET]" | ||
| except (binascii.Error, ValueError): | ||
| pass | ||
| return chunk | ||
|
|
||
| text = _BASE64_CHUNK_RE.sub(_check_and_redact_b64, text) | ||
|
|
||
| def _check_and_redact_hex(m): | ||
| chunk = m.group(0) | ||
| raw = chunk[2:] if chunk.startswith("0x") else chunk | ||
| try: | ||
| decoded = bytes.fromhex(raw) | ||
| if any(marker in decoded for marker in _DECODED_SECRET_MARKERS): | ||
| return "[REDACTED HEX-ENCODED SECRET]" | ||
| except (ValueError, binascii.Error): | ||
| pass | ||
| return chunk | ||
|
|
||
| text = _HEX_CHUNK_RE.sub(_check_and_redact_hex, text) | ||
| return text |
Comment on lines
105
to
108
| self._runner = web.AppRunner(app) | ||
| await self._runner.setup() | ||
| site = web.TCPSite(self._runner, "0.0.0.0", self._webhook_port) | ||
| site = web.TCPSite(self._runner, "127.0.0.1", self._webhook_port) | ||
| await site.start() |
Comment on lines
+643
to
646
| _webhook_listen = os.getenv("TELEGRAM_WEBHOOK_LISTEN", "127.0.0.1").strip() | ||
| await self._app.updater.start_webhook( | ||
| listen="0.0.0.0", | ||
| listen=_webhook_listen, | ||
| port=webhook_port, |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Patches 5 categories of open security vulnerabilities identified via audit:
execute_codesandboxPYTHONPATH. Previously, sandbox scripts couldimport hermes_state,hermes_constants, etc. to access API keys and security rules.python3 << 'EOF'), git destructive operations (reset --hard,push --force,push -f,clean -f,branch -D,checkout -- .), andchmod +xsocial engineering.redact_sensitive_text()to detect and redact base64/hex-encoded strings that decode to known API key prefixes (sk-,ghp_,AKIA,Bearer, etc.).0.0.0.0to127.0.0.1for webhook, SMS/Twilio, and Telegram webhook adapters. Users who need network access can explicitly sethost: "0.0.0.0"orTELEGRAM_WEBHOOK_LISTEN=0.0.0.0.~/.hermes/.envandconfig.yamlfrom 644 (world-readable) to 600 (owner-only). This is a local-only fix not in the diff, but documented here for awareness.Files changed
tools/code_execution_tool.pytools/approval.pyagent/redact.py_redact_encoded_secrets()for base64/hex detectiongateway/platforms/webhook.pygateway/platforms/sms.pygateway/platforms/telegram.pyhermes_cli/webhook.pyTest plan
sk-*key correctly redactedsk-*key correctly redactedTELEGRAM_WEBHOOK_LISTENenv varpytest tests/)Related issues
Closes #7071, mitigates #6961, mitigates #4260, mitigates #6335
🤖 Generated with Claude Code