Skip to content

Fix cache poisoning vulnerability in IntermediateCache - #172

Closed
bhuvan-somisetty wants to merge 1 commit into
OSIPI:mainfrom
bhuvan-somisetty:fix-cache-poisoning-171
Closed

Fix cache poisoning vulnerability in IntermediateCache#172
bhuvan-somisetty wants to merge 1 commit into
OSIPI:mainfrom
bhuvan-somisetty:fix-cache-poisoning-171

Conversation

@bhuvan-somisetty

Copy link
Copy Markdown
Contributor

Fixes #171

IntermediateCache loaded its disk cache with np.load(cache_file, allow_pickle=True) from a path that's fully predictable (tempfile.gettempdir()/osipy_cache/<md5 hash>.npz, shared by every user on the box). On a shared machine that means another local user could plant a crafted .npz file there and get arbitrary code executed the moment your process called cache.get(...).

Changes

  • Load with allow_pickle=False. Cache entries only ever contain plain ndarrays and JSON-encoded strings, so pickle was never actually needed here, it was just unused attack surface.
  • Default cache directory is now scoped per user (osipy_cache_<username>) instead of one name shared by everyone.
  • On POSIX, refuse to use the cache directory if it's a symlink or owned by someone else, and lock it to 0700. No-op on Windows (no os.getuid/POSIX permission bits there).
  • Writes go to a temp file in the cache dir first and get renamed into place, so a partially written file can never be read back.

Testing

  • Added TestCachePoisoningRegression in tests/unit/common/test_caching.py: plants a malicious .npz at the exact predictable path and confirms get() returns None without executing the payload, checks the default dir is per-user scoped, and (POSIX-only, skipped on Windows) checks directory permissions, symlink rejection, and cross-user ownership rejection.
  • Full suite: pytest → 777 passed, 43 skipped (GPU/data-dependent), 6 xfailed, same as on main (verified the 8 pre-existing cli/test_config.py / cli/test_wizard.py failures are unrelated to this change, they fail identically on main too, looks like a local package-metadata issue unrelated to caching).
  • ruff check / ruff format --check clean on the changed files.
  • mypy osipy/common/caching.py: no new errors introduced (verified against main before this change); pre-existing strict-mode findings elsewhere in the file are untouched.

IntermediateCache loaded its disk cache with allow_pickle=True from a
predictable, shared path (tempfile.gettempdir()/osipy_cache), so on a
shared machine another local user could plant a crafted .npz file that
executed arbitrary code as soon as it was loaded via cache.get().

- Drop allow_pickle=True on load; cache entries are always plain
  ndarrays/JSON strings, so pickle support was unnecessary attack surface.
- Scope the default cache directory per user instead of one shared name.
- Refuse to use a cache directory that's a symlink or owned by another
  user, and restrict it to 0700 (POSIX only; no-op on Windows).
- Write cache entries to a temp file and rename into place atomically.

Fixes OSIPI#171
@bhuvan-somisetty

Copy link
Copy Markdown
Contributor Author

cc @ltorres6 @MohamedNasser8 for review

@ltorres6

Copy link
Copy Markdown
Collaborator

Thanks for your contribution. I don't think we use the caching mechanism anymore and it should simply be removed. You are welcome to take on that issue #177 if you would like.

@ltorres6 ltorres6 closed this Aug 30, 2026
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.

[BUG] IntermediateCache is vulnerable to local code execution via pickle cache poisoning

2 participants