Skip to content

fix(agents): prevent local attachment symlink escapes - #73

Merged
yetone merged 1 commit into
yetone:mainfrom
KIDA-MNESIA:codex/harden-local-attachment-paths
Aug 26, 2026
Merged

fix(agents): prevent local attachment symlink escapes#73
yetone merged 1 commit into
yetone:mainfrom
KIDA-MNESIA:codex/harden-local-attachment-paths

Conversation

@KIDA-MNESIA

@KIDA-MNESIA KIDA-MNESIA commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add a dedicated local attachment reader that revalidates the server-managed attachments/ key
  • require both lexical and realpath() containment under the upload root
  • open the verified canonical path with O_NOFOLLOW where supported and accept regular files only
  • route Agent text attachment extraction through that boundary
  • cover regular files, final symlink escapes, directory symlink escapes, missing files, invalid namespaces, and directories

Security context

PR #66 removed the remotely controllable URL and traversal path, but its local read boundary still relied on lexical resolve() containment. A symbolic link already present inside UPLOAD_DIR could therefore point the Agent reader outside the upload tree.

This PR closes that defense-in-depth gap. No normal remote path for creating such a symbolic link was identified, so this is not presented as a confirmed remote exploit chain.

Validation

Local

  • node --import tsx --test server/src/__tests__/storage-keys.test.ts server/src/__tests__/local-attachment-files.test.ts (11/11)
  • npm run lint
  • npm run typecheck
  • npm run server:typecheck
  • npm run guard:big-brain
  • npm run guard:llm-tracked
  • full local Unit and Integration suites require the Postgres and Redis services supplied by CI

GitHub CI

  • Unit tests
  • Integration tests
  • Typecheck
  • Lint
  • Big-brain guard
  • LLM-tracked guard

@yetone
yetone marked this pull request as ready for review August 26, 2026 18:11
@yetone

yetone commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Merging — good follow-through on your own #66.

Confirmed the gap is real on main: the local branch does resolve(root, key) plus a startsWith check and then readFile, so a link anywhere under the upload tree would be followed. And I appreciate you saying plainly that you found no remote way to create one — hardening described as hardening is more useful than hardening dressed up as an exploit.

The implementation is the right shape: realpath() on both root and candidate rather than the candidate alone, relative() containment instead of startsWith (which has its own /rootfoo edge), O_NOFOLLOW to close the window between resolving and opening, isFile() so a directory or device node can't be read, and re-running normalizeStorageKey inside the helper so it doesn't inherit the caller's trust. Checked the call site too — if (!buf) return null sits right below, so returning null fails closed rather than falling through.

Your CI checklist is fully green, so I've marked it ready and merged. Going out in the release now.

@yetone
yetone merged commit af650ca into yetone:main Aug 26, 2026
6 checks passed
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