diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index ccb7dbcd..02951148 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -1,4 +1,4 @@ -import { generateDockerCompose, subnetsOverlap, writeConfigs, startContainers, stopContainers, cleanup, runAgentCommand } from './docker-manager'; +import { generateDockerCompose, subnetsOverlap, writeConfigs, startContainers, stopContainers, cleanup, runAgentCommand, validateIdNotInSystemRange, getSafeHostUid, getSafeHostGid, MIN_REGULAR_UID } from './docker-manager'; import { WrapperConfig } from './types'; import * as fs from 'fs'; import * as path from 'path'; @@ -47,6 +47,127 @@ describe('docker-manager', () => { }); }); + describe('validateIdNotInSystemRange', () => { + it('should return 1000 for system UIDs (0-999)', () => { + expect(validateIdNotInSystemRange(0)).toBe('1000'); + expect(validateIdNotInSystemRange(1)).toBe('1000'); + expect(validateIdNotInSystemRange(13)).toBe('1000'); // proxy user + expect(validateIdNotInSystemRange(999)).toBe('1000'); + }); + + it('should return the UID as-is for regular users (>= 1000)', () => { + expect(validateIdNotInSystemRange(1000)).toBe('1000'); + expect(validateIdNotInSystemRange(1001)).toBe('1001'); + expect(validateIdNotInSystemRange(65534)).toBe('65534'); // nobody user on some systems + }); + }); + + describe('getSafeHostUid', () => { + const originalGetuid = process.getuid; + const originalSudoUid = process.env.SUDO_UID; + + afterEach(() => { + process.getuid = originalGetuid; + if (originalSudoUid !== undefined) { + process.env.SUDO_UID = originalSudoUid; + } else { + delete process.env.SUDO_UID; + } + }); + + it('should return 1000 when SUDO_UID is a system UID', () => { + process.getuid = () => 0; // Running as root + process.env.SUDO_UID = '13'; // proxy user + expect(getSafeHostUid()).toBe('1000'); + }); + + it('should return SUDO_UID when it is a regular user UID', () => { + process.getuid = () => 0; // Running as root + process.env.SUDO_UID = '1001'; + expect(getSafeHostUid()).toBe('1001'); + }); + + it('should return 1000 when SUDO_UID is 0', () => { + process.getuid = () => 0; // Running as root + process.env.SUDO_UID = '0'; + expect(getSafeHostUid()).toBe('1000'); + }); + + it('should return 1000 when running as root without SUDO_UID', () => { + process.getuid = () => 0; + delete process.env.SUDO_UID; + expect(getSafeHostUid()).toBe('1000'); + }); + + it('should return 1000 for non-root system UID', () => { + process.getuid = () => 13; // proxy user + delete process.env.SUDO_UID; + expect(getSafeHostUid()).toBe('1000'); + }); + + it('should return the UID when running as regular user', () => { + process.getuid = () => 1001; + delete process.env.SUDO_UID; + expect(getSafeHostUid()).toBe('1001'); + }); + }); + + describe('getSafeHostGid', () => { + const originalGetgid = process.getgid; + const originalSudoGid = process.env.SUDO_GID; + + afterEach(() => { + process.getgid = originalGetgid; + if (originalSudoGid !== undefined) { + process.env.SUDO_GID = originalSudoGid; + } else { + delete process.env.SUDO_GID; + } + }); + + it('should return 1000 when SUDO_GID is a system GID', () => { + process.getgid = () => 0; // Running as root + process.env.SUDO_GID = '13'; // proxy group + expect(getSafeHostGid()).toBe('1000'); + }); + + it('should return SUDO_GID when it is a regular user GID', () => { + process.getgid = () => 0; // Running as root + process.env.SUDO_GID = '1001'; + expect(getSafeHostGid()).toBe('1001'); + }); + + it('should return 1000 when SUDO_GID is 0', () => { + process.getgid = () => 0; // Running as root + process.env.SUDO_GID = '0'; + expect(getSafeHostGid()).toBe('1000'); + }); + + it('should return 1000 when running as root without SUDO_GID', () => { + process.getgid = () => 0; + delete process.env.SUDO_GID; + expect(getSafeHostGid()).toBe('1000'); + }); + + it('should return 1000 for non-root system GID', () => { + process.getgid = () => 13; // proxy group + delete process.env.SUDO_GID; + expect(getSafeHostGid()).toBe('1000'); + }); + + it('should return the GID when running as regular user', () => { + process.getgid = () => 1001; + delete process.env.SUDO_GID; + expect(getSafeHostGid()).toBe('1001'); + }); + }); + + describe('MIN_REGULAR_UID constant', () => { + it('should be 1000 (standard Linux regular user UID threshold)', () => { + expect(MIN_REGULAR_UID).toBe(1000); + }); + }); + describe('generateDockerCompose', () => { const mockConfig: WrapperConfig = { allowedDomains: ['github.com', 'npmjs.org'], diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 5195570b..29595d63 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -12,41 +12,70 @@ const SQUID_PORT = 3128; const SQUID_INTERCEPT_PORT = 3129; // Port for transparently intercepted traffic /** - * Gets the host user's UID, with fallback to 1000 if unavailable or root (0). + * Minimum UID/GID value for regular users. + * UIDs 0-999 are reserved for system users on most Linux distributions. + */ +export const MIN_REGULAR_UID = 1000; + +/** + * Validates that a UID/GID value is safe for use (not in system range). + * Returns the value if valid, or the default (1000) if in system range. + * @internal Exported for testing + */ +export function validateIdNotInSystemRange(id: number): string { + // Reject system UIDs/GIDs (0-999) - use default unprivileged user instead + if (id < MIN_REGULAR_UID) { + return MIN_REGULAR_UID.toString(); + } + return id.toString(); +} + +/** + * Gets the host user's UID, with fallback to 1000 if unavailable, root (0), + * or in the system UID range (0-999). * When running with sudo, uses SUDO_UID to get the actual user's UID. + * @internal Exported for testing */ -function getSafeHostUid(): string { +export function getSafeHostUid(): string { const uid = process.getuid?.(); // When running as root (sudo), try to get the original user's UID if (!uid || uid === 0) { const sudoUid = process.env.SUDO_UID; - if (sudoUid && sudoUid !== '0') { - return sudoUid; + if (sudoUid) { + const parsedUid = parseInt(sudoUid, 10); + if (!isNaN(parsedUid)) { + return validateIdNotInSystemRange(parsedUid); + } } - return '1000'; + return MIN_REGULAR_UID.toString(); } - return uid.toString(); + return validateIdNotInSystemRange(uid); } /** - * Gets the host user's GID, with fallback to 1000 if unavailable or root (0). + * Gets the host user's GID, with fallback to 1000 if unavailable, root (0), + * or in the system GID range (0-999). * When running with sudo, uses SUDO_GID to get the actual user's GID. + * @internal Exported for testing */ -function getSafeHostGid(): string { +export function getSafeHostGid(): string { const gid = process.getgid?.(); // When running as root (sudo), try to get the original user's GID if (!gid || gid === 0) { const sudoGid = process.env.SUDO_GID; - if (sudoGid && sudoGid !== '0') { - return sudoGid; + if (sudoGid) { + const parsedGid = parseInt(sudoGid, 10); + if (!isNaN(parsedGid)) { + return validateIdNotInSystemRange(parsedGid); + } } - return '1000'; + return MIN_REGULAR_UID.toString(); } - return gid.toString(); + return validateIdNotInSystemRange(gid); } /**