about summary refs log blame commit diff stats
path: root/src/utils/sandbox.nim
blob: efc03e4988c63bf86af349188f330ea0c054eec2 (plain) (tree)
1
2
3
4
5
6
7
8
9








                                                                        





                                                                        

                                                                  

















                                                                          

















                                         

                          
                                              




                                                                            




                                                                     
                             




                                              
                                      
                                      
                                                              



                                                                  
                                 


















                                                                               
                                        


                                                        
                                             



                                                           
                                                        

                                  
                                   




                                                                       
                                                                         


                                                                     
                                                


                                                                            




















                                                                           
                                         



                                                                         
                                                        
                                                       
                                                                     
                               
                                                                            
                                             











                                                                               
     
                                                      



                                              
           
# 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

  proc enterBufferSandbox*(sockPath: string) =
    onSignal SIGSYS:
      discard sig
      raise newException(Defect, "Sandbox violation in buffer")
    let ctx = seccomp_init(SCMP_ACT_TRAP)
    doAssert pointer(ctx) != nil
    const allowList = [
      cstring"accept", # for incoming requests to our controlling socket
      "accept4", # for when accept is implemented as accept4
      "bind", # for outgoing requests to loader
      "brk", # memory allocation
      "clock_gettime", # used by QuickJS in atomics
      "clone", # for when fork is implemented as clone
      "close", # duh
      "connect", # for outgoing requests to loader
      "epoll_create", "epoll_create1", "epoll_ctl", "epoll_wait", # epoll stuff
      "eventfd", # used by Nim selectors
      "exit_group", # for quit
      "fcntl", "fcntl64", # for changing blocking status
      "fork", # for when fork is really fork
      "fstat", # glibc fread seems to call it
      "getpid", # for determining current PID after we fork
      "getrlimit", # glibc uses it after fork it seems
      "getsockname", # Nim needs it for connecting
      "gettimeofday", # used by QuickJS in Date.now()
      "lseek", # glibc calls lseek on open files at exit
      "mmap", # memory allocation
      "mmap2", # memory allocation
      "mremap", # memory allocation
      "munmap", # memory allocation
      "pipe", # for pipes to child process
      "pipe2", # for when pipe is implemented as pipe2
      "prlimit64", # for when getrlimit is implemented as prlimit64
      "read", "recv", "recvfrom", "recvmsg", # for reading from sockets
      "rt_sigreturn", # for when sigreturn is implemented as rt_sigreturn
      "send", "sendmsg", "sendto", # for writing to sockets
      "set_robust_list", # glibc seems to need it for whatever reason
      "setrlimit", # glibc seems to use it for whatever reason
      "sigreturn", # called by signal trampoline
      "timerfd_create", # used by Nim selectors
      "timerfd_gettime", # not actually used by Nim but may be in the future
      "timerfd_settime", # used by Nim selectors
      "write" # for writing to sockets
    ]
    for it in allowList:
      let syscall = seccomp_syscall_resolve_name(it)
      doAssert seccomp_rule_add(ctx, SCMP_ACT_ALLOW, syscall, 0) == 0
    block allowUnixSockets:
      # only allow creation of UNIX domain sockets.
      let syscall = seccomp_syscall_resolve_name("socket")
      let arg0 = scmp_arg_cmp(
        arg: 0, # domain
        op: SCMP_CMP_EQ, # equals
        datum_a: 1 # PF_LOCAL == PF_UNIX == AF_UNIX
      )
      doAssert seccomp_rule_add(ctx, SCMP_ACT_ALLOW, syscall, 1, arg0) == 0
    doAssert seccomp_load(ctx) == 0
    seccomp_release(ctx)

  proc enterNetworkSandbox*() =
    onSignal SIGSYS:
      discard sig
      raise newException(Defect, "Sandbox violation in network process")
    let ctx = seccomp_init(SCMP_ACT_TRAP)
    doAssert pointer(ctx) != nil
    const allowList = [
      cstring"close", "exit_group", # duh
      "read", "write", "recv", "send", "recvfrom", "sendto", # socket i/o
      "lseek", # glibc calls lseek on open files at exit
      "fcntl", "fcntl64", # so we can set nonblock etc.
      "mmap", "mmap2", "mremap", "munmap", "brk", # memory allocation
      "poll", # curl needs poll
      "getpid", # used indirectly by OpenSSL EVP_RAND_CTX_new (through drbg)
      "fstat", # glibc fread seems to call it
      # maybe it will need epoll too in the future
      "epoll_create", "epoll_create1", "epoll_ctl", "epoll_wait",
      "ppoll", # or ppoll
      # we either have to use CURLOPT_NOSIGNAL or allow signals.
      # do the latter, otherwise the default name resolver will never time out.
      "signal", "sigaction", "rt_sigaction",
    ]
    for it in allowList:
      doAssert seccomp_rule_add(ctx, SCMP_ACT_ALLOW,
        seccomp_syscall_resolve_name(it), 0) == 0
    doAssert seccomp_load(ctx) == 0
    seccomp_release(ctx)
else:
  {.warning: "Building without OS-level sandboxing!".}
  proc enterBufferSandbox*(sockPath: string) =
    discard

  proc enterNetworkSandbox*() =
    discard