fix(approvals): match denied executable paths behind shell prefixes

Adapt the command-position, bounded-candidate and launcher-option work from
embwl0x's #76063 to the current detection owner, then add executable basename
projection from Rohith Pariki's #104338. Parse raw quote state before applying
existing text normalization so quoted arguments do not become commands.

Cover shell payloads and literal env split-string carriers, retain path-specific
rules and whole-command globs, and document the supported normalization rather
than claiming an OS capability sandbox. Related: #104308, #76037, #76063,
#104338, #78521, #86711. No automatic closing directives: the older carriers
also contain broader case syntax and git-option work not included here.

Co-authored-by: embwl0x <embwl0x@users.noreply.github.com>
Co-authored-by: Rohith Pariki <rohithpariki@gmail.com>
This commit is contained in:
Teknium
2026-09-07 13:07:37 -07:00
parent 4810074d73
commit 58faa10134
5 changed files with 254 additions and 24 deletions
+44
View File
@@ -0,0 +1,44 @@
"""Offline production-dispatch receipt; never invokes a privileged binary.
Run with the shared Python environment and a source tree argument to compare
base/head. The executable named sudo is an owned temporary marker script.
"""
import json
import os
from pathlib import Path
import shlex
import sys
import tempfile
source = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix="hermes-deny-dispatch-") as directory:
home = Path(directory)
os.environ.clear()
# The terminal's own NOPASSWD probe must also resolve to our inert fixture.
os.environ.update(PATH=directory + ":/usr/bin:/bin", HOME=directory, HERMES_HOME=directory,
HERMES_INTERACTIVE="1", TERMINAL_ENV="local")
(home / "config.yaml").write_text(
'approvals:\n mode: "off"\n deny: ["sudo *"]\n'
'security:\n tirith_enabled: false\n', encoding="utf-8")
executable = home / "sudo"
executable.write_text('#!/bin/sh\nprintf "OWNED_EXECUTABLE_REACHED\\n"\n', encoding="utf-8")
executable.chmod(0o700)
sys.path.insert(0, str(source))
from tools import terminal_tool # noqa: F401
from tools.registry import registry
from tools import approval_context
binary = shlex.quote(str(executable))
cases = [binary + " -n id -u", "env -i " + binary + " -n id -u",
"command " + binary + " -n id -u",
"true && " + binary + " -n id -u",
"bash -c " + shlex.quote(binary + " -n id -u"),
"printf '%s\\n' 'sudo -n id -u'"]
rows = []
for command in cases:
result = registry.dispatch("terminal", {"command": command, "workdir": directory, "timeout": 10})
if isinstance(result, str):
result = json.loads(result)
rows.append({"command": command, "result": result})
print(json.dumps({"source": str(source), "config": approval_context._get_approval_config(),
"rows": rows}, indent=2))