Back to skill

Security audit

hermes-profile-browser-isolation

Security checks for vulnerabilities and agentic risk

Overview

The skill is coherent and local, but it creates persistent logged-in browser profiles exposed through unauthenticated local debugging ports, which is a high-impact risk users should review before installing.

Install only on a trusted single-user machine or an environment where other local processes and users are not a concern. Treat each created browser profile as a sensitive logged-in session, review the assigned CDP ports, avoid preserving high-value accounts in these profiles, and do not add scheduled apply/audit tasks unless you are comfortable with recurring local process control.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (1)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/hermes_browser_provision.py:211
Finding

Unauthenticated Persistent Browser Debugging Endpoint Exposes Authenticated Sessions

Content
View full analysis

Vulnerability Details

File Location: scripts/hermes_browser_provision.py, lines 37, 159, and 211–224
Vulnerability Type: Unauthenticated Chrome DevTools Protocol exposure and insufficient endpoint identity validation
Risk Level: High

Vulnerable Code

python
BASE_PORT = int(os.environ.get("HERMES_BROWSER_BASE_PORT", "9410"))
python
cur.setdefault("dir", str(home / "browser-profiles" / name))
python
def start_instance(name, info, headless=True):
    port, udd = info["port"], info["dir"]
    if is_alive(port):
        v = cdp_get(port, "/json/version") or {}
        print(f"  [=] {name:<12} :{port} Already running  {v.get('Browser', '')}")
        return True
    Path(udd).mkdir(parents=True, exist_ok=True)
    exe = find_browser(info.get("binary", "chrome")) or find_browser("chrome")
    if not exe:
        print(f"  [!] {name}: Browser executable not found")
        return False
    args = [exe, f"--remote-debugging-port={port}", f"--user-data-dir={udd}",
            "--no-first-run", "--no-default-browser-check", "--disable-sync",
            "--disable-background-networking", "--disable-features=Translate"]

Technical Analysis

The provisioner launches Chrome-compatible browsers with a persistent user-data-dir and a fixed TCP Chrome DevTools Protocol port. These persistent profiles are expressly intended to retain authenticated login state.

CDP does not provide application-level authentication in this configuration. Although the endpoint is accessed through 127.0.0.1, loopback TCP does not impose a per-user authorization boundary. Another local process or OS user able to connect to the port can therefore interact with the browser debugging interface.

Port allocation starts at the predictable default port 9410. Before launching a browser, start_instance() calls is_alive(), which only checks whether /json/version returns parseable JSON. It d ...[truncated 2518 chars]

Remediation
View remediation

Remediation Suggestions

  1. Avoid exposing persistent authenticated browser profiles through an unauthenticated TCP debugging endpoint. Prefer a per-user IPC mechanism or an authenticated local proxy protected by OS-level access controls.
  2. Use unpredictable, per-instance endpoint assignments rather than allocating ports sequentially from a fixed default.
  3. Record the launched browser PID and verify its owner, executable path, command line, debugging port, and user-data-dir before reusing an existing endpoint.
  4. If a configured port is already occupied by a process that cannot be positively associated with the recorded instance, fail closed instead of accepting it as alive.
  5. Verify more than /json/version; correlate the CDP response with trusted process metadata and the expected profile.
  6. Apply owner-only permissions to the instance state file, Hermes configuration files, and persistent browser profile directories.
  7. Clearly document that persistent CDP profiles contain sensitive authenticated state and must not be used on a host where untrusted local users or processes can access their endpoints.
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (16)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
96% confidence
Finding

The skill explicitly instructs users to run local Python scripts that can read environment state, modify profile configuration files, launch and stop browser processes, and clean directories, yet it declares no tool scope or permission boundaries. In a skill ecosystem, missing capability declarations weakens informed consent and review, making it easier for a skill with file, shell, and possible network effects to be executed with broader privileges than users expect.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The listed trigger terms include broad phrases such as “多智能体并行” and “独立 profile” alongside a long keyword list, but the file does not define when the skill should not activate or provide negative examples. This increases the chance of matching general discussion about multi-agent work or browsers rather than a specific request to configure this skill.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The installation flow tells users to run an apply command that writes configuration files and starts browser instances, but the warning about side effects appears only later and is not prominent at the execution point. This increases the risk of users making persistent changes or launching processes without understanding the consequences, especially in multi-profile environments where multiple accounts and stored sessions are involved.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The security section understates file-system impact by claiming the skill only modifies profile configs and browser data directories, while elsewhere it says it also deletes temporary orphan directories under %TEMP%. Misstating destructive behavior is dangerous because operators may authorize execution under false assumptions, and cleanup logic targeting temp paths can cause data loss if matching is too broad or implemented incorrectly.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

This markdown file contains user-facing natural-language content only in Chinese, and there is no indication that readers may choose another language or that the document is intentionally limited to a Chinese-speaking audience. Under the policy, forcing a specific language without opt-in is a natural-language policy violation.

Content

No source excerpt is available for this finding.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
70% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · references/platform-notes.md (reported line 16)May include surrounding context.

md
- 浏览器路径:`/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`

## Linux / X11
- 降级为**进程级**:`wmctrl -lp`(`sudo apt install wmctrl`);未装则返回空 → 只做参数判据
- 浏览器路径:`/usr/bin/google-chrome`、`/usr/bin/chromium`
- headless 服务器(无 GUI):无窗口概念,`--headless` 天然满足,审计恒为 0

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/hermes_browser_audit.py (reported line 116)May include surrounding context.

python
s = ('tell application "System Events" to get unix id of every process '
         'whose visible is true')
    try:
        o = subprocess.run(["osascript", "-e", s], capture_output=True,
                           text=True, timeout=25).stdout
        return {int(x): ["(进程可见)"] for x in re.findall(r"\d+", o)}
    except Exception:

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/hermes_browser_audit.py (reported line 125)May include surrounding context.

python
def _x11():
    try:
        o = subprocess.run(["wmctrl", "-lp"], capture_output=True,
                           text=True, timeout=20).stdout
    except Exception:
        return {}

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/hermes_browser_audit.py (reported line 193)May include surrounding context.

python
print("\n[纠偏] 发现可见窗口 → 按端口重启为无头 …")
        for n in bad:
            if prov.exists():
                subprocess.run([sys.executable, str(prov), "restart", n],
                               capture_output=True, text=True, timeout=180)
                print(f"  → {n} :{bad[n]['port']} 已重启为无头")
        time.sleep(3)

Internal Network Request

Medium
Category
Server-Side Request Forgery
Confidence
70% confidence
Finding

Code issues a request to a loopback, link-local, or private-range host. This can reach internal services not meant to be exposed and is a common SSRF pivot.

Content

Scanner excerpt · scripts/hermes_browser_provision.py (reported line 124)May include surrounding context.

python
# ───────────────────────── CDP ─────────────────────────
def cdp_get(port, path, timeout=3):
    try:
        with urllib.request.urlopen(f"http://127.0.0.1:{port}{path}", timeout=timeout) as r:
            return json.loads(r.read().decode("utf-8", "replace"))
    except Exception:
        return None

Tainted flow: 'new' from pathlib.Path.read_text (line 214, file read) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · scripts/hermes_browser_provision.py (reported line 222)May include surrounding context.

python
bak = cfg_path.with_name(cfg_path.name + ".bak-" + time.strftime("%Y%m%d_%H%M%S"))
    try:
        bak.write_text(text, encoding="utf-8")
        cfg_path.write_text(new, encoding="utf-8")
    except Exception as e:
        return f"write-error: {e}"
    return "changed"

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/hermes_browser_provision.py (reported line 251)May include surrounding context.

python
else:
        kw["start_new_session"] = True
    try:
        subprocess.Popen(args, **kw)
    except Exception as e:
        print(f"  [!] {name}: 启动异常 {e}")
        return False

Internal Network Request

Medium
Category
Server-Side Request Forgery
Confidence
70% confidence
Finding

Code issues a request to a loopback, link-local, or private-range host. This can reach internal services not meant to be exposed and is a common SSRF pivot.

Content

Scanner excerpt · scripts/hermes_browser_provision.py (reported line 428)May include surrounding context.

python
n = 0
        for t in pg[keep:]:
            try:
                urllib.request.urlopen(
                    f"http://127.0.0.1:{port}/json/close/{t['id']}", timeout=3).read()
                n += 1
            except Exception:

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
88% confidence
Finding

Natural-language instructions, warnings, and usage details are presented only in Chinese, which effectively forces a specific language for users without opt-in. The file does not indicate that the skill is region-specific or provide an alternative language option.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Low
Category
Not specified by scanner
Confidence
77% confidence
Finding

The skill description emphasizes local-only browser isolation, auditing, and cleanup. However, the file also includes a WeChat contact, a public product page URL, and a remote image URL, which are not part of provisioning or auditing isolated browser instances and broaden the skill's apparent function into promotion/contacting the author.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
92% confidence
Finding

This markdown file contains user-facing operational guidance exclusively in Chinese, and there is no indication that the skill is intended only for Chinese-speaking users or a China-specific environment. Under the language/locale policy, forcing a specific language without user opt-in can be a natural-language policy violation.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.