@wuyuchentr/run-as-user 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/package.json +30 -0
- package/src/index.js +86 -0
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# @wuyuchentr/run-as-user
|
|
2
|
+
|
|
3
|
+
Drop privileges and run a function as another user. **Requires root.** POSIX-only (Linux, macOS).
|
|
4
|
+
|
|
5
|
+
> Designed for daemon processes that start as root and want to drop to a lower-privileged user.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @wuyuchentr/run-as-user
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
const { runAs, isRoot } = require('@wuyuchentr/run-as-user');
|
|
17
|
+
|
|
18
|
+
runAs('nobody', () => {
|
|
19
|
+
// This code runs as the 'nobody' user
|
|
20
|
+
startServer();
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Forms
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
// By username
|
|
28
|
+
runAs('www-data', () => { ... });
|
|
29
|
+
runAs('nobody', () => { ... });
|
|
30
|
+
|
|
31
|
+
// By UID
|
|
32
|
+
runAs(65534, () => { ... });
|
|
33
|
+
|
|
34
|
+
// By object (no /etc/passwd lookup)
|
|
35
|
+
runAs({ uid: 1000, gid: 1000 }, () => { ... });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Helpers
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
isRoot(); // → true if euid === 0
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## How it works
|
|
45
|
+
|
|
46
|
+
Resolves the target user via `/etc/passwd`, then in order:
|
|
47
|
+
|
|
48
|
+
1. `initgroups()` — set supplementary groups
|
|
49
|
+
2. `setgid()` — set primary group ID
|
|
50
|
+
3. `setuid()` — set user ID (permanent drop)
|
|
51
|
+
|
|
52
|
+
After `setuid()` the process **cannot regain root**. This is intentional.
|
|
53
|
+
|
|
54
|
+
## Notes
|
|
55
|
+
|
|
56
|
+
- Only works on POSIX with `setuid`/`setgid` syscalls
|
|
57
|
+
- The target user must exist in `/etc/passwd` (unless using `{ uid, gid }` form)
|
|
58
|
+
- `{ uid, gid }` form skips supplementary group initialization
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wuyuchentr/run-as-user",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Drop privileges and run a function as another user (requires root). POSIX-only.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"privilege",
|
|
15
|
+
"drop",
|
|
16
|
+
"setuid",
|
|
17
|
+
"setgid",
|
|
18
|
+
"user",
|
|
19
|
+
"security",
|
|
20
|
+
"root"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/wuyuchentr/run-as-user.git"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=14.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
function parsePasswd() {
|
|
4
|
+
const byName = {};
|
|
5
|
+
const byUid = {};
|
|
6
|
+
try {
|
|
7
|
+
const data = fs.readFileSync('/etc/passwd', 'utf-8');
|
|
8
|
+
for (const line of data.split('\n')) {
|
|
9
|
+
if (!line || line.startsWith('#')) continue;
|
|
10
|
+
const parts = line.split(':');
|
|
11
|
+
if (parts.length < 7) continue;
|
|
12
|
+
const entry = {
|
|
13
|
+
username: parts[0],
|
|
14
|
+
uid: parseInt(parts[2], 10),
|
|
15
|
+
gid: parseInt(parts[3], 10),
|
|
16
|
+
home: parts[5],
|
|
17
|
+
shell: parts[6].trim(),
|
|
18
|
+
};
|
|
19
|
+
byName[entry.username] = entry;
|
|
20
|
+
byUid[entry.uid] = entry;
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
return { byName, byUid };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const _passwd = parsePasswd();
|
|
27
|
+
|
|
28
|
+
function resolveUser(spec) {
|
|
29
|
+
if (typeof spec === 'number') {
|
|
30
|
+
const e = _passwd.byUid[spec];
|
|
31
|
+
if (!e) throw new Error(`User with UID ${spec} not found in /etc/passwd`);
|
|
32
|
+
return e;
|
|
33
|
+
}
|
|
34
|
+
if (typeof spec === 'string') {
|
|
35
|
+
if (/^\d+$/.test(spec)) return resolveUser(parseInt(spec, 10));
|
|
36
|
+
const e = _passwd.byName[spec];
|
|
37
|
+
if (!e) throw new Error(`User '${spec}' not found in /etc/passwd`);
|
|
38
|
+
return e;
|
|
39
|
+
}
|
|
40
|
+
if (spec && typeof spec.uid === 'number' && typeof spec.gid === 'number') {
|
|
41
|
+
return {
|
|
42
|
+
username: spec.username || `uid-${spec.uid}`,
|
|
43
|
+
uid: spec.uid,
|
|
44
|
+
gid: spec.gid,
|
|
45
|
+
home: spec.home || '',
|
|
46
|
+
shell: spec.shell || '',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
throw new Error('runAs: expected username (string), UID (number), or { uid, gid } object');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isRoot() {
|
|
53
|
+
return typeof process.geteuid === 'function' && process.geteuid() === 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function runAs(user, fn) {
|
|
57
|
+
if (process.platform === 'win32')
|
|
58
|
+
throw new Error('runAs is not supported on Windows');
|
|
59
|
+
|
|
60
|
+
if (typeof fn !== 'function')
|
|
61
|
+
throw new Error('runAs: second argument must be a function');
|
|
62
|
+
|
|
63
|
+
if (!isRoot())
|
|
64
|
+
throw new Error('runAs requires root privileges (euid !== 0)');
|
|
65
|
+
|
|
66
|
+
const target = resolveUser(user);
|
|
67
|
+
|
|
68
|
+
if (process.getuid() === target.uid && process.getgid() === target.gid) {
|
|
69
|
+
return fn();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (target.username) {
|
|
73
|
+
try { process.initgroups(target.username, target.gid); } catch {}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
process.setgid(target.gid);
|
|
78
|
+
process.setuid(target.uid);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
throw new Error(`Failed to drop privileges to ${target.username || target.uid}: ${err.message}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return fn();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { runAs, isRoot, resolveUser };
|