fix: block group folder path escapes

This commit is contained in:
Lawyered
2026-02-22 11:36:06 -05:00
committed by gavrielc
parent de64dab3e9
commit c6391cceb1
9 changed files with 186 additions and 25 deletions

View File

@@ -4,7 +4,6 @@
*/
import { ChildProcess, exec, spawn } from 'child_process';
import fs from 'fs';
import os from 'os';
import path from 'path';
import {
@@ -16,6 +15,7 @@ import {
IDLE_TIMEOUT,
} from './config.js';
import { readEnvFile } from './env.js';
import { resolveGroupFolderPath, resolveGroupIpcPath } from './group-folder.js';
import { logger } from './logger.js';
import { CONTAINER_RUNTIME_BIN, readonlyMountArgs, stopContainer } from './container-runtime.js';
import { validateAdditionalMounts } from './mount-security.js';
@@ -25,16 +25,6 @@ import { RegisteredGroup } from './types.js';
const OUTPUT_START_MARKER = '---NANOCLAW_OUTPUT_START---';
const OUTPUT_END_MARKER = '---NANOCLAW_OUTPUT_END---';
function getHomeDir(): string {
const home = process.env.HOME || os.homedir();
if (!home) {
throw new Error(
'Unable to determine home directory: HOME environment variable is not set and os.homedir() returned empty',
);
}
return home;
}
export interface ContainerInput {
prompt: string;
sessionId?: string;
@@ -63,8 +53,8 @@ function buildVolumeMounts(
isMain: boolean,
): VolumeMount[] {
const mounts: VolumeMount[] = [];
const homeDir = getHomeDir();
const projectRoot = process.cwd();
const groupDir = resolveGroupFolderPath(group.folder);
if (isMain) {
// Main gets the project root read-only. Writable paths the agent needs
@@ -80,14 +70,14 @@ function buildVolumeMounts(
// Main also gets its group folder as the working directory
mounts.push({
hostPath: path.join(GROUPS_DIR, group.folder),
hostPath: groupDir,
containerPath: '/workspace/group',
readonly: false,
});
} else {
// Other groups only get their own folder
mounts.push({
hostPath: path.join(GROUPS_DIR, group.folder),
hostPath: groupDir,
containerPath: '/workspace/group',
readonly: false,
});
@@ -149,7 +139,7 @@ function buildVolumeMounts(
// Per-group IPC namespace: each group gets its own IPC directory
// This prevents cross-group privilege escalation via IPC
const groupIpcDir = path.join(DATA_DIR, 'ipc', group.folder);
const groupIpcDir = resolveGroupIpcPath(group.folder);
fs.mkdirSync(path.join(groupIpcDir, 'messages'), { recursive: true });
fs.mkdirSync(path.join(groupIpcDir, 'tasks'), { recursive: true });
fs.mkdirSync(path.join(groupIpcDir, 'input'), { recursive: true });
@@ -228,7 +218,7 @@ export async function runContainerAgent(
): Promise<ContainerOutput> {
const startTime = Date.now();
const groupDir = path.join(GROUPS_DIR, group.folder);
const groupDir = resolveGroupFolderPath(group.folder);
fs.mkdirSync(groupDir, { recursive: true });
const mounts = buildVolumeMounts(group, input.isMain);
@@ -259,7 +249,7 @@ export async function runContainerAgent(
'Spawning container agent',
);
const logsDir = path.join(GROUPS_DIR, group.folder, 'logs');
const logsDir = path.join(groupDir, 'logs');
fs.mkdirSync(logsDir, { recursive: true });
return new Promise((resolve) => {
@@ -603,7 +593,7 @@ export function writeTasksSnapshot(
}>,
): void {
// Write filtered tasks to the group's IPC directory
const groupIpcDir = path.join(DATA_DIR, 'ipc', groupFolder);
const groupIpcDir = resolveGroupIpcPath(groupFolder);
fs.mkdirSync(groupIpcDir, { recursive: true });
// Main sees all tasks, others only see their own
@@ -633,7 +623,7 @@ export function writeGroupsSnapshot(
groups: AvailableGroup[],
registeredJids: Set<string>,
): void {
const groupIpcDir = path.join(DATA_DIR, 'ipc', groupFolder);
const groupIpcDir = resolveGroupIpcPath(groupFolder);
fs.mkdirSync(groupIpcDir, { recursive: true });
// Main sees all groups; others see nothing (they can't activate groups)