├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── docs ├── linux.md └── macos.md ├── lib ├── index.d.ts ├── index.js └── runtime │ ├── _seccomp.cjs │ ├── exec.cjs │ └── ipc.cjs ├── package.json └── test ├── _util └── index.js ├── eval-no-sandbox.js ├── eval.js ├── isolate-no-sandbox.js ├── isolate.js ├── programs ├── dns.js ├── exec-echo.js ├── exec-node.js ├── fizzbuzz.wasm ├── fizzbuzz.wat ├── net-bind-socket-to-file.js ├── net-bind-socket-to-port.js ├── net-connect-external.js ├── net-connect-localhost.js ├── noop.js ├── read-cwd-files.js ├── readdir-cwd.js ├── readdir-home.js ├── readdir-root.js └── stdout.js ├── wasm-no-sandbox.js └── wasm.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | package-lock.json 4 | id_rsa 5 | id_rsa.pub 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM debian:bullseye 3 | 4 | RUN apt-get update 5 | RUN apt-get install systemd curl python make automake libtool g++ libseccomp-dev git strace openssh-server -y 6 | RUN curl -fs https://raw.githubusercontent.com/mafintosh/node-install/master/install | sh 7 | RUN node-install 16.8.0 8 | 9 | # Add the keys and set permissions (uncomment this if cloning from a private repo) 10 | #RUN mkdir -p /root/.ssh 11 | #ADD id_rsa /root/.ssh/id_rsa 12 | #ADD id_rsa.pub /root/.ssh/id_rsa.pub 13 | #RUN chmod 700 /root/.ssh/id_rsa && \ 14 | # chmod 700 /root/.ssh/id_rsa.pub 15 | 16 | WORKDIR /app 17 | COPY . . 18 | RUN npm i -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Confine: a secure sandboxing framework 2 | 3 | *Work in progress* 4 | 5 | A NodeJS framework for creating sandboxed runtimes for untrusted code. Uses OS process isolation (supported: [macos](./docs/macos.md), [linux](./docs/linux.md)) along with a pluggable runtime. 6 | 7 | All runtimes are a subclass of [abstract-confine-runtime](https://npm.im/abstract-confine-runtime). Current runtimes include: 8 | 9 | - [jseval-confine-runtime](https://github.com/confine-sandbox/jseval-confine-runtime) Runs javascript with no additional sandboxing. 10 | - [jsisolate-confine-runtime](https://github.com/confine-sandbox/jsisolate-confine-runtime) Runs javascript in an isolate using [isolated-vm](https://github.com/laverdet/isolated-vm). 11 | 12 | ``` 13 | npm i confine-sandbox 14 | ``` 15 | 16 | Usage example: 17 | 18 | ```js 19 | import { Sandbox } from 'confine-sandbox' 20 | 21 | const sbx = new Sandbox({ 22 | runtime: 'jseval-confine-runtime', // the name of the runtime module; must conform to abstract-confine-runtime 23 | nodeModulesPath: path.join(__dirname, 'node_modules'), // the path to your project's node_modules 24 | strace: false, // print an strace of the execution? 25 | logSpawn: false, // log the spawn() call parameters? 26 | noSandbox: false, // disable the process-level isolation? 27 | pipeStdout: false, // pipe the spawned process's stdout to the parent stdout? 28 | pipeStderr: false, // pipe the spawned process's stderr to the parent stderr? 29 | globals: { 30 | // ... methods that should be injected into the global context 31 | } 32 | }) 33 | const {cid} = await sbx.execContainer({ 34 | source: 'module.exports.addOne = (num) => num + 1', 35 | sourcePath: '/path/to/source/file.js', 36 | // ...any other opts specific to the runtime 37 | }) 38 | await sbx.configContainer({cid, opts: {/*...*/}}) 39 | const res = await sbx.handleAPICall(cid, 'addOne', [5]) // => 6 40 | ``` 41 | 42 | ## Process isolation 43 | 44 | Allowed access to the host environment will be tuned as the runtime environment is developed and the threats are identified. Please file issues with any concerns. 45 | 46 | Notes: 47 | 48 | - On MacOS, it's not currently possible to stop a readdir() on the cwd. No subsequent files can be read. 49 | 50 | ## API 51 | 52 | ```typescript 53 | import { EventEmitter } from 'events' 54 | import { Server } from 'net' 55 | import { ChildProcess } from 'child_process' 56 | 57 | export declare interface SandboxConstructorOpts { 58 | runtime?: string 59 | globals?: any 60 | strace?: boolean 61 | logSpawn?: boolean 62 | noSandbox?: boolean 63 | pipeStdout?: boolean 64 | pipeStderr?: boolean 65 | } 66 | 67 | export declare interface SandboxExecContainerOpts { 68 | source?: string 69 | sourcePath?: string 70 | } 71 | 72 | export declare interface ConfineContainer { 73 | cid: number 74 | opts?: any 75 | } 76 | 77 | export type ConfineIPC = any 78 | 79 | export declare class Sandbox extends EventEmitter { 80 | opts: SandboxConstructorOpts 81 | guestProcess?: ChildProcess 82 | ipcServer?: Server 83 | ipcServerPort?: number 84 | ipcClientPort?: number 85 | ipc?: ConfineIPC 86 | whenGuestProcessClosed: Promise 87 | globalsMap: Map 88 | 89 | constructor (opts: SandboxConstructorOpts) 90 | get guestExitCode (): number 91 | get guestExitSignal (): string 92 | get isGuestProcessActive (): boolean 93 | get runtimePath (): string 94 | init (): Promise 95 | teardown (): Promise 96 | listContainers (): Promise 97 | execContainer (opts: SandboxExecContainerOpts|any): Promise 98 | killContainer (container: ConfineContainer): Promise 99 | handleAPICall (cid: number, methodName: string, params: any[]): Promise 100 | } 101 | ``` -------------------------------------------------------------------------------- /docs/linux.md: -------------------------------------------------------------------------------- 1 | # Linux process isolation 2 | 3 | For Linux, we use seccomp to restrict access to the host environment. After the runtime has initialized itself, the seccomp sandbox is created. 4 | 5 | The seccomp uses an allowlist (default deny) with the following allowed syscalls: 6 | 7 | - `close` 8 | - `epoll_create` 9 | - `epoll_ctl` 10 | - `epoll_wait` 11 | - `exit` 12 | - `exit_group` 13 | - `fcntl` 14 | - `fcntl64` 15 | - `fstat` 16 | - `fstat64` 17 | - `fstatat64` 18 | - `futex` 19 | - `getcwd` 20 | - `getsockname` 21 | - `getsockopt` 22 | - `ioctl` 23 | - `madvise` 24 | - `mmap` 25 | - `mprotect` 26 | - `munmap` 27 | - `openat` 28 | - `read` 29 | - `rt_sigaction` 30 | - `rt_sigpending` 31 | - `rt_sigprocmask` 32 | - `rt_sigqueueinfo` 33 | - `rt_sigreturn` 34 | - `rt_sigsuspend` 35 | - `rt_sigtimedwait` 36 | - `rt_tgsigqueueinfo` 37 | - `sendmsg` 38 | - `write` 39 | - `writev` -------------------------------------------------------------------------------- /docs/macos.md: -------------------------------------------------------------------------------- 1 | # MacOS process isolation 2 | 3 | For MacOS, we use Seatbelt. Apple claims that [Seatbelt is deprecated](https://stackoverflow.com/questions/56703697/how-to-sandbox-third-party-applications-when-sandbox-exec-is-deprecated-now) in favor of the "App Sandbox," but what this really means is that some useful tools such as the trace api and `sandbox-simplify` command have been removed without an alternative while the core of Seatbelt still works. Thanks, Apple. 4 | 5 | We isolate the runtime by using `sandbox-exec` with a policy file that's generated at runtime. The policy file restricts the runtime to reading sysctl (v8 needs to read the kernel version), calling exec on node, allowing various file reads necessary to start the runtime, and allowing access to two local network ports which are used for messaging. 6 | 7 | ## Resources 8 | 9 | - [Apple Sandbox Guide 1.0](https://reverse.put.as/wp-content/uploads/2011/09/Apple-Sandbox-Guide-v1.0.pdf) 10 | - [OSX Sandbox Seatbelt Profiles](https://github.com/s7ephen/OSX-Sandbox--Seatbelt--Profiles) -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import { Server } from 'net' 3 | import { ChildProcess } from 'child_process' 4 | 5 | export declare interface SandboxConstructorOpts { 6 | runtime?: string 7 | globals?: any 8 | nodeModulesPath?: string 9 | strace?: boolean 10 | logSpawn?: boolean 11 | noSandbox?: boolean 12 | pipeStdout?: boolean 13 | pipeStderr?: boolean 14 | } 15 | 16 | export declare interface SandboxExecContainerOpts { 17 | source?: string 18 | sourcePath?: string 19 | } 20 | 21 | export declare interface ConfineContainer { 22 | cid: number 23 | opts?: any 24 | } 25 | 26 | export type ConfineIPC = any 27 | 28 | export declare class Sandbox extends EventEmitter { 29 | opts: SandboxConstructorOpts 30 | guestProcess?: ChildProcess 31 | ipcServer?: Server 32 | ipcServerPort?: number 33 | ipcClientPort?: number 34 | ipc?: ConfineIPC 35 | whenGuestProcessClosed: Promise 36 | globalsMap: Map 37 | 38 | constructor (opts: SandboxConstructorOpts) 39 | get guestExitCode (): number 40 | get guestExitSignal (): string 41 | get isGuestProcessActive (): boolean 42 | get runtimePath (): string 43 | init (): Promise 44 | teardown (): Promise 45 | listContainers (): Promise 46 | execContainer (opts: SandboxExecContainerOpts|any): Promise 47 | configContainer (container: ConfineContainer) 48 | killContainer (container: ConfineContainer): Promise 49 | handleAPICall (cid: number, methodName: string, params: any[]): Promise 50 | } -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const { join, dirname } = require('path') 2 | const { spawn } = require('child_process') 3 | const fs = require('fs').promises 4 | const EventEmitter = require('events') 5 | const net = require('net') 6 | const getPortPromise = import('get-port') 7 | const { IPC } = require('./runtime/ipc.cjs') 8 | const { pack, unpack } = require('msgpackr') 9 | const _get = require('lodash.get') 10 | 11 | const SINGLE_EXECUTE = true // for now, only run one script per guest process 12 | const NODE_MODULES_PATH = join(__dirname, '..', 'node_modules') 13 | 14 | exports.Sandbox = class Sandbox extends EventEmitter { 15 | constructor (opts = {}) { 16 | super() 17 | this.opts = { 18 | runtime: typeof opts.runtime === 'string' ? opts.runtime : undefined, 19 | globals: opts.globals && typeof opts.globals === 'object' ? opts.globals : {}, 20 | nodeModulesPath: opts.nodeModulesPath && typeof opts.nodeModulesPath === 'string' ? opts.nodeModulesPath : NODE_MODULES_PATH, 21 | strace: !!opts.strace, 22 | logSpawn: !!opts.logSpawn, 23 | noSandbox: !!opts.noSandbox, 24 | pipeStdout: !!opts.pipeStdout, 25 | pipeStderr: !!opts.pipeStderr 26 | } 27 | if (!this.opts.runtime) { 28 | throw new Error('Must specify a runtime module') 29 | } 30 | this.guestProcess = undefined 31 | this.ipcServer = undefined 32 | this.ipcServerPort = undefined 33 | this.ipcClientPort = undefined 34 | this.ipc = undefined 35 | this.whenGuestProcessClosed = new Promise(_r => { 36 | this._onClose = _r 37 | }) 38 | this.globalsMap = toGlobalsMap(this.opts.globals) 39 | process.on('exit', () => { 40 | this.ipcServer?.close() 41 | this.guestProcess?.kill() 42 | }) 43 | } 44 | 45 | get guestExitCode () { 46 | return this.guestProcess?.exitCode 47 | } 48 | 49 | get guestExitSignal () { 50 | return this.guestProcess?.signalCode 51 | } 52 | 53 | get isGuestProcessActive () { 54 | return this.guestExitCode === null && this.guestExitSignal === null 55 | } 56 | 57 | get runtimePath () { 58 | return join(__dirname, 'runtime', 'exec.cjs') 59 | } 60 | 61 | async init () { 62 | const getPort = await getPortPromise 63 | this.ipcServerPort = await getPort.default(getPort.portNumbers(3000, 65e3)) 64 | this.ipcClientPort = await getPort.default(getPort.portNumbers(3000, 65e3)) 65 | this.ipcServer = net.createServer() 66 | this.ipcServer.listen(this.ipcServerPort) 67 | 68 | const connPromise = new Promise(resolve => { 69 | this.ipcServer.on('connection', resolve) 70 | }) 71 | 72 | if (this.opts.noSandbox) { 73 | this.guestProcess = doSpawn(this.opts, process.execPath, [this.runtimePath, this.opts.runtime, this.ipcServerPort, this.ipcClientPort, '--no-sandbox']) 74 | } else { 75 | switch (process.platform) { 76 | case 'darwin': 77 | this.guestProcess = macosSpawn(this) 78 | break 79 | case 'linux': 80 | this.guestProcess = doSpawn(this.opts, process.execPath, [this.runtimePath, this.opts.runtime, this.ipcServerPort, this.ipcClientPort]) 81 | break 82 | default: 83 | throw new Error(`The ${process.platform} platform is not yet supported`) 84 | } 85 | } 86 | 87 | if (this.opts.pipeStdout) { 88 | this.guestProcess.stdout.on('data', chunk => { 89 | console.log('[SANDBOX]', chunk.toString('utf8')) 90 | }) 91 | } 92 | if (this.opts.pipeStderr) { 93 | this.guestProcess.stderr.on('data', chunk => { 94 | console.log('[SANDBOX]', chunk.toString('utf8')) 95 | }) 96 | } 97 | this.guestProcess.on('exit', code => { 98 | this._onClose(code) 99 | }) 100 | 101 | const conn = await connPromise 102 | conn.on('error', ()=>{}) // ignore 103 | this.ipc = new IPC(conn, conn, this._handleIpcRequest.bind(this)) 104 | } 105 | 106 | async teardown () { 107 | if (this.isGuestProcessActive) { 108 | this.guestProcess.kill() 109 | } 110 | return await this.whenGuestProcessClosed 111 | } 112 | 113 | async listContainers (params) { 114 | return await this._request(0, `__list_containers`, params) 115 | } 116 | 117 | async execContainer (opts) { 118 | opts = Object.assign({}, opts) 119 | if (!opts.source && typeof opts?.sourcePath !== 'string') { 120 | throw new Error(`source or sourcePath must be provided`) 121 | } 122 | opts.source = opts.source || await fs.readFile(opts.sourcePath) 123 | opts.globalsMap = Object.keys(this.globalsMap) 124 | return this._request(0, `__exec_container`, opts) 125 | } 126 | 127 | async configContainer (params) { 128 | return await this._request(0, `__config_container`, params) 129 | } 130 | 131 | async killContainer (params) { 132 | return await this._request(0, `__kill_container`, params) 133 | } 134 | 135 | handleAPICall (cid, methodName, params) { 136 | return this._request(cid, methodName, params) 137 | } 138 | 139 | async _handleIpcRequest (cid, body) { 140 | if (cid !== 0) { 141 | throw new Error(`Unable to handle request: host CID ${cid} invalid`) 142 | } 143 | const {method, params} = unpack(body) 144 | switch (method) { 145 | case '__notify_container_closed': { 146 | this.emit('container-closed', params) 147 | if (params.error) { 148 | this.emit('container-runtime-error', params) 149 | } 150 | if (SINGLE_EXECUTE) { 151 | this.teardown() 152 | } 153 | break 154 | } 155 | default: { 156 | const fn = _get(this.globalsMap, method) 157 | if (typeof fn === 'function') { 158 | return pack(await fn(...(params || []))) 159 | } else { 160 | throw pack({message: `Function ${method} does not exist`}) 161 | } 162 | } 163 | } 164 | } 165 | 166 | async _request (cid, method, params) { 167 | try { 168 | const res = await this.ipc.request(cid, pack({method, params})) 169 | return unpack(res) 170 | } catch (errPacked) { 171 | const errBody = unpack(errPacked) 172 | const err = new Error(errBody.message) 173 | err.errorName = errBody.name 174 | err.details = errBody.details 175 | throw err 176 | } 177 | } 178 | } 179 | 180 | function macosSpawn (sbx) { 181 | const runtimePathSegments = sbx.runtimePath.split('/').filter(Boolean) 182 | const runtimePathVariations = [] 183 | const acc = [] 184 | for (const segment of runtimePathSegments) { 185 | acc.push(segment) 186 | runtimePathVariations.push(`/${acc.join('/')}`) 187 | } 188 | 189 | const profile = `(version 1) 190 | (debug allow) 191 | (deny default) 192 | 193 | ; v8 needs to read the kernel version 194 | (allow sysctl-read) 195 | 196 | ; nodejs needs to run cwd() 197 | (allow file-read* (literal "${process.cwd()}")) 198 | 199 | ; nodejs needs to read the runtime scripts folder as well as the node_modules dir 200 | ; this appears to require running lstat up the path chain (presumably to check for symlinks?) 201 | (allow file-read* (subpath "${dirname(sbx.runtimePath)}")) 202 | (allow file-read* (subpath "${sbx.opts.nodeModulesPath}")) 203 | ${runtimePathVariations.map(str => `(allow file-read-metadata (literal "${str}"))`).join('\n')} 204 | 205 | ; the runtime needs to be able to connect to the ipc socket 206 | (allow network* (local ip "*:${sbx.ipcServerPort}")) 207 | (allow network* (local ip "*:${sbx.ipcClientPort}")) 208 | 209 | ; allow nodejs to run 210 | (allow process-exec (literal "${process.execPath}"))` 211 | 212 | const childProcess = doSpawn(sbx.opts, 213 | 'sandbox-exec', 214 | [ 215 | '-p', profile, 216 | process.execPath, sbx.runtimePath, sbx.opts.runtime, sbx.ipcServerPort, sbx.ipcClientPort 217 | ] 218 | ) 219 | return childProcess 220 | } 221 | 222 | function doSpawn (extraOpts, cmd, args) { 223 | if (extraOpts.strace) { 224 | args.unshift(cmd) 225 | args.unshift('-c') 226 | cmd = 'strace' 227 | } 228 | if (extraOpts.logSpawn) { 229 | console.log('SPAWN', cmd, args) 230 | } 231 | return spawn(cmd, args) 232 | } 233 | 234 | function toGlobalsMap (obj, path = []) { 235 | if (typeof obj !== 'object') { 236 | return {[path.join('.')]: obj} 237 | } else { 238 | return Object.entries(obj) 239 | .map(([key, value]) => toGlobalsMap(value, [...path, key])) 240 | .reduce((acc, v) => Object.assign(acc, v), {}) 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /lib/runtime/_seccomp.cjs: -------------------------------------------------------------------------------- 1 | exports.ALLOWED_SYSCALLS = [ 2 | // "accept", 3 | // "accept4", 4 | // "access", 5 | // "alarm", 6 | // "arch_prctl", 7 | // "bind", 8 | "brk", 9 | // "capget", 10 | // "capset", 11 | // "chdir", 12 | // "chmod", 13 | // "chown", 14 | // "chown32", 15 | // "chroot", 16 | // "clock_getres", 17 | // "clock_gettime", 18 | // "clock_nanosleep", 19 | "clone", 20 | "close", 21 | // "connect", 22 | // "copy_file_range", 23 | // "creat", 24 | // "dup", 25 | // "dup2", 26 | // "dup3", 27 | "epoll_create", 28 | // "epoll_create1", 29 | "epoll_ctl", 30 | // "epoll_ctl_old", 31 | // "epoll_pwait", 32 | "epoll_wait", 33 | // "epoll_wait_old", 34 | // "eventfd", 35 | // "eventfd2", 36 | // "execve", 37 | // "execveat", 38 | "exit", 39 | "exit_group", 40 | // "faccessat", 41 | // "fadvise64", 42 | // "fadvise64_64", 43 | // "fallocate", 44 | // "fanotify_mark", 45 | // "fchdir", 46 | // "fchmod", 47 | // "fchmodat", 48 | // "fchown", 49 | // "fchown32", 50 | // "fchownat", 51 | "fcntl", 52 | "fcntl64", 53 | // "fdatasync", 54 | // "fgetxattr", 55 | // "flistxattr", 56 | // "flock", 57 | // "fork", 58 | // "fremovexattr", 59 | // "fsetxattr", 60 | "fstat", 61 | "fstat64", 62 | "fstatat64", 63 | // "fstatfs", 64 | // "fstatfs64", 65 | // "fsync", 66 | // "ftruncate", 67 | // "ftruncate64", 68 | "futex", 69 | // "futimesat", 70 | // "getcpu", 71 | "getcwd", 72 | // "getdents", 73 | // "getdents64", 74 | // "getegid", 75 | // "getegid32", 76 | // "geteuid", 77 | // "geteuid32", 78 | // "getgid", 79 | // "getgid32", 80 | // "getgroups", 81 | // "getgroups32", 82 | // "getitimer", 83 | // "getpeername", 84 | // "getpgid", 85 | // "getpgrp", 86 | "getpid", 87 | "getppid", 88 | // "getpriority", 89 | // "getrandom", 90 | // "getresgid", 91 | // "getresgid32", 92 | // "getresuid", 93 | // "getresuid32", 94 | // "getrlimit", 95 | // "get_robust_list", 96 | // "getrusage", 97 | // "getsid", 98 | "getsockname", 99 | "getsockopt", 100 | // "get_thread_area", 101 | // "gettid", 102 | // "gettimeofday", 103 | // "getuid", 104 | // "getuid32", 105 | // "getxattr", 106 | // "inotify_add_watch", 107 | // "inotify_init", 108 | // "inotify_init1", 109 | // "inotify_rm_watch", 110 | // "io_cancel", 111 | "ioctl", 112 | // "io_destroy", 113 | // "io_getevents", 114 | // "ioprio_get", 115 | // "ioprio_set", 116 | // "io_setup", 117 | // "io_submit", 118 | // "ipc", 119 | // "kill", 120 | // "lchown", 121 | // "lchown32", 122 | // "lgetxattr", 123 | // "link", 124 | // "linkat", 125 | // "listen", 126 | // "listxattr", 127 | // "llistxattr", 128 | // "_llseek", 129 | // "lremovexattr", 130 | // "lseek", 131 | // "lsetxattr", 132 | // "lstat", 133 | // "lstat64", 134 | "madvise", 135 | // "memfd_create", 136 | // "mincore", 137 | // "mkdir", 138 | // "mkdirat", 139 | // "mknod", 140 | // "mknodat", 141 | // "mlock", 142 | // "mlock2", 143 | // "mlockall", 144 | "mmap", 145 | // "mmap2", 146 | // "modify_ldt", 147 | "mprotect", 148 | // "mq_getsetattr", 149 | // "mq_notify", 150 | // "mq_open", 151 | // "mq_timedreceive", 152 | // "mq_timedsend", 153 | // "mq_unlink", 154 | // "mremap", 155 | // "msgctl", 156 | // "msgget", 157 | // "msgrcv", 158 | // "msgsnd", 159 | // "msync", 160 | // "munlock", 161 | // "munlockall", 162 | "munmap", 163 | // "nanosleep", 164 | // "newfstatat", 165 | // "_newselect", 166 | // "open", 167 | "openat", 168 | // "pause", 169 | // "personality", 170 | // "pipe", 171 | // "pipe2", 172 | // "poll", 173 | // "ppoll", 174 | // "prctl", 175 | "pread64", 176 | // "preadv", 177 | "prlimit64", 178 | // "pselect6", 179 | // "pwrite64", 180 | // "pwritev", 181 | "read", 182 | // "readahead", 183 | // "readlink", 184 | // "readlinkat", 185 | // "readv", 186 | // "recv", 187 | // "recvfrom", 188 | // "recvmmsg", 189 | // "recvmsg", 190 | // "remap_file_pages", 191 | // "removexattr", 192 | // "rename", 193 | // "renameat", 194 | // "renameat2", 195 | // "restart_syscall", 196 | // "rmdir", 197 | "rt_sigaction", 198 | "rt_sigpending", 199 | "rt_sigprocmask", 200 | "rt_sigqueueinfo", 201 | "rt_sigreturn", 202 | "rt_sigsuspend", 203 | "rt_sigtimedwait", 204 | "rt_tgsigqueueinfo", 205 | // "sched_getaffinity", 206 | // "sched_getattr", 207 | // "sched_getparam", 208 | // "sched_get_priority_max", 209 | // "sched_get_priority_min", 210 | // "sched_getscheduler", 211 | // "sched_rr_get_interval", 212 | // "sched_setaffinity", 213 | // "sched_setattr", 214 | // "sched_setparam", 215 | // "sched_setscheduler", 216 | // "sched_yield", 217 | // "seccomp", 218 | // "select", 219 | // "semctl", 220 | // "semget", 221 | // "semop", 222 | // "semtimedop", 223 | // "send", 224 | // "sendfile", 225 | // "sendfile64", 226 | // "sendmmsg", 227 | "sendmsg", 228 | // "sendto", 229 | // "setfsgid", 230 | // "setfsgid32", 231 | // "setfsuid", 232 | // "setfsuid32", 233 | // "setgid", 234 | // "setgid32", 235 | // "setgroups", 236 | // "setgroups32", 237 | // "setitimer", 238 | // "setpgid", 239 | // "setpriority", 240 | // "setregid", 241 | // "setregid32", 242 | // "setresgid", 243 | // "setresgid32", 244 | // "setresuid", 245 | // "setresuid32", 246 | // "setreuid", 247 | // "setreuid32", 248 | // "setrlimit", 249 | "set_robust_list", 250 | // "setsid", 251 | // "setsockopt", 252 | // "set_thread_area", 253 | // "set_tid_address", 254 | // "setuid", 255 | // "setuid32", 256 | // "setxattr", 257 | // "shmat", 258 | // "shmctl", 259 | // "shmdt", 260 | // "shmget", 261 | // "shutdown", 262 | // "sigaltstack", 263 | // "signalfd", 264 | // "signalfd4", 265 | // "sigreturn", 266 | // "socket", 267 | // "socketcall", 268 | // "socketpair", 269 | // "splice", 270 | "statx", 271 | // "stat", 272 | // "stat64", 273 | // "statfs", 274 | // "statfs64", 275 | // "symlink", 276 | // "symlinkat", 277 | // "sync", 278 | // "sync_file_range", 279 | // "syncfs", 280 | // "sysinfo", 281 | // "syslog", 282 | // "tee", 283 | // "tgkill", 284 | // "time", 285 | // "timer_create", 286 | // "timer_delete", 287 | // "timerfd_create", 288 | // "timerfd_gettime", 289 | // "timerfd_settime", 290 | // "timer_getoverrun", 291 | // "timer_gettime", 292 | // "timer_settime", 293 | // "times", 294 | // "tkill", 295 | // "truncate", 296 | // "truncate64", 297 | // "ugetrlimit", 298 | // "umask", 299 | // "uname", 300 | // "unlink", 301 | // "unlinkat", 302 | // "utime", 303 | // "utimensat", 304 | // "utimes", 305 | // "vfork", 306 | // "vmsplice", 307 | // "wait4", 308 | // "waitid", 309 | // "waitpid", 310 | "write", 311 | "writev" 312 | ] -------------------------------------------------------------------------------- /lib/runtime/exec.cjs: -------------------------------------------------------------------------------- 1 | const seccomp = process.platform === 'linux' ? require('node-seccomp') : undefined 2 | const { ALLOWED_SYSCALLS } = process.platform === 'linux' ? require('./_seccomp.cjs') : {} 3 | const { IPC } = require('./ipc.cjs') 4 | const { pack, unpack } = require('msgpackr') 5 | const { connect } = require('net') 6 | const _set = require('lodash.set') 7 | 8 | const noSandbox = process.argv.includes('--no-sandbox') 9 | const runtimeModule = process.argv[2] 10 | const ipcServerPort = process.argv[3] 11 | const ipcClientPort = process.argv[4] 12 | const Runtime = require(runtimeModule) 13 | const conn = connect({port: Number(ipcServerPort), family: 4, localAddress: '127.0.0.1', localPort: Number(ipcClientPort), lookup: (hostname, opts, cb) => cb(null, '127.0.0.1', 4)}) 14 | 15 | class ContainerManager { 16 | constructor () { 17 | this._cidInc = 0 18 | this.containers = new Map() 19 | } 20 | 21 | list () { 22 | const containers = Object.values(this.containers) 23 | return { 24 | containers: containers.map(c => ({cid: c.id, opts: c.opts})) 25 | } 26 | } 27 | 28 | async exec (opts) { 29 | if (typeof opts?.source === 'undefined') { 30 | throw new Error(`source must be provided`) 31 | } 32 | opts.globals = {} 33 | if (Array.isArray(opts.globalsMap)) { 34 | opts.globalsMap.forEach(path => { 35 | _set(opts.globals, path, async (...params) => unpack(await ipc.request(0, pack({method: path, params})))) 36 | }) 37 | } 38 | const container = { 39 | cid: ++this._cidInc, 40 | opts, 41 | runtime: new Runtime(opts) 42 | } 43 | this.containers.set(container.cid, container) 44 | container.runtime.on('closed', (exitCode) => this.onClose(container, exitCode)) 45 | await container.runtime.init() 46 | await container.runtime.run().catch(e => this.onRunError(container, e)) 47 | return {cid: container.cid} 48 | } 49 | 50 | config ({cid, opts}) { 51 | const container = this.containers.get(cid) 52 | if (!container) throw new Error(`Container not found: ${cid}`) 53 | container.runtime.configure(opts) 54 | } 55 | 56 | kill ({cid}) { 57 | const container = this.containers.get(cid) 58 | if (!container) throw new Error(`Container not found: ${cid}`) 59 | container.runtime.close() 60 | } 61 | 62 | killAll () { 63 | for (const container of Object.values(this.containers)) { 64 | container.runtime?.close?.() 65 | } 66 | } 67 | 68 | onClose (container, exitCode) { 69 | this.containers.delete(container.cid) 70 | ipc.notify(0, pack({method: '__notify_container_closed', params: {cid: container.cid, exitCode}})) 71 | } 72 | 73 | onRunError (container, error) { 74 | this.containers.delete(container.cid) 75 | ipc.notify(0, pack({method: '__notify_container_closed', params: {cid: container.cid, error: {name: error.name || 'Error', message: error.message || error.toString(), details: error.details}}})) 76 | } 77 | } 78 | 79 | const manager = new ContainerManager() 80 | 81 | const ipc = new IPC(conn, conn, async (cid, body) => { 82 | if (cid === 0) { 83 | try { 84 | const {method, params} = unpack(body) 85 | switch (method) { 86 | case '__list_containers': { 87 | return pack(manager.list()) 88 | } 89 | case '__exec_container': { 90 | return pack(await manager.exec(params)) 91 | } 92 | case '__config_container': { 93 | return pack(await manager.config(params)) 94 | } 95 | case '__kill_container': { 96 | return pack(manager.kill(params)) 97 | } 98 | default: 99 | throw new Error(`Method not found: ${method}`) 100 | } 101 | } catch (e) { 102 | throw pack({name: e.constructor?.name || 'Error', message: e.message || e.name, details: e.details}) 103 | } 104 | } else { 105 | const container = manager.containers.get(cid) 106 | if (!container) throw pack({message: `No container found with cid ${cid}`}) 107 | try { 108 | const {method, params} = unpack(body) 109 | return pack(await container.runtime.handleAPICall(method, params)) 110 | } catch (e) { 111 | throw pack({name: e.constructor?.name || 'Error', message: e.message || e.name, details: e.details}) 112 | } 113 | } 114 | }) 115 | 116 | if (seccomp && !noSandbox) { 117 | const sc = seccomp.NodeSeccomp() 118 | sc.init(seccomp.SCMP_ACT_KILL_PROCESS) 119 | for (const syscall of ALLOWED_SYSCALLS) { 120 | sc.ruleAdd(seccomp.SCMP_ACT_ALLOW, syscall) 121 | } 122 | sc.load() 123 | } -------------------------------------------------------------------------------- /lib/runtime/ipc.cjs: -------------------------------------------------------------------------------- 1 | const cenc = require('compact-encoding') 2 | const frame = require('frame-stream') 3 | const EventEmitter = require('events') 4 | const { pack } = require('msgpackr') 5 | 6 | const MSGTYPE_REQUEST = 100 7 | const MSGTYPE_RESPONSE = 101 8 | const MSGTYPE_NOTIFY = 102 9 | const EMPTY_BUFFER = Buffer.from([0]) 10 | 11 | exports.IPC = class IPC extends EventEmitter { 12 | constructor (inStream, outStream, requestHandler) { 13 | super() 14 | this._reqIdCtr = 0 15 | this._reqPromises = new Map() 16 | this.requestHandler = requestHandler 17 | 18 | this.framedOutStream = frame.encode() 19 | this.framedOutStream.pipe(outStream) 20 | this.framedInStream = inStream.pipe(frame.decode()) 21 | this.framedInStream.on('data', this._onmsg.bind(this)) 22 | inStream.on('close', this._onclose.bind(this)) 23 | outStream.on('close', this._onclose.bind(this)) 24 | } 25 | 26 | _onclose () { 27 | this.framedOutStream?.end() 28 | this.framedInStream?.end() 29 | this._reqPromises.forEach(req => { 30 | req.reject(pack({message: 'Connection closed prematurely'})) 31 | }) 32 | } 33 | 34 | request (cid, body) { 35 | const reqId = ++this._reqIdCtr 36 | body = typeof body !== 'undefined' ? body : EMPTY_BUFFER 37 | 38 | const state = cenc.state() 39 | cenc.uint.preencode(state, MSGTYPE_REQUEST) 40 | cenc.uint.preencode(state, reqId) 41 | cenc.uint.preencode(state, cid) 42 | cenc.buffer.preencode(state, body) 43 | 44 | state.buffer = Buffer.allocUnsafe(state.end) 45 | cenc.uint.encode(state, MSGTYPE_REQUEST) 46 | cenc.uint.encode(state, reqId) 47 | cenc.uint.encode(state, cid) 48 | cenc.buffer.encode(state, body) 49 | 50 | this.framedOutStream.write(state.buffer) 51 | 52 | return new Promise((resolve, reject) => { 53 | this._reqPromises.set(reqId, {resolve, reject}) 54 | }) 55 | } 56 | 57 | notify (cid, body) { 58 | body = typeof body !== 'undefined' ? body : EMPTY_BUFFER 59 | 60 | const state = cenc.state() 61 | cenc.uint.preencode(state, MSGTYPE_NOTIFY) 62 | cenc.uint.preencode(state, cid) 63 | cenc.buffer.preencode(state, body) 64 | 65 | state.buffer = Buffer.allocUnsafe(state.end) 66 | cenc.uint.encode(state, MSGTYPE_NOTIFY) 67 | cenc.uint.encode(state, cid) 68 | cenc.buffer.encode(state, body) 69 | 70 | this.framedOutStream.write(state.buffer) 71 | } 72 | 73 | _respond (reqId, success, body) { 74 | body = typeof body !== 'undefined' ? body : EMPTY_BUFFER 75 | 76 | const state = cenc.state() 77 | cenc.uint.preencode(state, MSGTYPE_RESPONSE) 78 | cenc.uint.preencode(state, reqId) 79 | cenc.bool.preencode(state, success) 80 | cenc.buffer.preencode(state, body) 81 | 82 | state.buffer = Buffer.allocUnsafe(state.end) 83 | cenc.uint.encode(state, MSGTYPE_RESPONSE) 84 | cenc.uint.encode(state, reqId) 85 | cenc.bool.encode(state, success) 86 | cenc.buffer.encode(state, body) 87 | 88 | // console.error('_respond', reqId, success, body) 89 | this.framedOutStream.write(state.buffer) 90 | } 91 | 92 | _onmsg (buffer) { 93 | // console.error('_onmsg', buffer) 94 | const state = {start: 0, end: buffer.length, buffer} 95 | const msgtype = cenc.uint.decode(state) 96 | if (msgtype === MSGTYPE_REQUEST) { 97 | const reqId = cenc.uint.decode(state) 98 | const cid = cenc.uint.decode(state) 99 | const body = cenc.buffer.decode(state) 100 | this.requestHandler(cid, body).then( 101 | body => this._respond(reqId, true, body), 102 | body => this._respond(reqId, false, body) 103 | ) 104 | } else if (msgtype === MSGTYPE_RESPONSE) { 105 | const reqId = cenc.uint.decode(state) 106 | const success = cenc.bool.decode(state) 107 | const body = cenc.buffer.decode(state) 108 | const promise = this._reqPromises.get(reqId) 109 | if (!promise) return console.error(`No response waiting for request ${reqId}`) 110 | if (success) promise.resolve(body) 111 | else promise.reject(body) 112 | } else if (msgtype === MSGTYPE_NOTIFY) { 113 | const cid = cenc.uint.decode(state) 114 | const body = cenc.buffer.decode(state) 115 | this.requestHandler(cid, body) 116 | } else { 117 | console.error(`Unknown message type ID: ${msgtype}`) 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "confine-sandbox", 3 | "version": "0.3.3", 4 | "description": "A NodeJS framework for creating sandboxed runtimes for untrusted code.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "ava -s test/*.js", 8 | "docker-build": "docker build -t sandbox-test .", 9 | "docker-test": "npm run docker-build && docker run sandbox-test sh -c \"./node_modules/.bin/ava test/eval.js\"" 10 | }, 11 | "dependencies": { 12 | "compact-encoding": "^2.5.1", 13 | "frame-stream": "^3.0.0", 14 | "get-port": "^6.0.0", 15 | "lodash.get": "^4.4.2", 16 | "lodash.set": "^4.3.2", 17 | "msgpackr": "^1.5.1" 18 | }, 19 | "optionalDependencies": { 20 | "node-seccomp": "pfrazee/node-seccomp#087140c1a31581e6a9b861bafe2788f581a9c8fb" 21 | }, 22 | "keywords": [], 23 | "author": "Paul Frazee ", 24 | "license": "MIT", 25 | "devDependencies": { 26 | "ava": "^3.15.0", 27 | "jseval-confine-runtime": "^0.3.0", 28 | "jsisolate-confine-runtime": "^0.3.1" 29 | }, 30 | "directories": { 31 | "doc": "docs", 32 | "lib": "lib", 33 | "test": "test" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/confine-sandbox/confine.git" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/confine-sandbox/confine/issues" 41 | }, 42 | "homepage": "https://github.com/confine-sandbox/confine#readme" 43 | } 44 | -------------------------------------------------------------------------------- /test/_util/index.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | const fs = require('fs').promises 3 | const { Sandbox } = require('../../lib/index.js') 4 | const { unpack } = require('msgpackr') 5 | 6 | exports.genIsBlocked = (opts) => { 7 | return async function (t, input, expected) { 8 | const sbx = new Sandbox(opts) 9 | sbx.on('container-runtime-error', ({cid, error}) => { 10 | console.error('Script error:', error) 11 | }) 12 | await sbx.init() 13 | const runtimeOpts = opts.runtimeOpts || {} 14 | runtimeOpts.sourcePath = join(__dirname, '..', 'programs', input) 15 | try { 16 | const {cid} = await sbx.execContainer(runtimeOpts) 17 | await sbx.handleAPICall(cid, 'runTest', []).catch(e => e) 18 | } catch (e) {} 19 | if (expected) { 20 | await sbx.whenGuestProcessClosed 21 | t.falsy(sbx.isGuestProcessActive) 22 | if (process.platform === 'linux') { 23 | t.is(sbx.guestExitSignal, opts.noSandbox ? 'SIGTERM' : 'SIGSYS') 24 | } else { 25 | t.not(sbx.guestExitCode, 0) 26 | } 27 | } else { 28 | t.truthy(sbx.isGuestProcessActive) 29 | await sbx.teardown() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/eval-no-sandbox.js: -------------------------------------------------------------------------------- 1 | const ava = require('ava') 2 | const net = require('net') 3 | const { genIsBlocked } = require('./_util/index.js') 4 | 5 | // create a socket for attempted connections 6 | const sock = net.createServer(conn => conn.end()) 7 | sock.listen(17000) 8 | sock.unref() 9 | 10 | const isBlocked = genIsBlocked({ 11 | noSandbox: true, 12 | runtime: 'jseval-confine-runtime', 13 | globals: { 14 | console: { 15 | log: console.log.bind(console), 16 | error: console.error.bind(console), 17 | warn: console.warn.bind(console), 18 | debug: console.debug.bind(console) 19 | } 20 | } 21 | }) 22 | 23 | function allow (program) { 24 | ava(`${program} allowed`, isBlocked, program, false) 25 | } 26 | 27 | function deny (program) { 28 | ava(`${program} denied`, isBlocked, program, true) 29 | } 30 | 31 | allow('dns.js') 32 | allow('exec-echo.js') 33 | allow('exec-node.js') 34 | allow('net-bind-socket-to-file.js') 35 | allow('net-bind-socket-to-port.js') 36 | allow('net-connect-external.js') 37 | allow('net-connect-localhost.js') 38 | allow('noop.js') 39 | allow('read-cwd-files.js') 40 | allow('readdir-cwd.js') 41 | allow('readdir-home.js') 42 | allow('readdir-root.js') 43 | allow('stdout.js') -------------------------------------------------------------------------------- /test/eval.js: -------------------------------------------------------------------------------- 1 | const ava = require('ava') 2 | const net = require('net') 3 | const { genIsBlocked } = require('./_util/index.js') 4 | 5 | // create a socket for attempted connections 6 | const sock = net.createServer(conn => conn.end()) 7 | sock.listen(17000) 8 | sock.unref() 9 | 10 | const isBlocked = genIsBlocked({ 11 | runtime: 'jseval-confine-runtime', 12 | globals: { 13 | console: { 14 | log: console.log.bind(console), 15 | error: console.error.bind(console), 16 | warn: console.warn.bind(console), 17 | debug: console.debug.bind(console) 18 | } 19 | } 20 | }) 21 | 22 | function allow (program) { 23 | ava(`${program} allowed`, isBlocked, program, false) 24 | } 25 | 26 | function deny (program) { 27 | ava(`${program} denied`, isBlocked, program, true) 28 | } 29 | 30 | deny('dns.js') 31 | deny('exec-echo.js') 32 | deny('exec-node.js') 33 | deny('net-bind-socket-to-file.js') 34 | deny('net-bind-socket-to-port.js') 35 | deny('net-connect-external.js') 36 | deny('net-connect-localhost.js') 37 | allow('noop.js') 38 | deny('read-cwd-files.js') 39 | if (process.platform === 'darwin') { 40 | allow('readdir-cwd.js') 41 | } else { 42 | deny('readdir-cwd.js') 43 | } 44 | deny('readdir-home.js') 45 | deny('readdir-root.js') 46 | allow('stdout.js') -------------------------------------------------------------------------------- /test/isolate-no-sandbox.js: -------------------------------------------------------------------------------- 1 | const ava = require('ava') 2 | const net = require('net') 3 | const { genIsBlocked } = require('./_util/index.js') 4 | 5 | // create a socket for attempted connections 6 | const sock = net.createServer(conn => conn.end()) 7 | sock.listen(17000) 8 | sock.unref() 9 | 10 | const isBlocked = genIsBlocked({ 11 | runtime: 'jsisolate-confine-runtime', 12 | noSandbox: true, 13 | runtimeOpts: {env: 'nodejs'}, 14 | globals: { 15 | console: { 16 | log: console.log.bind(console), 17 | error: console.error.bind(console), 18 | warn: console.warn.bind(console), 19 | debug: console.debug.bind(console) 20 | } 21 | } 22 | }) 23 | 24 | function allow (program) { 25 | ava(`${program} allowed`, isBlocked, program, false) 26 | } 27 | 28 | function deny (program) { 29 | ava(`${program} denied`, isBlocked, program, true) 30 | } 31 | 32 | deny('dns.js') 33 | deny('exec-echo.js') 34 | deny('exec-node.js') 35 | deny('net-bind-socket-to-file.js') 36 | deny('net-bind-socket-to-port.js') 37 | deny('net-connect-external.js') 38 | deny('net-connect-localhost.js') 39 | allow('noop.js') 40 | deny('read-cwd-files.js') 41 | deny('readdir-cwd.js') 42 | deny('readdir-home.js') 43 | deny('readdir-root.js') 44 | allow('stdout.js') -------------------------------------------------------------------------------- /test/isolate.js: -------------------------------------------------------------------------------- 1 | const ava = require('ava') 2 | const net = require('net') 3 | const { genIsBlocked } = require('./_util/index.js') 4 | 5 | // create a socket for attempted connections 6 | const sock = net.createServer(conn => conn.end()) 7 | sock.listen(17000) 8 | sock.unref() 9 | 10 | const isBlocked = genIsBlocked({ 11 | runtime: 'jsisolate-confine-runtime', 12 | runtimeOpts: {env: 'nodejs'}, 13 | globals: { 14 | console: { 15 | log: console.log.bind(console), 16 | error: console.error.bind(console), 17 | warn: console.warn.bind(console), 18 | debug: console.debug.bind(console) 19 | } 20 | } 21 | }) 22 | 23 | function allow (program) { 24 | ava(`${program} allowed`, isBlocked, program, false) 25 | } 26 | 27 | function deny (program) { 28 | ava(`${program} denied`, isBlocked, program, true) 29 | } 30 | 31 | deny('dns.js') 32 | deny('exec-echo.js') 33 | deny('exec-node.js') 34 | deny('net-bind-socket-to-file.js') 35 | deny('net-bind-socket-to-port.js') 36 | deny('net-connect-external.js') 37 | deny('net-connect-localhost.js') 38 | allow('noop.js') 39 | deny('read-cwd-files.js') 40 | deny('readdir-cwd.js') 41 | deny('readdir-home.js') 42 | deny('readdir-root.js') 43 | allow('stdout.js') -------------------------------------------------------------------------------- /test/programs/dns.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runTest: () => { 3 | return new Promise(r => { 4 | try { 5 | require('dns').lookup('example.com', (err, res) => { 6 | if (err?.code === 'ENOTFOUND') { 7 | process.exit(1) // this is the expected failure 8 | } 9 | console.log('dns', err, res) 10 | r() 11 | }) 12 | } catch (e) { 13 | process.exit(1) 14 | } 15 | }) 16 | } 17 | } -------------------------------------------------------------------------------- /test/programs/exec-echo.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runTest: () => { 3 | return new Promise(r => { 4 | try { 5 | const {spawn} = require('child_process') 6 | spawn('echo', ['test'], {stdio: 'inherit'}) 7 | r() 8 | } catch (e) { 9 | process.exit(1) 10 | } 11 | }) 12 | } 13 | } -------------------------------------------------------------------------------- /test/programs/exec-node.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runTest: () => { 3 | return new Promise(r => { 4 | try { 5 | const {spawn} = require('child_process') 6 | spawn(process.execPath, ['-e', 'console.log("test")'], {stdio: 'inherit'}) 7 | r() 8 | } catch (e) { 9 | process.exit(1) 10 | } 11 | }) 12 | } 13 | } -------------------------------------------------------------------------------- /test/programs/fizzbuzz.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/confine-sandbox/confine/e135d62b0cbd77069a84271b022ab9b14cb193a1/test/programs/fizzbuzz.wasm -------------------------------------------------------------------------------- /test/programs/fizzbuzz.wat: -------------------------------------------------------------------------------- 1 | ;; https://github.com/rhmoller/wasm-by-hand/blob/master/src/wat/fizzbuzz.wat 2 | ;; MIT license (Copyright Rene Hangstrup Møller) 3 | 4 | (module 5 | (import "js" "memory" (memory 1)) 6 | (import "js" "println" (func $println (param i32 i32))) 7 | (data (i32.const 0) "Fizz") 8 | (data (i32.const 4) "Buzz") 9 | (data (i32.const 8) "FizzBuzz") 10 | 11 | (func (export "main") (param $max i32) 12 | (local $c i32) 13 | (local $tmp i32) 14 | (local $adr i32) 15 | 16 | (set_local $c (i32.const 1)) ;; start counting at 1 17 | (set_local $max (i32.add (get_local $max) (i32.const 1))) ;; adjust max, so we include the number provided by user 18 | 19 | (loop $loop 20 | (if (i32.eqz (i32.rem_u (get_local $c) (i32.const 3))) 21 | (then 22 | (if (i32.eqz (i32.rem_u (get_local $c) (i32.const 5))) 23 | (then (call $println (i32.const 8) (i32.const 8))) ;; fizzbuzz 24 | (else (call $println (i32.const 0) (i32.const 4))) ;; fizz 25 | ) 26 | ) 27 | (else 28 | (if (i32.eqz (i32.rem_u (get_local $c) (i32.const 5))) 29 | (then (call $println (i32.const 4) (i32.const 4))) ;; buzz 30 | (else 31 | (set_local $adr (i32.const 16)) 32 | (set_local $tmp (get_local $c)) 33 | 34 | ;; if 10 or larger, put number of 10s in first memory position 35 | (if (i32.gt_u (get_local $c) (i32.const 9)) 36 | (then 37 | (i32.store 38 | (get_local $adr) 39 | (i32.add 40 | (i32.div_u (get_local $tmp) (i32.const 10)) ;; number of tens 41 | (i32.const 48) ;; convert to ASCII digit 42 | ) 43 | ) 44 | 45 | ;; move address pointer to where next digit will be written 46 | (set_local $adr (i32.add (get_local $adr) (i32.const 1))) 47 | ) 48 | ) 49 | 50 | ;; store remainder (after division by 10) in next memory position 51 | (i32.store 52 | (get_local $adr) 53 | (i32.add 54 | (i32.rem_u (get_local $tmp) (i32.const 10)) ;; number of ones 55 | (i32.const 48) ;; convert to ASCII digit 56 | ) 57 | ) 58 | 59 | ;; print constructed string from memory 60 | (call $println (i32.const 16) (i32.sub (get_local $adr) (i32.const 15))) 61 | ) 62 | ) 63 | ) 64 | ) 65 | 66 | ;; increment counter 67 | (set_local $c (i32.add (get_local $c) (i32.const 1))) 68 | 69 | ;; loop until counter reaches max 70 | (br_if $loop (i32.lt_u (get_local $c) (get_local $max))) 71 | ) 72 | ) 73 | ) -------------------------------------------------------------------------------- /test/programs/net-bind-socket-to-file.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runTest: () => { 3 | return new Promise(r => { 4 | try { 5 | const net = require('net') 6 | const server = net.createServer((c) => {}) 7 | server.listen('/tmp/server' + Date.now() + '.sock', () => { 8 | console.log('server bound') 9 | r() 10 | }) 11 | } catch (e) { 12 | process.exit(1) 13 | } 14 | }) 15 | } 16 | } -------------------------------------------------------------------------------- /test/programs/net-bind-socket-to-port.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runTest: () => { 3 | return new Promise(r => { 4 | try { 5 | const net = require('net') 6 | const server = net.createServer((c) => {}) 7 | server.listen(12345, () => { 8 | console.log('server bound') 9 | r() 10 | }) 11 | } catch (e) { 12 | process.exit(1) 13 | } 14 | }) 15 | } 16 | } -------------------------------------------------------------------------------- /test/programs/net-connect-external.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runTest: () => { 3 | return new Promise(r => { 4 | try { 5 | const sock = require('net').connect(80, 'example.com') 6 | sock.on('connect', () => { 7 | console.log('socket connected') 8 | r() 9 | }) 10 | sock.on('error', err => { 11 | if (err.code === 'ENOTFOUND') { 12 | process.exit(1) // this is the expected failure 13 | } 14 | console.log('socket error', err) 15 | }) 16 | sock.on('end', () => console.log('socket ended')) 17 | } catch (e) { 18 | if (e.toString().includes('connect is not a function')) { 19 | process.exit(1) 20 | } 21 | console.log(e) 22 | } 23 | }) 24 | } 25 | } -------------------------------------------------------------------------------- /test/programs/net-connect-localhost.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runTest: () => { 3 | return new Promise(r => { 4 | try { 5 | const sock = require('net').connect(17000) 6 | sock.on('connect', () => { 7 | console.log('socket connected') 8 | r() 9 | }) 10 | sock.on('error', err => { 11 | if (err.code === 'ENOTFOUND') { 12 | process.exit(1) // this is the expected failure 13 | } 14 | console.log('socket error', err) 15 | }) 16 | sock.on('end', () => console.log('socket ended')) 17 | } catch (e) { 18 | if (e.toString().includes('connect is not a function')) { 19 | process.exit(1) 20 | } 21 | console.log(e) 22 | } 23 | }) 24 | } 25 | } -------------------------------------------------------------------------------- /test/programs/noop.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runTest: () => { 3 | // do nothing 4 | } 5 | } -------------------------------------------------------------------------------- /test/programs/read-cwd-files.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runTest: () => { 3 | try { 4 | const {join} = require('path') 5 | const fs = require('fs') 6 | const names = fs.readdirSync(process.cwd()) 7 | for (const name of names) { 8 | const path = join(process.cwd(), name) 9 | if (fs.statSync(path).isFile()) { 10 | console.log(typeof fs.readFileSync(path)) 11 | } 12 | } 13 | } catch (e) { 14 | if (e.code === 'EPERM' || e.toString().includes('readdirSync is not a function')) { 15 | process.exit(1) 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /test/programs/readdir-cwd.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runTest: () => { 3 | try { 4 | const fs = require('fs') 5 | console.log(fs.readdirSync(process.cwd())) 6 | } catch (e) { 7 | if (e.toString().includes('readdirSync is not a function')) { 8 | process.exit(1) 9 | } 10 | console.log(e) 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /test/programs/readdir-home.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runTest: () => { 3 | try { 4 | const fs = require('fs') 5 | console.log(fs.readdirSync(require('os').homedir())) 6 | } catch (e) { 7 | if (e.code === 'EPERM' || e.toString().includes('homedir is not a function')) { 8 | process.exit(1) 9 | } 10 | console.log(e) 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /test/programs/readdir-root.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runTest: () => { 3 | try { 4 | const fs = require('fs') 5 | fs.readdirSync('/') 6 | } catch (e) { 7 | if (e.code === 'EPERM' || e.toString().includes('readdirSync is not a function')) { 8 | process.exit(1) 9 | } 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /test/programs/stdout.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runTest: () => { 3 | console.log('Hello from this very secure script') 4 | } 5 | } -------------------------------------------------------------------------------- /test/wasm-no-sandbox.js: -------------------------------------------------------------------------------- 1 | const ava = require('ava') 2 | const { genIsBlocked } = require('./_util/index.js') 3 | 4 | const isBlocked = genIsBlocked({runtime: 'simplewasm-confine-runtime', noSandbox: true}) 5 | 6 | function allow (program) { 7 | ava(`${program} allowed`, isBlocked, program, false) 8 | } 9 | 10 | function deny (program) { 11 | ava(`${program} denied`, isBlocked, program, true) 12 | } 13 | 14 | // TODO 15 | // allow('fizzbuzz.wasm') -------------------------------------------------------------------------------- /test/wasm.js: -------------------------------------------------------------------------------- 1 | const ava = require('ava') 2 | const { genIsBlocked } = require('./_util/index.js') 3 | 4 | const isBlocked = genIsBlocked({runtime: 'simplewasm-confine-runtime'}) 5 | 6 | function allow (program) { 7 | ava(`${program} allowed`, isBlocked, program, false) 8 | } 9 | 10 | function deny (program) { 11 | ava(`${program} denied`, isBlocked, program, true) 12 | } 13 | 14 | // TODO 15 | // allow('fizzbuzz.wasm') --------------------------------------------------------------------------------