T09 · Insecure Skill Coding Practices
- Location
scripts/tushare_stock.py:23- Finding
Predictable and Symlink-Unsafe Shared Cache Path
- Content
View full analysis
Path: CACHE_DIR.mkdir(parents=True, exist_ok=True) return CACHE_DIR / name def load_stock_cache() -> list[dict] | None: path = cache_path("stock_basic_cache.json") if not path.exists(): return None age = time_since(path) if age > timedelta(hours=24): return None return json.loads(path.read_text(encoding="utf-8")) ``` ```python if not frames: return [] merged = pd.concat(frames, ignore_index=True).drop_duplicates(subset=["ts_code"]) merged = merged.fillna("") rows = merged.to_dict(orient="records") cache_path("stock_basic_cache.json").write_text(json.dumps(rows, ensure_ascii=False), encoding="utf-8") return rows ``` ### Technical Analysis The default cache directory is a predictable, shared path under `/tmp`. The implementation creates the directory with `exist_ok=True` but does not verify: - Whether the directory is owned by the current user. - Whether the directory or cache file is a symbolic link. - Whether the cache file is a regular file. - Whether directory and file permissions prevent access by other local users. - Whether the final write is atomic. Both `read_text()` and `write_text()` follow symbolic links. An attacker with local access could pre-create `/tmp/tushare_stock_skill`, place a crafted `stock_basic_cache.json` in it, or arrange a link to another path. The program would then trust the attacker-controlled cache when resolving stock names or follow the selected path during a cache write. The cached information is public stock metadata rather than credentials. Consequently, this issue does not directly expose ...[truncated 1583 chars]- Remediation
View remediation
None: CACHE_DIR.mkdir(mode=0o700, parents=True, exist_ok=True) stat = CACHE_DIR.lstat() if CACHE_DIR.is_symlink() or stat.st_uid != os.geteuid(): raise RuntimeError("Unsafe cache directory") def atomic_cache_write(path: Path, content: str) -> None: initialize_cache_dir() fd, temporary_name = tempfile.mkstemp(dir=CACHE_DIR) try: os.fchmod(fd, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as handle: handle.write(content) handle.flush() os.fsync(handle.fileno()) os.replace(temporary_name, path) finally: if os.path.exists(temporary_name): os.unlink(temporary_name) ``` ]]>
