fix(security): add path traversal protection to File.read and File.list
Summary
Adds path containment checks to File.read() and File.list() to prevent directory traversal attacks (e.g., ../../../etc/passwd).
Problem
The File module constructs paths via path.join(Instance.directory, file) without validating containment. An attacker-controlled path like ../../../etc/passwd resolves to a valid path outside the project directory.
Solution
Uses the existing Filesystem.contains() utility (already used in tool/read.ts, tool/write.ts, etc.) to validate that resolved paths remain within Instance.directory. Throws on violation.
Changes
-
packages/opencode/src/file/index.ts: Added containment checks toFile.read()andFile.list() -
packages/opencode/test/file/path-traversal.test.ts: Added tests for traversal prevention
Known Limitations (documented via TODO)
- Symlinks inside the project can still escape (lexical check only)
- Windows cross-drive paths may bypass the check
These are pre-existing limitations in Filesystem.contains() affecting all current callers and warrant a separate PR.
Testing
bun test test/file/path-traversal.test.ts
# 4 pass, 0 fail
/review
lgtm
I'd argue it would be good to use a whitelist / blacklist / have some sort of config setting configure this.
Preventing malicious path traversals is important, but I find myself having genuine use cases for path traversal. E.g. cloning repos to study in /tmp or having opencode read / edit some config files (outside of current project)
We could:
- Block all path traversal by default, have config option with whitelist
- same as 1 but with built in white list for common paths like /tmp
- Only block predefined blacklist traversal for sensitive paths
The agent tools already have a permission system for external paths and aren't affected here. This PR only hardens the UI file browser API, which should be scoped to the project directory. For external repos, you'd run opencode from that directory.
Hm im pretty sure the code works as is actually, try this:
import { Agent } from "./agent/agent"
import { bootstrap } from "./cli/bootstrap"
import { SystemPrompt } from "./session/system"
import { mergeDeep } from "remeda"
import { ReadTool } from "./tool/read"
async function main() {
await bootstrap(process.cwd(), async () => {
const read = await ReadTool.init()
const result = await read.execute(
{
filePath: "../../../../etc/passwd",
},
{
agent: "build",
},
)
console.log(result)
})
}
await main()
It will correctly prevent it from being read (i set permission to deny instead of read here)
The ReadTool you tested has its own containment check in tool/read.ts:L51. My PR addresses a different code path: the File.read()/File.list() functions used by the HTTP API (/file/list, /file/read endpoints in server.ts:L1840-1889) that power the TUI file browser. These endpoints don't go through the agent tool layer and previously had no path validation.
oh right i forgot
i misread rhe description my b