bf8b28f27a
Today's spill/cache-writer hardening (tools/spill_safety.py, write_text_exclusive/ensure_spill_dir with O_CREAT|O_EXCL|O_NOFOLLOW) migrated tools/web_tools.py::_store_full_text() — which writes to the same cache/web directory with the same content-hash filename scheme — but left its near-identical sibling, tools/browser_tool.py::_store_full_snapshot(), on the pre-fix plain open()/write_text() pattern. A pre-planted symlink at the content-hash path redirected the write onto an arbitrary user-owned file, same as the sites that commit fixed. Reproduced live: with a symlink planted at the exact browser-snapshot-<digest>.txt path (predictable from the snapshot content hash), the pre-fix write followed the link and overwrote the link's target with the (secret-redacted but otherwise user/page-controlled) snapshot content. Fix mirrors _store_full_text's exact usage: ensure_spill_dir(private=False) + write_text_exclusive(private=False, overwrite=True) — not private since cache/web is bind-mounted into remote backends whose container UID must read it; overwrite=True because re-snapshotting the same page state legitimately reuses the same content-hash name (the overwrite path lstat-unlinks the link itself, never following it to write through). Added a regression test planting a symlink at the exact digest path and asserting the link's target is untouched (only the link itself gets safely replaced by a real file). Mutation-verified: with the fix stashed, the pre-fix code wrote the snapshot content into the symlink's target file, reproducing the vulnerability exactly.