Skip to content

fix(security): bind Meet node server to localhost and restrict token file to owner read#19382

Closed
memosr wants to merge 1 commit into
NousResearch:mainfrom
memosr:fix/google-meet-node-server-binding
Closed

fix(security): bind Meet node server to localhost and restrict token file to owner read#19382
memosr wants to merge 1 commit into
NousResearch:mainfrom
memosr:fix/google-meet-node-server-binding

Conversation

@memosr

@memosr memosr commented May 3, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

The Google Meet plugin's node server (plugins/google_meet/node/server.py)
has two security weaknesses that combine into a network-exploitable
RPC takeover.

1. Default binding to 0.0.0.0 (network exposure)

# Before (vulnerable)
def __init__(
    self,
    host: str = "0.0.0.0",   # ← exposed on all interfaces
    port: int = 18789,
    ...

The node server listens on all interfaces. On a developer's laptop on
corporate Wi-Fi, any machine on the same network can send requests
to port 18789.

2. Token file world-readable (0644)

# Before (vulnerable)
tmp = self.token_path.with_suffix(".json.tmp")
tmp.write_text(
    json.dumps({"token": tok, "generated_at": time.time()}, indent=2),
    encoding="utf-8",
)
tmp.replace(self.token_path)

Path.write_text() does not set restrictive permissions. With a typical
umask of 022, the file at ~/.hermes/workspace/meetings/node_token.json
is created as -rw-r--r-- (0644) — readable by any local user on
the machine.

Combined attack scenario

  1. Developer runs Hermes Meet node server on laptop at 10.0.0.42
  2. Another machine on the same Wi-Fi subnet finds port 18789 open
  3. Attacker reads node_token.json via any local access path (VS Code
    extension, npm package, browser exploit) — gets the 128-bit token
  4. Sends RPC commands to ws://10.0.0.42:18789 with the stolen token:
    • start_bot → bot joins target meeting
    • transcribe → reads transcripts
    • say → injects attacker text into the meeting
  5. Full RPC takeover with no authentication challenge

Fix

1. Default host = 127.0.0.1

Bind to localhost only by default. For remote deployments that
legitimately need external access, the user must pass --host 0.0.0.0
explicitly (and document the firewall rule they need).

def __init__(
    self,
    host: str = "127.0.0.1",   # ← localhost only by default
    ...

2. chmod 0o600 on token file

Restrict the token file to owner read/write only, before the atomic
replace:

tmp.write_text(...)
try:
    tmp.chmod(0o600)
except (OSError, NotImplementedError):
    # Best-effort on non-POSIX filesystems
    pass
tmp.replace(self.token_path)

The chmod is wrapped in try/except for non-POSIX filesystems
(Windows, some FUSE mounts) where it may not apply.

Type of Change

  • 🔒 Security fix (HIGH — credential exposure + network exposure)

Checklist

  • Read the Contributing Guide
  • Commit messages follow Conventional Commits
  • Restrictive default — operators must explicitly opt into network exposure
  • Best-effort chmod for cross-platform compatibility
  • No behavior change for legitimate localhost usage

@alt-glitch alt-glitch added type/security Security vulnerability or hardening comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have labels May 3, 2026
@teknium1

teknium1 commented May 4, 2026

Copy link
Copy Markdown
Contributor

Salvaged via #19597 onto current main - your commit authorship was preserved. Thanks!

@teknium1 teknium1 closed this May 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants