Skip to content

Harden /api/files: authorize against a server-validated project registry, not all of $HOME #3

Description

@cbeaulieu-gt

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

  • /api/files denies reads of arbitrary $HOME files not under claudeDir or a validated project root (Codex's ~/projects/other/.env case returns 403)
  • Legitimate FileEditorSidebar* project reads still work (no regression of PR Apply upstream security hardening (parent PR #29) + cross-platform fixes #2's fix)
  • Registry source documented; no new untrusted client-supplied root is trusted for authorization
  • Tests cover allow (registered project) / deny (unregistered home path) / deny (system path)
  • directories.get browsing boundary reviewed for consistency

References

🤖 Generated by Claude Code on behalf of @cbeaulieu-gt

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions