Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions packages/opencode/src/file/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,6 @@ export namespace File {
const project = Instance.project
const full = path.join(Instance.directory, file)

// TODO: Filesystem.contains is lexical only - symlinks inside the project can escape.
// TODO: On Windows, cross-drive paths bypass this check. Consider realpath canonicalization.
if (!Instance.containsPath(full)) {
throw new Error(`Access denied: path escapes project directory`)
}
Expand Down Expand Up @@ -337,8 +335,6 @@ export namespace File {
}
const resolved = dir ? path.join(Instance.directory, dir) : Instance.directory

// TODO: Filesystem.contains is lexical only - symlinks inside the project can escape.
// TODO: On Windows, cross-drive paths bypass this check. Consider realpath canonicalization.
if (!Instance.containsPath(resolved)) {
throw new Error(`Access denied: path escapes project directory`)
}
Expand Down
27 changes: 25 additions & 2 deletions packages/opencode/src/util/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,31 @@ export namespace Filesystem {
return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..")
}

export function contains(parent: string, child: string) {
return !relative(parent, child).startsWith("..")
/**
* Check if a path is within a parent directory, with protection against:
* - Symlink escapes: Resolves symlinks to their real paths
* - Cross-drive paths on Windows: Checks drive letters match
* Returns true if child is within parent, false otherwise.
*/
export function contains(parent: string, child: string): boolean {
try {
const realParent = realpathSync(parent)
const realChild = realpathSync(child)

if (process.platform === "win32") {
const parentDrive = realParent.split(":")[0]?.toLowerCase()
const childDrive = realChild.split(":")[0]?.toLowerCase()
if (parentDrive !== childDrive) {
return false
}
}

const rel = relative(realParent, realChild)
return !rel.startsWith("..") && rel !== ".."
} catch {
const rel = relative(parent, child)
return !rel.startsWith("..")
}
}

export async function findUp(target: string, start: string, stop?: string) {
Expand Down