Skip to content

Commit ac4129c

Browse files
CopilotMossaka
andauthored
fix: add system UID range validation to prevent privilege escalation (#267)
* Initial plan * fix: add system UID range validation to prevent privilege escalation Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com> Co-authored-by: Jiaxiao (mossaka) Zhou <duibao55328@gmail.com>
1 parent d20b34c commit ac4129c

2 files changed

Lines changed: 163 additions & 13 deletions

File tree

src/docker-manager.test.ts

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { generateDockerCompose, subnetsOverlap, writeConfigs, startContainers, stopContainers, cleanup, runAgentCommand } from './docker-manager';
1+
import { generateDockerCompose, subnetsOverlap, writeConfigs, startContainers, stopContainers, cleanup, runAgentCommand, validateIdNotInSystemRange, getSafeHostUid, getSafeHostGid, MIN_REGULAR_UID } from './docker-manager';
22
import { WrapperConfig } from './types';
33
import * as fs from 'fs';
44
import * as path from 'path';
@@ -47,6 +47,127 @@ describe('docker-manager', () => {
4747
});
4848
});
4949

50+
describe('validateIdNotInSystemRange', () => {
51+
it('should return 1000 for system UIDs (0-999)', () => {
52+
expect(validateIdNotInSystemRange(0)).toBe('1000');
53+
expect(validateIdNotInSystemRange(1)).toBe('1000');
54+
expect(validateIdNotInSystemRange(13)).toBe('1000'); // proxy user
55+
expect(validateIdNotInSystemRange(999)).toBe('1000');
56+
});
57+
58+
it('should return the UID as-is for regular users (>= 1000)', () => {
59+
expect(validateIdNotInSystemRange(1000)).toBe('1000');
60+
expect(validateIdNotInSystemRange(1001)).toBe('1001');
61+
expect(validateIdNotInSystemRange(65534)).toBe('65534'); // nobody user on some systems
62+
});
63+
});
64+
65+
describe('getSafeHostUid', () => {
66+
const originalGetuid = process.getuid;
67+
const originalSudoUid = process.env.SUDO_UID;
68+
69+
afterEach(() => {
70+
process.getuid = originalGetuid;
71+
if (originalSudoUid !== undefined) {
72+
process.env.SUDO_UID = originalSudoUid;
73+
} else {
74+
delete process.env.SUDO_UID;
75+
}
76+
});
77+
78+
it('should return 1000 when SUDO_UID is a system UID', () => {
79+
process.getuid = () => 0; // Running as root
80+
process.env.SUDO_UID = '13'; // proxy user
81+
expect(getSafeHostUid()).toBe('1000');
82+
});
83+
84+
it('should return SUDO_UID when it is a regular user UID', () => {
85+
process.getuid = () => 0; // Running as root
86+
process.env.SUDO_UID = '1001';
87+
expect(getSafeHostUid()).toBe('1001');
88+
});
89+
90+
it('should return 1000 when SUDO_UID is 0', () => {
91+
process.getuid = () => 0; // Running as root
92+
process.env.SUDO_UID = '0';
93+
expect(getSafeHostUid()).toBe('1000');
94+
});
95+
96+
it('should return 1000 when running as root without SUDO_UID', () => {
97+
process.getuid = () => 0;
98+
delete process.env.SUDO_UID;
99+
expect(getSafeHostUid()).toBe('1000');
100+
});
101+
102+
it('should return 1000 for non-root system UID', () => {
103+
process.getuid = () => 13; // proxy user
104+
delete process.env.SUDO_UID;
105+
expect(getSafeHostUid()).toBe('1000');
106+
});
107+
108+
it('should return the UID when running as regular user', () => {
109+
process.getuid = () => 1001;
110+
delete process.env.SUDO_UID;
111+
expect(getSafeHostUid()).toBe('1001');
112+
});
113+
});
114+
115+
describe('getSafeHostGid', () => {
116+
const originalGetgid = process.getgid;
117+
const originalSudoGid = process.env.SUDO_GID;
118+
119+
afterEach(() => {
120+
process.getgid = originalGetgid;
121+
if (originalSudoGid !== undefined) {
122+
process.env.SUDO_GID = originalSudoGid;
123+
} else {
124+
delete process.env.SUDO_GID;
125+
}
126+
});
127+
128+
it('should return 1000 when SUDO_GID is a system GID', () => {
129+
process.getgid = () => 0; // Running as root
130+
process.env.SUDO_GID = '13'; // proxy group
131+
expect(getSafeHostGid()).toBe('1000');
132+
});
133+
134+
it('should return SUDO_GID when it is a regular user GID', () => {
135+
process.getgid = () => 0; // Running as root
136+
process.env.SUDO_GID = '1001';
137+
expect(getSafeHostGid()).toBe('1001');
138+
});
139+
140+
it('should return 1000 when SUDO_GID is 0', () => {
141+
process.getgid = () => 0; // Running as root
142+
process.env.SUDO_GID = '0';
143+
expect(getSafeHostGid()).toBe('1000');
144+
});
145+
146+
it('should return 1000 when running as root without SUDO_GID', () => {
147+
process.getgid = () => 0;
148+
delete process.env.SUDO_GID;
149+
expect(getSafeHostGid()).toBe('1000');
150+
});
151+
152+
it('should return 1000 for non-root system GID', () => {
153+
process.getgid = () => 13; // proxy group
154+
delete process.env.SUDO_GID;
155+
expect(getSafeHostGid()).toBe('1000');
156+
});
157+
158+
it('should return the GID when running as regular user', () => {
159+
process.getgid = () => 1001;
160+
delete process.env.SUDO_GID;
161+
expect(getSafeHostGid()).toBe('1001');
162+
});
163+
});
164+
165+
describe('MIN_REGULAR_UID constant', () => {
166+
it('should be 1000 (standard Linux regular user UID threshold)', () => {
167+
expect(MIN_REGULAR_UID).toBe(1000);
168+
});
169+
});
170+
50171
describe('generateDockerCompose', () => {
51172
const mockConfig: WrapperConfig = {
52173
allowedDomains: ['github.com', 'npmjs.org'],

src/docker-manager.ts

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,41 +12,70 @@ const SQUID_PORT = 3128;
1212
const SQUID_INTERCEPT_PORT = 3129; // Port for transparently intercepted traffic
1313

1414
/**
15-
* Gets the host user's UID, with fallback to 1000 if unavailable or root (0).
15+
* Minimum UID/GID value for regular users.
16+
* UIDs 0-999 are reserved for system users on most Linux distributions.
17+
*/
18+
export const MIN_REGULAR_UID = 1000;
19+
20+
/**
21+
* Validates that a UID/GID value is safe for use (not in system range).
22+
* Returns the value if valid, or the default (1000) if in system range.
23+
* @internal Exported for testing
24+
*/
25+
export function validateIdNotInSystemRange(id: number): string {
26+
// Reject system UIDs/GIDs (0-999) - use default unprivileged user instead
27+
if (id < MIN_REGULAR_UID) {
28+
return MIN_REGULAR_UID.toString();
29+
}
30+
return id.toString();
31+
}
32+
33+
/**
34+
* Gets the host user's UID, with fallback to 1000 if unavailable, root (0),
35+
* or in the system UID range (0-999).
1636
* When running with sudo, uses SUDO_UID to get the actual user's UID.
37+
* @internal Exported for testing
1738
*/
18-
function getSafeHostUid(): string {
39+
export function getSafeHostUid(): string {
1940
const uid = process.getuid?.();
2041

2142
// When running as root (sudo), try to get the original user's UID
2243
if (!uid || uid === 0) {
2344
const sudoUid = process.env.SUDO_UID;
24-
if (sudoUid && sudoUid !== '0') {
25-
return sudoUid;
45+
if (sudoUid) {
46+
const parsedUid = parseInt(sudoUid, 10);
47+
if (!isNaN(parsedUid)) {
48+
return validateIdNotInSystemRange(parsedUid);
49+
}
2650
}
27-
return '1000';
51+
return MIN_REGULAR_UID.toString();
2852
}
2953

30-
return uid.toString();
54+
return validateIdNotInSystemRange(uid);
3155
}
3256

3357
/**
34-
* Gets the host user's GID, with fallback to 1000 if unavailable or root (0).
58+
* Gets the host user's GID, with fallback to 1000 if unavailable, root (0),
59+
* or in the system GID range (0-999).
3560
* When running with sudo, uses SUDO_GID to get the actual user's GID.
61+
* @internal Exported for testing
3662
*/
37-
function getSafeHostGid(): string {
63+
export function getSafeHostGid(): string {
3864
const gid = process.getgid?.();
3965

4066
// When running as root (sudo), try to get the original user's GID
4167
if (!gid || gid === 0) {
4268
const sudoGid = process.env.SUDO_GID;
43-
if (sudoGid && sudoGid !== '0') {
44-
return sudoGid;
69+
if (sudoGid) {
70+
const parsedGid = parseInt(sudoGid, 10);
71+
if (!isNaN(parsedGid)) {
72+
return validateIdNotInSystemRange(parsedGid);
73+
}
4574
}
46-
return '1000';
75+
return MIN_REGULAR_UID.toString();
4776
}
4877

49-
return gid.toString();
78+
return validateIdNotInSystemRange(gid);
5079
}
5180

5281
/**

0 commit comments

Comments
 (0)