fix(attachments): harden registry + close magic-link injection vector

Security (MAJOR): the terminal-output codeman://attach scanner registered any
matching path server-side with no user confirmation and broadcast the rawUrl
over SSE. Terminal output is attacker-influenceable (a prompt-injected session
can print an arbitrary path), so on the default no-auth deployment this was an
arbitrary host-file (png/pdf/docx/pptx/md/txt) read primitive reachable by any
SSE client. Magic-link registration is now force-confined to the session
workspace (forceWorkspaceConfinement) regardless of the global confine setting;
deliberate cross-workspace attach still works through the explicit,
Origin-guarded POST /attachments route and 'codeman attach' (which POSTs
directly inside a managed session). Documented in security-architecture.md.

Regression (MAJOR): .png was rerouted from the image-popup path to
attachment:detected, which has no frontend consumer — silently breaking the
dropped/pasted-screenshot popup. PNG stays on image:detected; only pdf/docx/pptx
(which never had a popup) emit attachment:detected.

Also:
- raw route streams the freshly-resolved path, not the stored one, so a
  post-registration symlink swap can't redirect the stream (TOCTOU).
- 50MB cap on the attachment raw route, matching file-raw / download.
- per-session attachment registry cap (200) to bound the POST path.
- CLI reads creds via dataPath('.env'), honoring CODEMAN_INSTANCE.

Tests: forced-confinement reject/allow cases; PNG popup-path assertions updated.
This commit is contained in:
arkon
2026-06-11 10:27:09 +02:00
parent f1c64994ad
commit f7ce8e4767
8 changed files with 144 additions and 31 deletions
+3 -3
View File
@@ -13,8 +13,8 @@ import { createRequire } from 'module';
import http from 'node:http';
import https from 'node:https';
import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { isAbsolute, join } from 'node:path';
import { isAbsolute } from 'node:path';
import { dataPath } from './config/instance.js';
import { getSessionManager } from './session-manager.js';
import { getTaskQueue } from './task-queue.js';
import { getRalphLoop } from './ralph-loop.js';
@@ -34,7 +34,7 @@ function makeAttachmentMagicLink(filePath: string): string {
}
function readCodemanEnv(): Record<string, string> {
const envPath = join(homedir(), '.codeman', '.env');
const envPath = dataPath('.env');
try {
const text = readFileSync(envPath, 'utf-8');
const result: Record<string, string> = {};