Merge pull request #1475 from foxsky/fix/security-stopcontainer-mount
fix(security): prevent command injection in stopContainer and mount path injection
This commit is contained in:
@@ -39,11 +39,20 @@ describe('readonlyMountArgs', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('stopContainer', () => {
|
describe('stopContainer', () => {
|
||||||
it('returns stop command using CONTAINER_RUNTIME_BIN', () => {
|
it('calls docker stop for valid container names', () => {
|
||||||
expect(stopContainer('nanoclaw-test-123')).toBe(
|
stopContainer('nanoclaw-test-123');
|
||||||
|
expect(mockExecSync).toHaveBeenCalledWith(
|
||||||
`${CONTAINER_RUNTIME_BIN} stop -t 1 nanoclaw-test-123`,
|
`${CONTAINER_RUNTIME_BIN} stop -t 1 nanoclaw-test-123`,
|
||||||
|
{ stdio: 'pipe' },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects names with shell metacharacters', () => {
|
||||||
|
expect(() => stopContainer('foo; rm -rf /')).toThrow('Invalid container name');
|
||||||
|
expect(() => stopContainer('foo$(whoami)')).toThrow('Invalid container name');
|
||||||
|
expect(() => stopContainer('foo`id`')).toThrow('Invalid container name');
|
||||||
|
expect(mockExecSync).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- ensureContainerRuntimeRunning ---
|
// --- ensureContainerRuntimeRunning ---
|
||||||
|
|||||||
@@ -27,9 +27,12 @@ export function readonlyMountArgs(
|
|||||||
return ['-v', `${hostPath}:${containerPath}:ro`];
|
return ['-v', `${hostPath}:${containerPath}:ro`];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the shell command to stop a container by name. */
|
/** Stop a container by name. Uses execFileSync to avoid shell injection. */
|
||||||
export function stopContainer(name: string): string {
|
export function stopContainer(name: string): void {
|
||||||
return `${CONTAINER_RUNTIME_BIN} stop -t 1 ${name}`;
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(name)) {
|
||||||
|
throw new Error(`Invalid container name: ${name}`);
|
||||||
|
}
|
||||||
|
execSync(`${CONTAINER_RUNTIME_BIN} stop -t 1 ${name}`, { stdio: 'pipe' });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ensure the container runtime is running, starting it if needed. */
|
/** Ensure the container runtime is running, starting it if needed. */
|
||||||
@@ -82,7 +85,7 @@ export function cleanupOrphans(): void {
|
|||||||
const orphans = output.trim().split('\n').filter(Boolean);
|
const orphans = output.trim().split('\n').filter(Boolean);
|
||||||
for (const name of orphans) {
|
for (const name of orphans) {
|
||||||
try {
|
try {
|
||||||
execSync(stopContainer(name), { stdio: 'pipe' });
|
stopContainer(name);
|
||||||
} catch {
|
} catch {
|
||||||
/* already stopped */
|
/* already stopped */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ export function loadMountAllowlist(): MountAllowlist | null {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(MOUNT_ALLOWLIST_PATH)) {
|
if (!fs.existsSync(MOUNT_ALLOWLIST_PATH)) {
|
||||||
allowlistLoadError = `Mount allowlist not found at ${MOUNT_ALLOWLIST_PATH}`;
|
// Do NOT cache this as an error — file may be created later without restart.
|
||||||
|
// Only parse/structural errors are permanently cached.
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{ path: MOUNT_ALLOWLIST_PATH },
|
{ path: MOUNT_ALLOWLIST_PATH },
|
||||||
'Mount allowlist not found - additional mounts will be BLOCKED. ' +
|
'Mount allowlist not found - additional mounts will be BLOCKED. ' +
|
||||||
@@ -209,6 +210,11 @@ function isValidContainerPath(containerPath: string): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Must not contain colons — prevents Docker -v option injection (e.g., "repo:rw")
|
||||||
|
if (containerPath.includes(':')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user