From 1dcb7dce712eec964e5893b4905a7253c3a948ca Mon Sep 17 00:00:00 2001 From: Saeed Al Mansouri Date: Wed, 25 Mar 2026 15:40:55 +0400 Subject: [PATCH] fix(zoom): restrict token and config file permissions to owner-only The Zoom harness was writing OAuth tokens (tokens.json) and app credentials (config.json) to ~/.cli-anything-zoom/ without setting restrictive file permissions, leaving sensitive secrets world-readable by default. This adds owner-only permissions (0o600 for files, 0o700 for the config directory) after every write, matching the pattern already used by the AnyGen harness. On Windows, where os.chmod only controls the read-only flag, we also call icacls to strip inherited ACLs and grant full control exclusively to the current user. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../cli_anything/zoom/utils/zoom_backend.py | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/zoom/agent-harness/cli_anything/zoom/utils/zoom_backend.py b/zoom/agent-harness/cli_anything/zoom/utils/zoom_backend.py index 199fff2a2..585c5865d 100644 --- a/zoom/agent-harness/cli_anything/zoom/utils/zoom_backend.py +++ b/zoom/agent-harness/cli_anything/zoom/utils/zoom_backend.py @@ -5,6 +5,9 @@ It is the only module that makes network requests. """ import json +import os +import platform +import subprocess import time import requests from pathlib import Path @@ -25,9 +28,35 @@ TOKEN_FILE = CONFIG_DIR / "tokens.json" CONFIG_FILE = CONFIG_DIR / "config.json" +def _restrict_path(path: Path, mode: int): + """Set file/directory permissions, with icacls enforcement on Windows. + + On Unix, uses os.chmod directly. + On Windows, os.chmod only controls the read-only flag, so we also + run icacls to grant access exclusively to the current user. + """ + try: + path.chmod(mode) + except OSError: + pass + + if platform.system() == "Windows": + try: + username = os.environ.get("USERNAME", "") + if username: + subprocess.run( + ["icacls", str(path), "/inheritance:r", + "/grant:r", f"{username}:(F)"], + capture_output=True, timeout=10, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + pass # icacls not available or timed out — best effort + + def get_config_dir() -> Path: - """Get or create config directory.""" + """Get or create config directory with owner-only permissions (0o700).""" CONFIG_DIR.mkdir(parents=True, exist_ok=True) + _restrict_path(CONFIG_DIR, 0o700) return CONFIG_DIR @@ -40,10 +69,11 @@ def load_config() -> dict: def save_config(config: dict): - """Save OAuth app config.""" + """Save OAuth app config with owner-only permissions (0o600).""" get_config_dir() with open(CONFIG_FILE, "w") as f: json.dump(config, f, indent=2) + _restrict_path(CONFIG_FILE, 0o600) def load_tokens() -> dict: @@ -55,11 +85,12 @@ def load_tokens() -> dict: def save_tokens(tokens: dict): - """Save OAuth tokens to disk.""" + """Save OAuth tokens to disk with owner-only permissions (0o600).""" get_config_dir() tokens["saved_at"] = time.time() with open(TOKEN_FILE, "w") as f: json.dump(tokens, f, indent=2) + _restrict_path(TOKEN_FILE, 0o600) def get_authorize_url(client_id: str, redirect_uri: str) -> str: