Skip to content

Symlink inside the project bypasses external_directory, and the permission prompt shows the wrong path #40160

Closed as not planned

Description

@0xSemizzz

Description

The project containment check is pure path arithmetic and does not resolve symlinks, but the file read or write that follows does. A symlink inside the project pointing somewhere outside it passes the check, so external_directory never fires and the operation lands outside the project.

The prompt you do get is also misleading rather than just absent. read and edit show the path relative to the worktree, so you see the in-project link path while a different file is being opened.

Windows does not have this, because it already resolves the path at this point. It is POSIX only.

Plugins

None

OpenCode version

1.18.11

Steps to reproduce

  1. mkdir -p /tmp/outside && echo secret > /tmp/outside/notes.txt
  2. In your project root: ln -s /tmp/outside vendor
  3. Ask the agent to read vendor/notes.txt
  4. No external_directory prompt appears, and the file contents are returned.
  5. The read prompt shows vendor/notes.txt, not /tmp/outside/notes.txt
  6. Same with a write. Ask it to write to vendor/new.txt and the file appears in /tmp/outside/

Screenshot and/or share link

No response

Operating System

Ubuntu 26.04

Terminal

GNOME Terminal (Ubuntu default)

Activity

  1. github-actions commented on Aug 2, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of or closely related to existing issues. Please check:

    If you believe this is a distinct report worth tracking separately, please let a maintainer know.

  2. iceteaSA commented on Aug 9, 2026

    @iceteaSA

    Confirming this independently, and adding one detail about the second half — the misleading prompt path — because it is worse than cosmetic.

    The containment check is FSUtil.contains, which is path.relative plus a .. test, so it is purely lexical:

    worktree  /tmp/symlink-probe/proj
    link      /tmp/symlink-probe/proj/link -> /tmp/symlink-probe/secret
    
    contains(worktree, "proj/link/creds.txt")   -> true    (lexically inside)
    contains(worktree, "secret/creds.txt")      -> false   (correctly outside)
    

    Same file, two paths, opposite verdicts.

    The part worth adding: the path that reaches the permission matcher is the relativized one, so an absolute deny rule on the real location cannot match it.

    permissionPath(via symlink) -> "link/creds.txt"
    realpath                     -> /tmp/symlink-probe/secret/creds.txt
    

    A rule like /tmp/symlink-probe/secret/**: deny is evaluated against link/creds.txt and does not match. So this is not only a missing external_directory prompt — an explicit deny rule covering the symlink's target is silently inert for access through the link. A user who has denied a sensitive directory still gets the read.

    Verified on dev. permissionPath lives in packages/opencode/src/project/instance-context.ts; FSUtil.contains in packages/core/src/fs-util.ts.

    Worth noting that realpath is not a free fix: resolving on every check costs a syscall per permission evaluation, and resolving only the containment check while leaving the emitted path lexical would fix the prompt-absence half but leave the deny-rule half broken. Both halves want the resolved path, which also changes what users see in prompts for legitimately symlinked project layouts.

  3. 0xSemizzz commented on Oct 4, 2026

    @0xSemizzz
    Author

    @iceteaSA Given the permission deny-rule bypass you pointed out, do you think #40160 warrants a CVE/security advisory?

  4. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions