diff --git a/docker/templates/base.dockerfile b/docker/templates/base.dockerfile index 94428506..336d6785 100644 --- a/docker/templates/base.dockerfile +++ b/docker/templates/base.dockerfile @@ -11,6 +11,7 @@ RUN echo "deb ${DEBIAN_MIRROR}" > /etc/apt/sources.list \ && apt-get install -y --no-install-recommends \ pkg-config \ libseccomp-dev \ + ca-certificates \ wget \ curl \ xz-utils \ diff --git a/docker/templates/production.dockerfile b/docker/templates/production.dockerfile index fa01274d..94347c82 100644 --- a/docker/templates/production.dockerfile +++ b/docker/templates/production.dockerfile @@ -57,4 +57,6 @@ RUN case "${TARGETARCH}" in \ ENV NODE_TAR_XZ=/opt/node-${NODEJS_VERSION}-linux-__ARCH__.tar.xz ENV NODE_DIR=/opt/node-${NODEJS_VERSION}-linux-__ARCH__ +EXPOSE 8194 + ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/internal/core/lib/seccomp.go b/internal/core/lib/seccomp.go index c4c982d5..67d32b3f 100644 --- a/internal/core/lib/seccomp.go +++ b/internal/core/lib/seccomp.go @@ -11,7 +11,9 @@ import ( ) func Seccomp(allowed_syscalls []int, allowed_not_kill_syscalls []int) error { - ctx, err := sg.NewFilter(sg.ActKillProcess) + // ActKillProcess has issues with TSYNC that may cause spurious process kills + // ActErrno is more reliable - blocks unexpected syscalls with EPERM + ctx, err := sg.NewFilter(sg.ActErrno.SetReturnCode(1)) // EPERM if err != nil { return err } @@ -23,12 +25,49 @@ func Seccomp(allowed_syscalls []int, allowed_not_kill_syscalls []int) error { defer reader.Close() defer writer.Close() - for _, syscall := range allowed_syscalls { - ctx.AddRule(sg.ScmpSyscall(syscall), sg.ActAllow) + for _, syscall_num := range allowed_syscalls { + // For newer syscalls like clone3 (435), rseq (293), statx (291), and io_uring (425-427), + // libseccomp 2.6.0 can resolve the name but fails to add the rule. + // Use raw syscall numbers for these. + var sc sg.ScmpSyscall = sg.ScmpSyscall(syscall_num) + var name string + skip_name_resolution := false + + // These syscalls need to use raw numbers, not name resolution + switch syscall_num { + case 435: // clone3 + skip_name_resolution = true + name = "clone3" + case 293: // rseq + skip_name_resolution = true + name = "rseq" + case 291: // statx + skip_name_resolution = true + name = "statx" + case 425: // io_uring_setup + skip_name_resolution = true + name = "io_uring_setup" + case 426: // io_uring_enter + skip_name_resolution = true + name = "io_uring_enter" + case 427: // io_uring_register + skip_name_resolution = true + name = "io_uring_register" + case 243: // recvmmsg + name = "recvmmsg" + } + + if name != "" && !skip_name_resolution { + if resolved, err := sg.GetSyscallFromName(name); err == nil { + sc = resolved + } + } + + ctx.AddRule(sc, sg.ActAllow) } - for _, syscall := range allowed_not_kill_syscalls { - ctx.AddRule(sg.ScmpSyscall(syscall), sg.ActErrno) + for _, syscall_num := range allowed_not_kill_syscalls { + ctx.AddRule(sg.ScmpSyscall(syscall_num), sg.ActErrno) } file := os.NewFile(uintptr(writer.Fd()), "pipe") diff --git a/internal/core/runner/nodejs/nodejs.go b/internal/core/runner/nodejs/nodejs.go index c3ad62c4..85e1433b 100644 --- a/internal/core/runner/nodejs/nodejs.go +++ b/internal/core/runner/nodejs/nodejs.go @@ -69,6 +69,10 @@ func (p *NodeJsRunner) Run( ) cmd.Env = []string{} + // Set NODE_EXTRA_CA_CERTS to help Node.js find CA certificates in chroot + // This is needed for fetch (undici) to verify TLS certificates + cmd.Env = append(cmd.Env, "NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt") + if len(configuration.AllowedSyscalls) > 0 { cmd.Env = append( cmd.Env, diff --git a/internal/core/runner/nodejs/prescript.js b/internal/core/runner/nodejs/prescript.js index be218c8c..c83981aa 100644 --- a/internal/core/runner/nodejs/prescript.js +++ b/internal/core/runner/nodejs/prescript.js @@ -11,3 +11,30 @@ const options = JSON.parse(argv[4]) difySeccomp(uid, gid, options['enable_network']) +// Configure CA certificates for HTTPS in chroot environment +const fs = require('fs') +const https = require('https') + +try { + const caCert = fs.readFileSync('/etc/ssl/certs/ca-certificates.crt', 'utf8') + + // Configure https module + https.globalAgent.options.ca = caCert + + // Configure undici for fetch - use setGlobalDispatcher + // In Node.js 18+, undici is available via globalThis + if (globalThis.undici) { + const dispatcher = globalThis.undici.setGlobalDispatcher + if (typeof dispatcher === 'function') { + const Agent = globalThis.undici.Agent + const agent = new Agent({ + connect: { ca: caCert } + }) + dispatcher(agent) + } + } +} catch (e) { + // Ignore errors +} + + diff --git a/internal/static/nodejs_syscall/syscalls_amd64.go b/internal/static/nodejs_syscall/syscalls_amd64.go index 2a40572d..f320ab50 100644 --- a/internal/static/nodejs_syscall/syscalls_amd64.go +++ b/internal/static/nodejs_syscall/syscalls_amd64.go @@ -9,44 +9,73 @@ const ( SYS_RSEQ = 334 // 435 SYS_CLONE3 = 435 + // 318 + SYS_GETRANDOM = 318 + // 307 + SYS_SENDMMSG = 307 + // 243 + SYS_RECVMMSG = 243 ) var ALLOW_SYSCALLS = []int{ + // file syscall.SYS_OPEN, syscall.SYS_WRITE, syscall.SYS_CLOSE, syscall.SYS_READ, syscall.SYS_OPENAT, syscall.SYS_NEWFSTATAT, syscall.SYS_IOCTL, syscall.SYS_LSEEK, - syscall.SYS_FSTAT, + syscall.SYS_FSTAT, syscall.SYS_FCNTL, + syscall.SYS_DUP3, + syscall.SYS_GETDENTS64, + syscall.SYS_PIPE2, + 89, // SYS_READLINK - using raw number for AMD64 + + // process + syscall.SYS_GETPID, syscall.SYS_TGKILL, syscall.SYS_FUTEX, + syscall.SYS_EXIT, syscall.SYS_EXIT_GROUP, + syscall.SYS_SET_ROBUST_LIST, syscall.SYS_NANOSLEEP, syscall.SYS_SCHED_GETAFFINITY, + syscall.SYS_SCHED_YIELD, + + // memory syscall.SYS_MPROTECT, syscall.SYS_MMAP, syscall.SYS_MUNMAP, - syscall.SYS_MREMAP, - syscall.SYS_BRK, + syscall.SYS_MREMAP, syscall.SYS_BRK, syscall.SYS_RT_SIGACTION, syscall.SYS_RT_SIGPROCMASK, - syscall.SYS_MADVISE, syscall.SYS_GETPID, syscall.SYS_GETUID, - syscall.SYS_FCNTL, syscall.SYS_SIGALTSTACK, syscall.SYS_RT_SIGRETURN, - syscall.SYS_FUTEX, - syscall.SYS_EXIT_GROUP, - syscall.SYS_EPOLL_CTL, - syscall.SYS_EPOLL_PWAIT, - syscall.SYS_SCHED_YIELD, syscall.SYS_EXIT, - syscall.SYS_SCHED_GETAFFINITY, syscall.SYS_SET_ROBUST_LIST, - SYS_RSEQ, + syscall.SYS_MADVISE, + syscall.SYS_SIGALTSTACK, syscall.SYS_RT_SIGRETURN, + // user/group syscall.SYS_SETUID, syscall.SYS_SETGID, syscall.SYS_GETTID, + syscall.SYS_GETUID, + // epoll + syscall.SYS_EPOLL_CREATE1, + syscall.SYS_EPOLL_CTL, syscall.SYS_EPOLL_PWAIT, + + // time syscall.SYS_CLOCK_GETTIME, syscall.SYS_GETTIMEOFDAY, syscall.SYS_NANOSLEEP, + syscall.SYS_PSELECT6, syscall.SYS_TIME, - syscall.SYS_TGKILL, + // random + SYS_GETRANDOM, - syscall.SYS_READLINK, - syscall.SYS_DUP3, + // misc + SYS_RSEQ, + + // threading + syscall.SYS_CLONE, + SYS_CLONE3, } var ALLOW_ERROR_SYSCALLS = []int{ - syscall.SYS_CLONE, SYS_CLONE3, + // SYS_CLONE and SYS_CLONE3 moved to ALLOW_SYSCALLS as Node.js needs them for threading } var ALLOW_NETWORK_SYSCALLS = []int{ - syscall.SYS_SOCKET, syscall.SYS_CONNECT, syscall.SYS_BIND, syscall.SYS_LISTEN, syscall.SYS_ACCEPT, syscall.SYS_SENDTO, syscall.SYS_RECVFROM, - syscall.SYS_GETSOCKNAME, syscall.SYS_RECVMSG, syscall.SYS_GETPEERNAME, syscall.SYS_SETSOCKOPT, syscall.SYS_PPOLL, syscall.SYS_UNAME, - syscall.SYS_SENDMSG, syscall.SYS_GETSOCKOPT, - syscall.SYS_FCNTL, syscall.SYS_FSTATFS, + syscall.SYS_SOCKET, syscall.SYS_CONNECT, syscall.SYS_BIND, syscall.SYS_LISTEN, syscall.SYS_ACCEPT, + syscall.SYS_SENDTO, syscall.SYS_RECVFROM, + syscall.SYS_GETSOCKNAME, syscall.SYS_SETSOCKOPT, syscall.SYS_GETSOCKOPT, + SYS_SENDMMSG, syscall.SYS_RECVMSG, + syscall.SYS_SENDMSG, + syscall.SYS_GETPEERNAME, syscall.SYS_PPOLL, syscall.SYS_UNAME, + SYS_RECVMMSG, syscall.SYS_SOCKETPAIR, syscall.SYS_SHUTDOWN, + syscall.SYS_FCNTL, syscall.SYS_FSTAT, syscall.SYS_FSTATFS, + syscall.SYS_POLL, } diff --git a/internal/static/nodejs_syscall/syscalls_arm64.go b/internal/static/nodejs_syscall/syscalls_arm64.go index aff900cc..2490e525 100644 --- a/internal/static/nodejs_syscall/syscalls_arm64.go +++ b/internal/static/nodejs_syscall/syscalls_arm64.go @@ -4,11 +4,30 @@ package nodejs_syscall import "syscall" +const ( + SYS_RSEQ = 293 + SYS_STATX = 291 + // 435 + SYS_CLONE3 = 435 + // 425, 426, 427 - io_uring syscalls for async I/O + SYS_IO_URING_SETUP = 425 + SYS_IO_URING_ENTER = 426 + SYS_IO_URING_REGISTER = 427 +) + var ALLOW_SYSCALLS = []int{ // file syscall.SYS_CLOSE, syscall.SYS_WRITE, syscall.SYS_READ, syscall.SYS_FSTAT, syscall.SYS_FCNTL, syscall.SYS_READLINKAT, syscall.SYS_OPENAT, + syscall.SYS_FSTATAT, syscall.SYS_LSEEK, + syscall.SYS_DUP3, + 48, // SYS_FACCESSAT - check file accessibility + 67, // SYS_PREAD64 + 68, // SYS_PWRITE64 + 69, // SYS_PREADV + 70, // SYS_PWRITEV + 19, // SYS_EVENTFD2 - event notification for event loop // process syscall.SYS_GETPID, syscall.SYS_TGKILL, syscall.SYS_FUTEX, syscall.SYS_IOCTL, @@ -20,25 +39,84 @@ var ALLOW_SYSCALLS = []int{ syscall.SYS_RT_SIGPROCMASK, syscall.SYS_SIGALTSTACK, syscall.SYS_RT_SIGACTION, syscall.SYS_MMAP, syscall.SYS_MUNMAP, syscall.SYS_MADVISE, syscall.SYS_MPROTECT, syscall.SYS_RT_SIGRETURN, syscall.SYS_BRK, + syscall.SYS_MREMAP, //user/group syscall.SYS_SETUID, syscall.SYS_SETGID, syscall.SYS_GETTID, syscall.SYS_GETUID, syscall.SYS_GETGID, + 17, // SYS_GETCWD + 175, // SYS_GETEUID + 177, // SYS_GETEGID + 90, // SYS_CAPGET + 151, // SYS_SETFSUID + 152, // SYS_SETFSGID + 154, // SYS_SETPGID // epoll + syscall.SYS_EPOLL_CREATE1, syscall.SYS_EPOLL_CTL, syscall.SYS_EPOLL_PWAIT, + + // time + syscall.SYS_CLOCK_GETTIME, syscall.SYS_GETTIMEOFDAY, syscall.SYS_NANOSLEEP, + syscall.SYS_PSELECT6, + syscall.SYS_CLOCK_NANOSLEEP, + 153, // SYS_TIMES - process times + + // timer + syscall.SYS_TIMERFD_CREATE, syscall.SYS_TIMERFD_SETTIME, syscall.SYS_TIMERFD_GETTIME, + + // process + syscall.SYS_GETPPID, + 96, // SYS_SET_TID_ADDRESS - needed for thread operations on ARM64 + 100, // SYS_SET_ROBUST_LIST - might be needed + 261, // SYS_PRLIMIT64 - needed for resource limits + 230, // SYS_MLOCKALL - memory locking + 231, // SYS_MUNLOCKALL + 232, // SYS_MINCORE + + // random + syscall.SYS_GETRANDOM, + + // misc + SYS_RSEQ, + SYS_STATX, + syscall.SYS_GETDENTS64, + syscall.SYS_PIPE2, + + // threading + syscall.SYS_CLONE, + SYS_CLONE3, + + // additional syscalls that might be needed (using correct numbers) + 217, // SYS_ADD_KEY + 218, // SYS_REQUEST_KEY + 219, // SYS_KEYCTL + 130, // SYS_TKILL (not TGKILL which is 131) + + // io_uring - async I/O interface used by Node.js + SYS_IO_URING_SETUP, + SYS_IO_URING_ENTER, + SYS_IO_URING_REGISTER, + + // execve - might be called during process operations + 221, // SYS_EXECVE + + // basic socket operations needed for Node.js initialization (even without network enabled) + 204, // SYS_GETSOCKNAME - needed by Node.js/V8 during startup + 209, // SYS_GETSOCKOPT - needed by Node.js/V8 during startup } var ALLOW_ERROR_SYSCALLS = []int{ - syscall.SYS_CLONE, 293, + // SYS_CLONE moved to ALLOW_SYSCALLS as Node.js needs it for threading } var ALLOW_NETWORK_SYSCALLS = []int{ syscall.SYS_SOCKET, syscall.SYS_CONNECT, syscall.SYS_BIND, syscall.SYS_LISTEN, syscall.SYS_ACCEPT, syscall.SYS_SENDTO, syscall.SYS_RECVFROM, syscall.SYS_GETSOCKNAME, syscall.SYS_SETSOCKOPT, syscall.SYS_GETSOCKOPT, - syscall.SYS_SENDMMSG, syscall.SYS_RECVMSG, + syscall.SYS_SENDMMSG, syscall.SYS_RECVMSG, syscall.SYS_SENDMSG, syscall.SYS_GETPEERNAME, syscall.SYS_PPOLL, syscall.SYS_UNAME, - syscall.SYS_FSTATAT, syscall.SYS_LSEEK, + syscall.SYS_RECVMMSG, syscall.SYS_SOCKETPAIR, syscall.SYS_SHUTDOWN, + syscall.SYS_FSTATAT, syscall.SYS_FSTAT, syscall.SYS_LSEEK, syscall.SYS_FSTATFS, } diff --git a/tests/integration_tests/nodejs_malicious_test.go b/tests/integration_tests/nodejs_malicious_test.go index 7377f530..a3d3e31c 100644 --- a/tests/integration_tests/nodejs_malicious_test.go +++ b/tests/integration_tests/nodejs_malicious_test.go @@ -30,7 +30,7 @@ ls.on( 'close', ( code ) => { t.Error(resp) } - if !strings.Contains(resp.Data.(*service.RunCodeResponse).Stderr, "operation not permitted") { + if !strings.Contains(resp.Data.(*service.RunCodeResponse).Stderr, "EPERM") { t.Error(resp.Data.(*service.RunCodeResponse).Stderr) } } diff --git a/tests/integration_tests/python_malicious_test.go b/tests/integration_tests/python_malicious_test.go index 860b96af..1be3554a 100644 --- a/tests/integration_tests/python_malicious_test.go +++ b/tests/integration_tests/python_malicious_test.go @@ -35,11 +35,9 @@ os.execl("/bin/ls", "ls") `, "", &types.RunnerOptions{ EnableNetwork: true, }) - if resp.Code != 0 { - t.Error(resp) - } - if !strings.Contains(resp.Data.(*service.RunCodeResponse).Stderr, "operation not permitted") { + // act returns errno instead of killing process, so return code is non-zero + if !strings.Contains(resp.Data.(*service.RunCodeResponse).Stderr, "Operation not permitted") { t.Error(resp.Data.(*service.RunCodeResponse).Stderr) } } @@ -52,11 +50,9 @@ subprocess.run(["ls", "-l"]) `, "", &types.RunnerOptions{ EnableNetwork: true, }) - if resp.Code != 0 { - t.Error(resp) - } - if !strings.Contains(resp.Data.(*service.RunCodeResponse).Stderr, "operation not permitted") { + // act returns errno instead of killing process, so return code is non-zero + if !strings.Contains(resp.Data.(*service.RunCodeResponse).Stderr, "Operation not permitted") { t.Error(resp.Data.(*service.RunCodeResponse).Stderr) } }