Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Trevin Chow <tmchow@users.noreply.github.com>
3.3 KiB
title, date, category, module, problem_type, component, severity, applies_when, tags
| title | date | category | module | problem_type | component | severity | applies_when | tags | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| A predictable-path cache in shared /tmp is a prompt-injection vector — ownership-check reads | 2026-06-29 | docs/solutions/best-practices/ | scratch-space | best_practice | tooling | medium |
|
|
A predictable-path cache in shared /tmp is a prompt-injection vector — ownership-check reads
Threat
A scratch file at a knowable path under world-traversable /tmp (keyed by commit SHA, repo name, username, or a digest of committed blobs) can be pre-created by a local co-tenant. A planted file can satisfy every content gate the reader applies (schema version, digest, freshness), because those gates check facts about the victim's tree, not who wrote the file. If the reader then feeds the content into an agent's context, the attacker has attacker-controlled text in the LLM prompt: indirect prompt injection.
The usual framing ("it's just cache metadata, low impact") misses this. Anything read into an LLM context is an injection sink, so "non-sensitive" data does not bound the risk when the data is instructions-adjacent. Impact is calibrated (local co-tenant plus predictable path; payload is text, not code execution), which is why the fix is a cheap check rather than abandoning shared temp.
Guidance
When a shared-/tmp file will be read back into an agent's context, path plus content gates do not prove authenticity. Verify it is yours:
- After opening,
os.fstat(fd).st_uid != os.geteuid()-> treat as a miss and re-derive. Check via the opened fd, not a pre-openstat, so a planted symlink pointing at an attacker-owned file is also rejected. Skip wheregeteuidis unavailable (non-POSIX); the shared-/tmpthreat does not apply there. - A rejected entry degrades to "derive fresh"; the cache must never be a correctness dependency.
- The write side is already safe with
tempfile.mkstemp(O_EXCL, mode0600) plusos.replace. The exposure is purely on the read path.
with open(path) as f:
geteuid = getattr(os, "geteuid", None)
if geteuid is not None and os.fstat(f.fileno()).st_uid != geteuid():
return miss()
doc = json.load(f)
Current practice layers both defenses: the per-effective-uid root from docs/solutions/developer-experience/always-on-agents-md.md (/tmp/compound-engineering-<effective-uid>/…, mode 0700, reject symlink or foreign ownership) and fstat-on-fd before content enters agent context. An earlier write-up rejected uid namespacing to keep a single shared /tmp/compound-engineering/ root; that was reversed. Do not revive the un-namespaced shared root.
Not needed for per-run mktemp -d scratch with an unguessable path consumed only within the same process, or for files never surfaced to the model.
Related
docs/solutions/developer-experience/always-on-agents-md.md(per-effective-uid/tmp/compound-engineering-<effective-uid>/plus the$TMPDIRfallback);AGENTS.md"Scratch Space" keeps the always-on pointer