chroot
Version:
Safely chroot the current process and drop privileges
157 lines (132 loc) • 5.65 kB
JavaScript
/**
* Copyright (c) 2014, 2015, 2021 Tim Kuijsten
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
;
var fs = require('fs');
var path = require('path');
var posix = require('posix');
/**
* Change the root directory of the current process. A normal user must be provided
* since changing root without dropping privileges makes no sense from a security
* point of view.
*
* @param {String} newRoot The path to the new root directory for this process.
* The whole path should be owned by the super user and may not be writable
* by the group owner or others.
* @param {String|Number} user The user to switch to after changing the root
* directory. Can be either a name or an id.
* @param {String|Number} [group] The group to switch to after changing the root
* directory. Can be either a name or an id of any group the user belongs to
* (see /etc/groups). Defaults to the users primary group (see /etc/passwd).
* @throw if any operation fails
*/
module.exports = function chroot(newRoot, user, group) {
if (typeof newRoot !== 'string') { throw new TypeError('newRoot must be a string'); }
if (typeof user !== 'string' && typeof user !== 'number') { throw new TypeError('user must be a string or a number'); }
if (typeof group !== 'undefined') {
if (typeof group !== 'string' && typeof group !== 'number') { throw new TypeError('group must be a string or a number'); }
}
if (!(newRoot.length > 0)) { throw new Error('newRoot must contain at least one character'); }
if (typeof user === 'string' && !(user.length > 0)) { throw new Error('user must contain at least one character'); }
if (process.getuid() !== 0 || posix.geteuid() !== 0) {
throw new Error('chroot must be called while running as root');
}
var pwd, grp, uid, gid;
// resolve user to a numeric id
try {
pwd = posix.getpwnam(user);
} catch(err) {
throw new Error('user not found: ' + user);
}
uid = pwd.uid;
gid = pwd.gid;
if (typeof group !== 'undefined') {
if (typeof group === 'number') {
gid = group;
} else {
// resolve group to a numeric id
try {
grp = posix.getgrnam(group);
} catch(err) {
throw new Error('group not found: ' + group);
}
gid = grp.gid;
}
}
if (typeof uid !== 'number') { throw new TypeError('could not resolve the user to a number'); }
if (typeof gid !== 'number') { throw new TypeError('could not resolve the group to a number'); }
if (!(uid > 0)) { throw new Error('new user can not have user id 0'); }
if (!(gid > 0)) { throw new Error('new group can not have group id 0'); }
// check permissions up to the original root of the file system
var rpath = newRoot = fs.realpathSync(newRoot);
var stats;
do {
stats = fs.statSync(rpath);
if (stats.uid !== 0 || (stats.mode & (fs.constants.S_IWGRP | fs.constants.S_IWOTH)) !== 0) {
throw new Error('bad chroot dir ' + rpath + ' owner: ' + stats.uid + ' or permissions: 0' + stats.mode.toString(8));
}
rpath = path.dirname(rpath);
} while (rpath !== '/');
try {
posix.chroot(newRoot);
} catch(err) {
throw new Error('changing root failed: ' + err.message);
}
process.chdir('/');
// PWD might be set in some environments and is part of POSIX
if (typeof process.env.PWD !== 'undefined') {
process.env.PWD = '/';
}
try {
if (typeof group === 'undefined') {
// change to the given user and all the groups that it is a member of
process.initgroups(uid, gid);
} else {
// change to the given user and the given group (not all groups the user is member of)
process.setgroups([gid]);
}
} catch(err) {
throw new Error('changing groups failed: ' + err.message);
}
process.setgid(gid);
process.setuid(uid);
// try to restore privileges
try {
posix.setreuid(-1, 0);
} catch(err) {
// double check real and effective ids of the user and group and supplemental groups
var ids = [process.getuid(), process.getgid(), posix.geteuid(), posix.getegid()];
Array.prototype.push.apply(ids, process.getgroups());
// if none of the ids is zero, privileges are successfully dropped
if (!~ids.indexOf(0)) {
// success
return;
}
}
throw new Error('unable to drop privileges');
};
/**
* TODO: add utilities that can help a user in inspecting a designated chroot by
* checking paths up to the original root for ownership and permissions,
* environment and open file descriptors.
*
* see also:
* http://www.unixwiz.net/techtips/chroot-practices.html
* http://www.dwheeler.com/secure-programs/Secure-Programs-HOWTO/minimize-privileges.html
* http://www.cs.berkeley.edu/~daw/papers/setuid-usenix02.pdf
* http://www.lst.de/~okir/blackhats/node97.html
* https://medium.com/@fun_cuddles/opening-files-in-node-js-considered-harmful-d7de566d499f
* https://github.com/joyent/node/issues/6905
*/