Skip to content

fix(discovery): store env discovery cache per-user, not in world-writable tmp#1011

Open
OfficialAbhinavSingh wants to merge 1 commit into
huggingface:mainfrom
OfficialAbhinavSingh:fix/discovery-cache-local-user
Open

fix(discovery): store env discovery cache per-user, not in world-writable tmp#1011
OfficialAbhinavSingh wants to merge 1 commit into
huggingface:mainfrom
OfficialAbhinavSingh:fix/discovery-cache-local-user

Conversation

@OfficialAbhinavSingh

@OfficialAbhinavSingh OfficialAbhinavSingh commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

EnvironmentDiscovery stored its cache at a fixed, world-writable path in the shared temp directory: tempfile.gettempdir() / "openenv_discovery_cache.json". On a multi-user host, another local user can pre-create that file with attacker-chosen client_module_path / client_class_name; the victim's discover(use_cache=True) (the default) then loads it and rebuilds EnvironmentInfo, and a later get_client_class() / get_action_class() / get_observation_class() calls importlib.import_module(client_module_path) + getattr(...) on the attacker-supplied names. This moves the cache to a per-user location and refuses to trust a foreign/world-writable cache file.

Type of Change

  • Bug fix

Alignment Checklist

  • I have read .claude/docs/PRINCIPLES.md and this PR aligns with our principles
  • I have checked .claude/docs/INVARIANTS.md and no invariants are violated
  • I have run lint (ruff format --check, ruff check, usort check) and the discovery/auto-env tests — all pass

RFC Status

  • Not required (bug fix, no public API change)

Details

src/openenv/auto/_discovery.py:

self._cache_file = Path(tempfile.gettempdir()) / "openenv_discovery_cache.json"  # shared, world-writable, fixed name

_load_cache() json.loads that file and rebuilds EnvironmentInfo(**env_data) with no ownership or integrity check; _save_cache() writes it with default permissions. The trust issue is CWE-377 (insecure temp file) escalating to CWE-427 (uncontrolled load path) via import_module.

Fix (three small, layered changes):

  • Per-user location_default_cache_file() returns $XDG_CACHE_HOME/openenv/… or ~/.cache/openenv/discovery_cache.json, removing the shared-writable premise entirely.
  • Trust check on load_is_trusted_cache_file() (POSIX) requires the file be owned by the current user and not group/other-writable; otherwise _load_cache() logs and returns None (falls back to live discovery). No-op on non-POSIX.
  • Restrictive write_save_cache() creates the parent dir and chmod 0o600s the file.

Behavior is unchanged for the normal single-user case; clear_cache() and the cache round-trip work exactly as before.

Test Plan

New TestCacheSecurity class in tests/envs/test_discovery.py:

  • test_cache_file_is_per_user_not_shared_tmp — path is not under the shared temp dir.
  • test_world_writable_cache_is_not_trusted / test_owner_only_cache_is_trusted — the guard.
  • test_load_cache_ignores_world_writable_file — a planted world-writable cache pointing client_module_path at os (→ system) is ignored, not loaded.
  • test_saved_cache_is_owner_only — saved file is 0600 and round-trips.

Verified (synthetic): a world-writable planted cache → _load_cache() returns None (logs "Ignoring discovery cache … not owned by the current user"); legit own-file round-trip loads and is 0600. 101 passed, 8 skipped across test_discovery.py + test_auto_env.py; ruff format --check, ruff check, usort check clean.

Claude Code Review

Self-reviewed: layered fix (relocate + guard + perms), POSIX-gated so Windows behavior is unchanged, no public API change, no new dependency, docstrings in HF doc-builder format.


Note

Medium Risk
Security fix around dynamic imports from cached metadata; behavior is unchanged for normal single-user caches but multi-user hosts gain stricter trust rules on POSIX.

Overview
Fixes a local privilege / cache poisoning issue where EnvironmentDiscovery wrote and read a fixed JSON cache under the shared temp directory, so another user could plant client_module_path / class names that later drive import_module.

The cache path moves to $XDG_CACHE_HOME/openenv/discovery_cache.json (or ~/.cache/openenv/…). On load, _is_trusted_cache_file (POSIX) requires owner-only, non–group/other-writable files; untrusted caches are ignored and discovery falls back to live package scan. On save, the parent dir is created and the file is chmod 0o600 on POSIX.

Adds TestCacheSecurity covering per-user path, trust checks, ignoring planted world-writable cache, and owner-only saved files.

Reviewed by Cursor Bugbot for commit 81dfc72. Bugbot is set up for automated code reviews on this repo. Configure here.

The discovery cache lived at a fixed, world-writable path in the shared temp
directory (tempfile.gettempdir()/openenv_discovery_cache.json). On a multi-user
host another local user could pre-create that file with attacker-chosen
client_module_path/client_class_name; the victim's discover(use_cache=True)
would load it and later importlib.import_module() the attacker-named module
(CWE-377 insecure temp file + CWE-427 uncontrolled load path).

- Move the cache to a per-user directory ($XDG_CACHE_HOME or ~/.cache/openenv).
- Ignore a cache file not owned by the current user or writable by group/others
  (POSIX) before loading it.
- Write the cache with 0600 permissions.

Adds regression tests covering the per-user location, the ownership/permission
guard, and that a world-writable planted cache is ignored.
Copilot AI review requested due to automatic review settings July 24, 2026 09:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants