Background
PR #2 (security hardening) re-scoped /api/files authorization to getBrowsableRoots() = [homedir(), getClaudeDir()] to restore legitimate project-file reads (the FileEditorSidebar*.vue components read project files via /api/files?path=<file>&projectDir=<workingDir>). A isSensitiveHomeSubdir guard blocks hidden dirs directly under home (~/.ssh, ~/.gnupg, …).
Codex flagged (P1) on commit f831b64 that this is still too broad: authorizing the entire home directory means GET /api/files?path=/home/<user>/projects/other/.env reads any non-hidden file anywhere under $HOME — the guard does not block it because the first path segment (projects) is not hidden. For a localhost tool reading the user's own files this is low-risk, but if the dashboard is ever bound to a non-loopback interface or hit via CSRF, it is an arbitrary-home-read primitive.
This was a conscious interim risk-acceptance to land the critical upstream hardening in PR #2; this issue tracks the proper fix.
Goal
Authorize /api/files (and re-check directories.get) against claudeDir + a server-validated current project root, instead of all of homedir().
Approach (chosen)
Build a server-side registry of working directories that have been legitimately opened in this app, and authorize file reads only under claudeDir + a dir present in that registry.
Candidate sources for the registry (investigate which already exist before adding new state):
- CLI sessions persist a
workingDir (~/.claude/cli-history/*.json, server/utils/cliSession.ts).
- Chat sessions /
chat.post.ts receive body.projectDir.
- A "recent projects" / explicitly-opened-folder list, if one exists or is worth adding.
Then:
files.get.ts: resolve the requested path; require it to be under claudeDir OR under a registry-validated project root. Drop the blanket homedir() root. Keep 403-before-404 ordering and the hidden-subdir guard as defense-in-depth.
- Replace/limit
getBrowsableRoots() accordingly (or split: directory browsing may legitimately keep the home boundary for the folder picker, while file content reads use the tighter validated set — decide per surface).
- Add regression tests: a path under a registered project root is allowed; an arbitrary non-registered home path (e.g.
~/projects/other/.env) is denied; system paths still denied.
Acceptance criteria
References
🤖 Generated by Claude Code on behalf of @cbeaulieu-gt
Background
PR #2 (security hardening) re-scoped
/api/filesauthorization togetBrowsableRoots()=[homedir(), getClaudeDir()]to restore legitimate project-file reads (theFileEditorSidebar*.vuecomponents read project files via/api/files?path=<file>&projectDir=<workingDir>). AisSensitiveHomeSubdirguard blocks hidden dirs directly under home (~/.ssh,~/.gnupg, …).Codex flagged (P1) on commit
f831b64that this is still too broad: authorizing the entire home directory meansGET /api/files?path=/home/<user>/projects/other/.envreads any non-hidden file anywhere under $HOME — the guard does not block it because the first path segment (projects) is not hidden. For a localhost tool reading the user's own files this is low-risk, but if the dashboard is ever bound to a non-loopback interface or hit via CSRF, it is an arbitrary-home-read primitive.This was a conscious interim risk-acceptance to land the critical upstream hardening in PR #2; this issue tracks the proper fix.
Goal
Authorize
/api/files(and re-checkdirectories.get) againstclaudeDir+ a server-validated current project root, instead of all ofhomedir().Approach (chosen)
Build a server-side registry of working directories that have been legitimately opened in this app, and authorize file reads only under
claudeDir+ a dir present in that registry.Candidate sources for the registry (investigate which already exist before adding new state):
workingDir(~/.claude/cli-history/*.json,server/utils/cliSession.ts).chat.post.tsreceivebody.projectDir.Then:
files.get.ts: resolve the requestedpath; require it to be underclaudeDirOR under a registry-validated project root. Drop the blankethomedir()root. Keep 403-before-404 ordering and the hidden-subdir guard as defense-in-depth.getBrowsableRoots()accordingly (or split: directory browsing may legitimately keep the home boundary for the folder picker, while file content reads use the tighter validated set — decide per surface).~/projects/other/.env) is denied; system paths still denied.Acceptance criteria
/api/filesdenies reads of arbitrary$HOMEfiles not underclaudeDiror a validated project root (Codex's~/projects/other/.envcase returns 403)FileEditorSidebar*project reads still work (no regression of PR Apply upstream security hardening (parent PR #29) + cross-platform fixes #2's fix)directories.getbrowsing boundary reviewed for consistencyReferences
f831b64(server/utils/path-security.ts:95)server/api/files.get.ts,server/utils/path-security.ts(getBrowsableRoots),server/api/directories.get.ts🤖 Generated by Claude Code on behalf of @cbeaulieu-gt