# Security model with sandboxing: # # Buffer processes are the most security-sensitive, since they parse # various resources retrieved from the network (CSS, HTML) and sometimes # even execute untrusted code (JS, with an engine written in C). So the # main goal is to give buffers as few permissions as possible. # # On FreeBSD, we create a file descriptor to the directory sockets # reside in, and then use that for manipulating our sockets. # # Capsicum does not enable more fine-grained capability control, but # in practice the things it does enable should not be enough to harm the # user's system. # # On OpenBSD, we pledge the minimum amount of promises we need, and # do not unveil anything. It seems to be roughly equivalent to the # security we get with FreeBSD Capsicum. # # On Linux, we use libseccomp so that I don't have to manually write # BPF filters. # Sandboxing on Linux is at the moment slightly less safe than on the # two BSDs, because a rogue buffer could in theory connect to whatever # open UNIX domain socket on the system that the user has access to. #TODO look into integrating Landlock to fix this. # # We do not have OS-level sandboxing on other systems (yet). # # Aside from sandboxing in buffer processes, we also have a more # restrictive "network" sandbox that is intended for CGI processes that # just read/write from/to the network and stdin/stdout. At the moment this # is only used in the HTTP process. #TODO add it to more CGI scripts const disableSandbox {.booldefine.} = false type SandboxType* = enum stNone = "no sandbox" stCapsicum = "capsicum" stPledge = "pledge" stLibSeccomp = "libseccomp" const SandboxMode* = when disableSandbox: stNone elif defined(freebsd): stCapsicum elif defined(openbsd): stPledge elif defined(linux): stLibSeccomp else: stNone when SandboxMode == stCapsicum: import bindings/capsicum proc enterBufferSandbox*(sockPath: string) = # per man:cap_enter(2), it may return ENOSYS if the kernel was compiled # without CAPABILITY_MODE. So it seems better not to panic in this case. # (But TODO: when we get enough sandboxing coverage it should print a # warning or something.) discard cap_enter() proc enterNetworkSandbox*() = # no difference between buffer; Capsicum is quite straightforward # to use in this regard. discard cap_enter() elif SandboxMode == stPledge: import bindings/pledge proc enterBufferSandbox*(sockPath: string) = # take whatever we need to # * fork # * connect to UNIX domain sockets # * take FDs from the main process doAssert pledge("unix stdio sendfd recvfd proc", nil) == 0 proc enterNetworkSandbox*() = # we don't need much to write out data from sockets to stdout. doAssert pledge("stdio", nil) == 0 elif SandboxMode == stLibSeccomp: import std/posix import bindings/libseccomp when defined(android): let PR_SET_VMA {.importc, header: "", nodecl.}: cint let PR_SET_VMA_ANON_NAME {.importc, header: "", nodecl.}: cint proc allowBionic(ctx: scmp_filter_ctx) = # Things needed for bionic libc. Tested with Termux. const androidAllowList = [ cstring"rt_sigprocmask", "epoll_pwait", "futex", "madvise" ] for it in androidAllowList: let syscall = seccomp_syscall_resolve_name(it) doAssert seccomp_rule_add(ctx, SCMP_ACT_ALLOW, syscall, 0) == 0 # bionic likes to set this very much. In fact, it was added to # the kernel by Android devs. block allowAnonVMAName: let syscall = seccomp_syscall_resolve_name("prctl") let arg0 = scmp_arg_cmp( arg: 0, # op op: SCMP_CMP_EQ, # equals datum_a: uint64(PR_SET_VMA) ) let arg1 = scmp_arg_cmp( arg: 1, # attr op: SCMP_CMP_EQ, # equals datum_a: uint64(PR_SET_VMA_ANON_NAME) ) doAssert seccomp_rule_add(ctx, SCMP_ACT_ALLOW, syscall, 2, arg0, arg1) == 0 # We have to be car