UNPKG

jailkeeper

Version:

OFFICIALLY DEPRECATED =====================

281 lines (244 loc) 9.39 kB
/** * JailKeeper by Joshua Gross * * josh@spandex.io * * Started August 12, 2012. * * Released under the MIT license. */ var spawn = require('child_process').spawn , fork = require('child_process').fork , stream = require('stream') , events = require('events') , util = require('util') , path = require('path') , fs = require('fs'); require('./regex.js'); var operatingSystem = require('os').platform(); var truss = ('solaris' === operatingSystem ? 'truss' : ('darwin' === operatingSystem ? 'dtruss' : 'truss')); module.exports = function (options) { var jail = this; var parentPID; var activePIDs = {}; events.EventEmitter.call(jail); var streamPrototype = function () { this.writable = true; this.reabable = true; return this; }; util.inherits(streamPrototype, stream.Stream); jail.stderr = new streamPrototype(); jail.stdout = new streamPrototype(); options = options || {}; jail.options = options; jail.options.read = options.read || []; jail.options.write = options.write || []; jail.options.write.push('/dev/dtracehelper'); jail.options.write.push('/dev/dtrace/helper'); jail.options.write.push('/dev/tty'); if ('truss' === truss) { jail.options.read.push('/usr/bin/amd64/ksh93'); // joyent machines, for running node jail.options.read.push('/usr/spool/cron/atjobs'); // joyent machines, for running node. Seems sketchy. jail.options.read.push('/var/run/name_service_door'); // joyent machines, for running node } else if ('dtruss' === truss) { jail.options.read.push('/AppleInternal'); } jail.options.read.push('/'); // for running node. Seems HELLA sketchy jail.options.read.push(/^\/dev\/pts\/[0-9]$/); // joyent machines, for running node jail.options.read.push('/dev/null'); // joyent machines, for running node jail.options.read.push('/dev/kstat'); // joyent machines, for running node jail.options.read.push('/dev/urandom'); jail.options.read.push('/etc/localtime'); jail.options.read.push('/usr/share/zoneinfo/UTC'); jail.options.read.push(/\/usr\/lib\/locale\/(.*?)/); // joyent machines, for running node jail.options.read.push(/^\/usr\/lib\/(.*?)\.dylib$/); jail.options.read.push('/var/ld/ld.config'); // joyent machines, for running node jail.options.read.push('/var/ld/64/ld.config'); // joyent machines, for running node jail.options.read.push(/\/lib\/64\/[^\/]+.so.[0-9]/); // joyent machines, for running node jail.options.read.push(/\/lib\/[^\/]+.so.[0-9]/); // joyent machines, for running node jail.options.read.push('/proc/self/auxv'); // joyent machines, for running node jail.options.read.push(path.resolve(__dirname+'/child.js')); jail.options.read.push(process.cwd()); for (var i in jail.options.write) { jail.options.read.push(options.write[i]) } jail.childProcess = null; var jailbreak = function (mode, file, line) { console.log('Jailbreak at:', mode, file, line); if (jail.childProcess) { jail.emit('jailbreak', {mode:mode, file:file}); killChild(); } return true; }; // Get a list of processes opened by some parent ID // If a process is spawned by another process we're watching... watch children too! var trussHasResponded = false; var trussRespond = function (out) { var lines = out.toString().split(/\s*[\r\n]+\s*/); for (var i in lines) { var line = lines[i]; var matchStat64 = line.match(/stat([^\(]*)\(([^\)]+)\)/); var matchOpen = line.match(/open([^\(]*)\(([^\)]+)\)/); var pidMatch = line.match(/^([0-9]+)[\/:]/); var forkMatch = line.match(/fork\(\)\s*=\s*([0-9]+)\s*/); if (line.indexOf('DTrace requires additional privileges') !== -1) { throw new Error('You must run this as a superuser; dtrace requires it.'); } if (!trussHasResponded) { trussHasResponded = true; jail.childProcess.send('trussReady'); } var pid = (pidMatch ? pidMatch[1] : null); if (forkMatch) { pid = forkMatch[1]; } if (pid && pid > parentPID) { // This seems to sometimes work on Mac OS X, sometimes not - but my installation of dtruss is extremely slow. Regardless, everything works consistently without this on Solaris. //if (!activePIDs[pid] && 'dtruss' === truss) spawnJail(pid); activePIDs[pid] = 1; } var file, mode; if (matchStat64) { var details = matchStat64[2].split(/\s*,\s*/); file = details[0]; mode = 'read'; } else if (matchOpen) { var details = matchOpen[2].split(/\s*,\s*/); file = details[0]; mode = ('0x0' === details[1] ? 'read' : (/(^|\|)O_RDONLY($|\|)/.test(details[1]) ? 'read' : 'write')); // 0x601 is definitely write } if (file || mode) { var matchQuotes = file.match(/^"([^"]+)"$/); if (matchQuotes) { file = matchQuotes[1]; var matchNullTerminator = file.match(/^(.*?)\\0$/); if (matchNullTerminator) { file = matchNullTerminator[1]; } } file = path.resolve(jail.options.cwd, file); // Allow reading and writing within the jail dir itself if (path.dirname(file).indexOf(jail.options.cwd) === 0) { return false; } // Check allow-read for (var j in jail.options.read) { if (jail.options.read[j].test && jail.options.read[j].test(file)) { return false; } else if (jail.options.read[j] === file) { return false; } } // Check allow-write if ('write' === mode) { for (var j in jail.options.write) { if (jail.options.write[j].test && jail.options.write[j].test(file)) { return false; } else if (jail.options.write[j] === file) { return false; } } } // Finally, if the file doesn't exist and we're in read mode, it's harmless to allow a read (?) if ('read' === mode) { var exists = fs.existsSync(file); return exists && jailbreak(mode, file, line); } else { return jailbreak(mode, file, line); } } } }; var killChild = function () { if (jail.childProcess) { for (var pid in activePIDs) { spawn('kill', ['-9', pid]); } // Attempt to kill processes forever, until they die process.nextTick(killChild); } }; var jails = 0; var spawnJail = function (pid, cmd, args) { var trussArgs = ['-f', '-l']; if ('dtruss' === truss) trussArgs.push('-o'); trussArgs.push('-p'); trussArgs.push(pid); jails++; var jailProcess = spawn(truss, trussArgs); jailProcess.stderr.on('data', trussRespond); jailProcess.stdout.on('data', trussRespond); jail.childProcess.on('exit', function (code) { jail.childProcess = null; jail.stderr.emit('end'); jail.stdout.emit('end'); if (jailProcess) { jailProcess.kill(); } }); jailProcess.on('exit', function (code) { jails--; if (0 === jails) { jail.emit('exit'); if (jail.childProcess) { jail.childProcess.kill(); } } jailProcess = null; }); }; jail.spawn = function (cmd, args, options) { if (jail.childProcess !== null) { throw new Error('This jailer still has an active child process. Please use a different jailer or wait until this one has finished.'); return null; } options = options || []; jail.options.cwd = options.cwd || process.cwd(); jail.options.cwd = path.resolve(jail.options.cwd); options.env = options.env || {}; options.env.PATH = options.env.PATH || process.env.PATH; // At least everything in PATH must be readable var pathSplit = options.env.PATH.split(':'); for (var i in pathSplit) { var sep = (/\/$/.test(pathSplit[i]) ? '' : '/'); jail.options.read.push(new RegExp('^'+RegExp.escape(pathSplit[i]+sep)+'[^\\/]+$')); } // Make all directories leading up to cwd, path, readable-files readable (there are lots of stat64 calls on these directories) jail.options.read.push(jail.options.cwd); var makeTraversalLegal = function (dir) { if (dir.test) { // this is a regex-to-string unescaper dir = dir.toString().match(/^\/[\^]?(.*?)(\(\.\*\?\)|\[\^\\\/\]\+)?[\$]?\/$/)[1].replace(/\\(.)/g, '$1'); if (dir.indexOf('[') !== -1 || dir.indexOf('(') !== -1) { return; } } while ((dir = path.dirname(dir)) != '/') { jail.options.read.push(dir); } }; for (var i in jail.options.read) { makeTraversalLegal(jail.options.read[i]); } jail.childProcess = spawn('node', [__dirname+'/child.js'], { stdio: ['ipc', null, null] }); jail.childProcess.send({cmd: cmd, args: args, options: options }); jail.childProcess.stdout.on('data', function (d) { jail.stdout.emit('data', d); }); jail.childProcess.stderr.on('data', function (d) { jail.stderr.emit('data', d); }); parentPID = jail.childProcess.pid; activePIDs[parentPID] = 1; spawnJail(jail.childProcess.pid); return jail; }; return jail; }; util.inherits(module.exports, events.EventEmitter);