fix(files): the editor never reaches Codeman's own .env by another spelling, and a save keeps the file's group (#797)

The four review items of #797, applied at merge.

The editor's name rule lowercases, the data dir's .env pattern did not:
.codeman/.ENV passed both, and on a case-insensitive filesystem (the macOS
default) that is ~/.codeman/.env, the login file codeman attach reads. A
session whose workspace is the home folder could open and save it. The
pattern is case-insensitive now, pinned in the blocklist table and on the
three editor routes.

A save writes a new file and renames it over the old one, so the file took
the server's own group: a 0640 .env owned you:www-data came back you:you
and the web server lost read access. The save now puts the old user and
group back where the account may (best effort), and the wiki says mode
and group instead of permissions.

Tests: an .env in an outside worktree is refused on edit=1 and PUT too,
now that the editor's own gate lets dotenv through; the comment on why
.env.example is blocked for downloads and attachments is back.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-11 20:25:37 +01:00
1 parent 18b309fc02
commit 59d4bbca2d
7 files changed
+89 -6

No files matched your search

+13
View File
@@ -2455,6 +2455,13 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
// masked by the process umask; fsync so the rename never publishes a
// partially-durable file. Trade-off (same as vim's default): the inode
// changes, so hardlinks keep the old content.
//
// The new file would also take this process's own user and group, so a
// `.env` shared by group (you:www-data, 0640) came back you:you and the
// web server lost it. fchown puts both back where the account may: the
// owner can give a file to a group they belong to, and only root can set
// the user. Best effort, since a save that cannot keep the group is still
// the save the user asked for.
const fileMode = stat.mode & 0o777;
const tmpPath = join(
dirname(resolvedPath),
@@ -2463,6 +2470,12 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
let handle;
try {
handle = await fs.open(tmpPath, 'wx', fileMode);
const tmpHandle = handle;
await tmpHandle
.chown(stat.uid, stat.gid)
.catch(() => tmpHandle.chown(-1, stat.gid))
.catch(() => {});
// After the chown: changing the owner can clear mode bits.
await handle.chmod(fileMode);
await handle.writeFile(outBuf);
await handle.sync();
+4 -1
View File
@@ -85,7 +85,10 @@ const SENSITIVE_PATTERNS: RegExp[] = [
// 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$/,
// Case-insensitive, unlike its neighbours: the editor's name rule lowercases
// (`isEditableFileName`), so `.codeman/.ENV` passes it, and on a case-insensitive
// filesystem (the macOS default) that IS this file.
/\/\.codeman[^/]*\/\.env$/i,
// 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