fix: keep credential files out of the file routes and Discord uploads (release review)

The Discord bot token was readable through the attachment routes, because
discord.json was not on the sensitive list, and so were webhook.json,
custom-model-hosts.json and the generated custom-model configs. Config backups
were the wider gap: a dated copy of users.json or settings.json matched none of
the name-anchored patterns. All are blocked now.

`codeman notify --file` relied on folder confinement alone, which holds nothing
back when a tab's folder is the home directory, and it would also post a
workspace .env. It now applies the attachment blocklist to the real path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-04 17:28:52 +01:00
1 parent 126d967ba3
commit 4276b53a8f
7 files changed
+57 -6

No files matched your search

+8 -2
View File
@@ -17,6 +17,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { ApiErrorCode, createErrorResponse, getErrorMessage, type ApiResponse } from '../../types.js';
import { canAccessOwned, findSessionOrFail, getAuthUser, isAdmin, parseBody } from '../route-helpers.js';
import type { SessionPort } from '../ports/session-port.js';
import { isBlockedAttachmentPath, loadAttachmentGuardConfig } from '../../config/attachment-guard.js';
import { isMultiUserMode } from '../../config/multiuser.js';
import { describeUnknownPath, probePathKind } from '../../utils/bounded-path-probe.js';
import { DiscordNotifySchema, DiscordUpdateSchema } from '../schemas.js';
@@ -80,8 +81,10 @@ export function _resetNotifyBuckets(): void {
/**
* A file a session may send: a regular file, at most MAX_FILE_BYTES, whose REAL path (symlinks
* followed) lies inside the session's working folder. Anything else is refused, so a CLI cannot be
* talked into posting ~/.codeman/discord.json or an ssh key. Returns the bytes, or the reason.
* followed) lies inside the session's working folder and is not on the attachment blocklist. The
* blocklist matters as much as the folder: with a home directory as the session folder, confinement
* alone would let a CLI be talked into posting ~/.codeman/discord.json, an ssh key or a `.env`.
* Returns the bytes, or the reason.
*/
async function fileProblem(
workingDir: string,
@@ -103,6 +106,9 @@ async function fileProblem(
const rel = relative(root, real);
if (rel === '' || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))
return 'The file must be inside the session folder';
if (isBlockedAttachmentPath(real, (await loadAttachmentGuardConfig()).blockedTrees)) {
return 'That file cannot be sent: it can hold credentials';
}
const info = await stat(real);
if (!info.isFile()) return 'Not a regular file';
if (info.size > MAX_FILE_BYTES) return `The file is over ${MAX_FILE_BYTES / 1024 / 1024} MB`;
+11
View File
@@ -106,6 +106,17 @@ const SENSITIVE_PATTERNS: RegExp[] = [
// agent wrote, keys and .env contents included; written 0600 and kept out of
// /api/search for the same reason as intents.json.
/\/\.codeman[^/]*\/edit-history\//,
// Credentials kept outside settings.json: the Discord bot token, the webhook URL
// (a bearer secret), the custom endpoints' API keys, and the generated per-session
// configs, some of which carry that key inline.
/\/\.codeman[^/]*\/discord\.json$/,
/\/\.codeman[^/]*\/webhook\.json$/,
/\/\.codeman[^/]*\/custom-model-hosts\.json$/,
/\/\.codeman[^/]*\/custom-model-configs\//,
// Config backups are dated COPIES of the files above (users.json and settings.json
// included), which the name-anchored patterns do not match. A custom
// `configBackupDir` is outside what a pattern here can name.
/\/\.codeman[^/]*\/backups\//,
];
/**