mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
Merge #797: the File Viewer edits workspace dotenv files
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
commit
18b309fc02
13 files changed
+417
-34
No files matched your search
@@ -126,9 +126,15 @@ export async function loadAttachmentGuardConfig(): Promise<AttachmentGuardConfig
|
||||
*
|
||||
* NOTE: this is intentionally a SUPERSET of `isSensitivePath` so `/api/download`
|
||||
* behavior is NOT changed — only attachment registration/serving uses this.
|
||||
* `opts` passes through to `isSensitivePath` (the File Viewer editor's
|
||||
* `allowDotenv`).
|
||||
*/
|
||||
export function isBlockedAttachmentPath(absPath: string, blockedTrees: readonly string[]): boolean {
|
||||
if (isSensitivePath(absPath)) return true;
|
||||
export function isBlockedAttachmentPath(
|
||||
absPath: string,
|
||||
blockedTrees: readonly string[],
|
||||
opts: { allowDotenv?: boolean } = {}
|
||||
): boolean {
|
||||
if (isSensitivePath(absPath, opts)) return true;
|
||||
return blockedTrees.some((tree) => isUnderTree(absPath, tree));
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,13 @@
|
||||
*
|
||||
* Design (docs/file-viewer-edit-plan.md):
|
||||
* - ALLOWLIST of text extensions/basenames, not a blocklist — matching the
|
||||
* attachment-guard precedent. `svg` and `env` are deliberately absent: svg is
|
||||
* treated as untrusted on the read side, and `.env` is sensitive-path blocked
|
||||
* anyway; excluding them here keeps a single obvious refusal.
|
||||
* attachment-guard precedent. `svg` and `env` are deliberately absent from
|
||||
* the extension list: svg is treated as untrusted on the read side, and the
|
||||
* list doubles as the attachment list, which must not gain `*.env`. Dotenv
|
||||
* files (`.env`, `.env.*`, `*.env`) are editable by NAME in
|
||||
* isEditableFileName instead: the plain preview already shows them, and the
|
||||
* routes pass `allowDotenv` to the sensitive-path check, which keeps every
|
||||
* other credential pattern.
|
||||
* - The `.git/` subtree is denied outright: `.git/hooks/*` is code execution and
|
||||
* a corrupted index looks unrecoverable to a user who wanted to fix a typo.
|
||||
* - EOL helpers exist because a browser <textarea> normalizes to LF; the server
|
||||
@@ -116,6 +120,10 @@ export const EDITABLE_BASENAMES: ReadonlySet<string> = new Set([
|
||||
export function isEditableFileName(fileName: string): boolean {
|
||||
const lower = fileName.toLowerCase();
|
||||
if (EDITABLE_BASENAMES.has(lower)) return true;
|
||||
// Dotenv in every spelling, by name: `.env.local`'s extension is `local`, and
|
||||
// `env` must stay off EDITABLE_EXTENSIONS, which is also the cross-workspace
|
||||
// attachment list (TEXT_ATTACHMENT_EXTENSIONS) that never accepted `*.env`.
|
||||
if (lower.endsWith('.env') || lower.startsWith('.env.')) return true;
|
||||
const dot = lower.lastIndexOf('.');
|
||||
// No extension (or a bare dotfile like `.bashrc`): only the basename list applies.
|
||||
if (dot <= 0) return false;
|
||||
|
||||
@@ -1200,10 +1200,14 @@ function throwRouteError(statusCode: number, code: ApiErrorCode, message: string
|
||||
* Gate a resolved workspace file for edit-mode read/write. Throws a structured
|
||||
* error when the file may not be edited; returns void when it may. Order
|
||||
* matters for the message a user sees: confinement (the caller's 404) →
|
||||
* sensitive/blocked (403) → .git (403) → extension allowlist (400).
|
||||
* sensitive/blocked (403) → .git (403) → extension allowlist (400). Dotenv is
|
||||
* the one sensitive family the editor may open (`allowDotenv`): the plain
|
||||
* preview already serves it to the same user. Keep this and the `editable`
|
||||
* advertisement in the read route in step.
|
||||
*/
|
||||
function assertEditableTarget(resolvedPath: string, relativePath: string, blockedTrees: readonly string[]): void {
|
||||
if (isSensitivePath(resolvedPath) || isBlockedAttachmentPath(resolvedPath, blockedTrees)) {
|
||||
// isBlockedAttachmentPath includes the sensitive-path check.
|
||||
if (isBlockedAttachmentPath(resolvedPath, blockedTrees, { allowDotenv: true })) {
|
||||
throwFileEditError(403, ApiErrorCode.FORBIDDEN, 'Editing this file is blocked');
|
||||
}
|
||||
if (isDeniedEditRelativePath(relativePath)) {
|
||||
@@ -2332,8 +2336,7 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
|
||||
editable =
|
||||
isEditableFileName(pathBasename(target.resolvedPath)) &&
|
||||
!isDeniedEditRelativePath(relativePath) &&
|
||||
!isSensitivePath(target.resolvedPath) &&
|
||||
!isBlockedAttachmentPath(target.resolvedPath, guard.blockedTrees) &&
|
||||
!isBlockedAttachmentPath(target.resolvedPath, guard.blockedTrees, { allowDotenv: true }) &&
|
||||
stat.size <= MAX_EDITABLE_BYTES &&
|
||||
Buffer.from(content, 'utf8').equals(buf);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
* A small defense-in-depth blocklist of absolute paths that must never be
|
||||
* served to the browser regardless of how the path was obtained (workspace
|
||||
* download, cross-workspace attachment registration, raw/preview serving).
|
||||
* The workspace preview routes (`file-content`, `file-raw`) do not consult it,
|
||||
* since realpath containment is their control, and the File Viewer editor
|
||||
* skips only the dotenv entries (`allowDotenv`).
|
||||
*
|
||||
* This is intentionally a BLOCKLIST, not a workspace-confinement check:
|
||||
* cross-workspace attachment is a supported feature (codeman-publish skill +
|
||||
@@ -45,10 +48,6 @@ const SENSITIVE_PATTERNS: RegExp[] = [
|
||||
/\/\.ssh\//,
|
||||
/\/\.gnupg\//,
|
||||
|
||||
// Dotenv, in every conventional spelling (.env, .env.local, .env.production).
|
||||
/\/\.env$/,
|
||||
/\/\.env\./,
|
||||
|
||||
// Generic credential files, plus the per-vendor spellings that do not match it.
|
||||
/\/credentials(\.json|\.yml|\.yaml|\.xml|\.toml|\.db)?$/i,
|
||||
/\/\.aws\/(credentials|config)$/,
|
||||
@@ -83,6 +82,10 @@ const SENSITIVE_PATTERNS: RegExp[] = [
|
||||
/\/\.claude\/\.credentials\.json$/,
|
||||
/\/\.codeman[^/]*\/hook-secret$/,
|
||||
/\/\.codeman[^/]*\/users\.json$/,
|
||||
// The data dir's `.env` is the CODEMAN_USERNAME/PASSWORD fallback `codeman attach`
|
||||
// reads. Named here as well as in DOTENV_PATTERNS so `allowDotenv` (the File
|
||||
// Viewer editor) cannot reach it from a session whose workspace is $HOME.
|
||||
/\/\.codeman[^/]*\/\.env$/,
|
||||
// Codeman's own state files. Named once `.json` became previewable outside
|
||||
// the workspace: `SessionState.envOverrides` persists whatever the user set
|
||||
// for a session, and the env allowlist admits key-shaped names
|
||||
@@ -124,6 +127,15 @@ const SENSITIVE_PATTERNS: RegExp[] = [
|
||||
/\/\.codeman[^/]*\/backups\//,
|
||||
];
|
||||
|
||||
/**
|
||||
* Dotenv, in every conventional spelling (.env, .env.local, .env.production).
|
||||
* Kept apart from SENSITIVE_PATTERNS so the File Viewer editor can skip them
|
||||
* (`allowDotenv`): the plain preview already shows a workspace dotenv file to
|
||||
* the same user, so refusing the edit protected nothing. Every other surface
|
||||
* (download, attachments, path picker) still blocks them.
|
||||
*/
|
||||
const DOTENV_PATTERNS: RegExp[] = [/\/\.env$/, /\/\.env\./];
|
||||
|
||||
/**
|
||||
* Claude config members that are credential-bearing ONLY under the user's real
|
||||
* home directory: `~/.claude/settings.json` can hold `env.ANTHROPIC_API_KEY`
|
||||
@@ -141,8 +153,11 @@ const HOME_SENSITIVE_MEMBERS = ['.claude.json', '.claude/settings.json', '.claud
|
||||
/**
|
||||
* Returns true if the given ABSOLUTE, symlink-resolved path matches the
|
||||
* sensitive-file blocklist and must not be served to the browser.
|
||||
* `allowDotenv` skips only DOTENV_PATTERNS: a dotenv file inside a blocked tree
|
||||
* (`.ssh/.env`, a config backup) still matches its tree's pattern.
|
||||
*/
|
||||
export function isSensitivePath(absPath: string): boolean {
|
||||
export function isSensitivePath(absPath: string, opts: { allowDotenv?: boolean } = {}): boolean {
|
||||
if (!opts.allowDotenv && DOTENV_PATTERNS.some((pattern) => pattern.test(absPath))) return true;
|
||||
if (SENSITIVE_PATTERNS.some((pattern) => pattern.test(absPath))) return true;
|
||||
const home = homedir();
|
||||
return HOME_SENSITIVE_MEMBERS.some((member) => absPath === join(home, member));
|
||||
|
||||
Reference in new issue
Block a user