diff --git a/README b/README index 914a672..0fb2db8 100644 --- a/README +++ b/README @@ -16,6 +16,7 @@ check (optional, for tests) intltool libmagic from file(1) (optional, for mime-type detection) libsynctex from TeXLive (optional, for SyncTeX support) +libseccomp (optional, for sandbox support) Sphinx (optional, for manpages and HTML documentation) doxygen (optional, for HTML documentation) breathe (optional, for HTML documentation) @@ -31,6 +32,9 @@ enable-sqlite=off and sqlite support won't be available. The use of magic to detect mime types is optional and can be disabled by setting enable-magic=off. +The use of seccomp to create a sandboxed environment is optional and can be +enabled by setting enable-seccomp=on. + Installation ------------ diff --git a/doc/man/zathurarc.5.rst b/doc/man/zathurarc.5.rst index f71aeb6..5b78644 100644 --- a/doc/man/zathurarc.5.rst +++ b/doc/man/zathurarc.5.rst @@ -1044,6 +1044,16 @@ Define the background color of the selected element in index mode. * Value type: String * Default value: #9FBC00 +sandbox +^^^^^^^ +Defines the sandbox mode to use for the seccomp syscall filter. Possible +values are "none", "normal" and "strict". If "none" is used, the sandbox +will be disabled. The use of "normal" will provide minimal protection and +allow normal use of seccomp with support for all features. The "strict" mode +is a read only sandbox that is intended for viewing documents only. + +* Value type: String +* Default value: normal SEE ALSO ======== diff --git a/meson.build b/meson.build index 41fe169..ee6f64c 100644 --- a/meson.build +++ b/meson.build @@ -70,11 +70,12 @@ additional_sources = [] sqlite = dependency('sqlite3', version: '>=3.5.9', required: false) synctex = dependency('synctex', required: false) magic = cc.find_library('magic', required: false) +seccomp = dependency('libseccomp', required: false) if get_option('enable-sqlite') and sqlite.found() build_dependencies += sqlite defines += '-DWITH_SQLITE' - additional_sources = files('zathura/database-sqlite.c') + additional_sources += files('zathura/database-sqlite.c') endif if get_option('enable-synctex') and synctex.found() @@ -87,6 +88,12 @@ if get_option('enable-magic') and magic.found() defines += '-DWITH_MAGIC' endif +if get_option('enable-seccomp') and seccomp.found() + build_dependencies += seccomp + defines += '-DWITH_SECCOMP' + additional_sources += files('zathura/libsec.c') +endif + # generate version header file version_header = configure_file( input: 'zathura/version.h.in', diff --git a/meson_options.txt b/meson_options.txt index 6853a9a..db63e3d 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -13,3 +13,8 @@ option('enable-magic', value: true, description: 'Enable magic support if available.' ) +option('enable-seccomp', + type: 'boolean', + value: true, + description: 'Enable seccomp support if available.' +) diff --git a/zathura/commands.c b/zathura/commands.c index e259b22..ce359f4 100644 --- a/zathura/commands.c +++ b/zathura/commands.c @@ -280,6 +280,15 @@ cmd_print(girara_session_t* session, girara_list_t* UNUSED(argument_list)) return false; } + char* sandbox = NULL; + girara_setting_get(zathura->ui.session, "sandbox", &sandbox); + if (g_strcmp0(sandbox, "strict") == 0) { + girara_notify(zathura->ui.session, GIRARA_ERROR, _("Printing is not permitted in strict sandbox mode")); + g_free(sandbox); + return false; + } + + print(zathura); return true; diff --git a/zathura/config.c b/zathura/config.c index ecc6026..66c5e1f 100644 --- a/zathura/config.c +++ b/zathura/config.c @@ -185,6 +185,8 @@ config_load_default(zathura_t* zathura) girara_setting_add(gsession, "index-active-fg", "#232323", STRING, true, _("Index mode foreground color (active element)"), NULL, NULL); girara_setting_add(gsession, "index-active-bg", "#9FBC00", STRING, true, _("Index mode background color (active element)"), NULL, NULL); + girara_setting_add(gsession, "sandbox", "normal", STRING, true, _("Sandbox level"), NULL, NULL); + bool_value = false; girara_setting_add(gsession, "recolor", &bool_value, BOOLEAN, false, _("Recolor pages"), cb_setting_recolor_change, NULL); bool_value = false; diff --git a/zathura/libsec.c b/zathura/libsec.c new file mode 100644 index 0000000..4c6f73a --- /dev/null +++ b/zathura/libsec.c @@ -0,0 +1,377 @@ +#include "libsec.h" +#include + +#ifdef WITH_SECCOMP + +#include /* libseccomp */ +#include /* prctl */ +#include +#include +#include +#include +#include + +#define DENY_RULE(call) { if (seccomp_rule_add (ctx, SCMP_ACT_KILL, SCMP_SYS(call), 0) < 0) goto out; } +#define ALLOW_RULE(call) { if (seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS(call), 0) < 0) goto out; } + +int seccomp_enable_basic_filter(void){ + + scmp_filter_ctx ctx; + + /* prevent child processes from getting more priv e.g. via setuid, capabilities, ... */ + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { + girara_error("prctl SET_NO_NEW_PRIVS"); + return -1; + } + + /* prevent escape via ptrace */ + if(prctl (PR_SET_DUMPABLE, 0, 0, 0, 0)){ + girara_error("prctl PR_SET_DUMPABLE"); + return -1; + } + + /* initialize the filter */ + ctx = seccomp_init(SCMP_ACT_ALLOW); + if (ctx == NULL){ + girara_error("seccomp_init failed"); + return -1; + } + + DENY_RULE (_sysctl); + DENY_RULE (acct); + DENY_RULE (add_key); + DENY_RULE (adjtimex); + DENY_RULE (chroot); + DENY_RULE (clock_adjtime); + DENY_RULE (create_module); + DENY_RULE (delete_module); + DENY_RULE (fanotify_init); + DENY_RULE (finit_module); + DENY_RULE (get_kernel_syms); + DENY_RULE (get_mempolicy); + DENY_RULE (init_module); + DENY_RULE (io_cancel); + DENY_RULE (io_destroy); + DENY_RULE (io_getevents); + DENY_RULE (io_setup); + DENY_RULE (io_submit); + DENY_RULE (ioperm); + DENY_RULE (iopl); + DENY_RULE (ioprio_set); + DENY_RULE (kcmp); + DENY_RULE (kexec_file_load); + DENY_RULE (kexec_load); + DENY_RULE (keyctl); + DENY_RULE (lookup_dcookie); + DENY_RULE (mbind); + DENY_RULE (nfsservctl); + DENY_RULE (migrate_pages); + DENY_RULE (modify_ldt); + DENY_RULE (mount); + DENY_RULE (move_pages); + DENY_RULE (name_to_handle_at); + DENY_RULE (open_by_handle_at); + DENY_RULE (perf_event_open); + DENY_RULE (pivot_root); + DENY_RULE (process_vm_readv); + DENY_RULE (process_vm_writev); + DENY_RULE (ptrace); + DENY_RULE (reboot); + DENY_RULE (remap_file_pages); + DENY_RULE (request_key); + DENY_RULE (set_mempolicy); + DENY_RULE (swapoff); + DENY_RULE (swapon); + DENY_RULE (sysfs); + DENY_RULE (syslog); + DENY_RULE (tuxcall); + DENY_RULE (umount2); + DENY_RULE (uselib); + DENY_RULE (vmsplice); + + /* TODO: check for additional syscalls to blacklist */ + /* DENY_RULE (execve); */ + + /* applying filter... */ + if (seccomp_load (ctx) >= 0){ + /* free ctx after the filter has been loaded into the kernel */ + seccomp_release(ctx); + return 0; + } + + out: + /* something went wrong */ + seccomp_release(ctx); + return -1; +} + + +int seccomp_enable_strict_filter(void){ + + scmp_filter_ctx ctx; + + /* prevent child processes from getting more priv e.g. via setuid, capabilities, ... */ + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { + perror("prctl SET_NO_NEW_PRIVS"); + exit(EXIT_FAILURE); + } + + /* prevent escape via ptrace */ + if(prctl (PR_SET_DUMPABLE, 0, 0, 0, 0)){ + perror("prctl PR_SET_DUMPABLE"); + exit(EXIT_FAILURE); + } + + /* initialize the filter */ + ctx = seccomp_init(SCMP_ACT_KILL); + if (ctx == NULL){ + perror("seccomp_init failed"); + exit(EXIT_FAILURE); + } + + ALLOW_RULE (access); + /* ALLOW_RULE (arch_prctl); */ + ALLOW_RULE (bind); + ALLOW_RULE (brk); + ALLOW_RULE (clock_getres); + ALLOW_RULE (clone); /* TODO: investigate */ + ALLOW_RULE (close); + /* ALLOW_RULE (connect); */ + ALLOW_RULE (eventfd2); + ALLOW_RULE (exit); + ALLOW_RULE (exit_group); + ALLOW_RULE (fadvise64); + ALLOW_RULE (fallocate); + ALLOW_RULE (fcntl); /* TODO: build detailed filter */ + ALLOW_RULE (fstat); + ALLOW_RULE (fstatfs); + ALLOW_RULE (ftruncate); + ALLOW_RULE (futex); + ALLOW_RULE (getdents); + ALLOW_RULE (getegid); + ALLOW_RULE (geteuid); + ALLOW_RULE (getgid); + ALLOW_RULE (getuid); + ALLOW_RULE (getpid); + /* ALLOW_RULE (getpeername); */ + ALLOW_RULE (getresgid); + ALLOW_RULE (getresuid); + ALLOW_RULE (getrlimit); + /* ALLOW_RULE (getsockname); */ + /* ALLOW_RULE (getsockopt); needed for access to x11 socket in network namespace (without abstract sockets) */ + ALLOW_RULE (inotify_add_watch); + ALLOW_RULE (inotify_init1); + ALLOW_RULE (inotify_rm_watch); + /* ALLOW_RULE (ioctl); specified below */ + ALLOW_RULE (lseek); + ALLOW_RULE (lstat); + ALLOW_RULE (madvise); + ALLOW_RULE (memfd_create); + ALLOW_RULE (mkdir); /* needed for first run only */ + ALLOW_RULE (mmap); + ALLOW_RULE (mprotect); + ALLOW_RULE (mremap); + ALLOW_RULE (munmap); + //ALLOW_RULE (open); /* (zathura needs to open for writing) TODO: avoid needing this somehow */ + //ALLOW_RULE (openat); + ALLOW_RULE (pipe); + ALLOW_RULE (poll); + ALLOW_RULE (pwrite64); /* TODO: build detailed filter */ + ALLOW_RULE (pread64); + /* ALLOW_RULE (prlimit64); */ + /* ALLOW_RULE (prctl); specified below */ + ALLOW_RULE (read); + ALLOW_RULE (readlink); + ALLOW_RULE (recvfrom); + ALLOW_RULE (recvmsg); + ALLOW_RULE (restart_syscall); + ALLOW_RULE (rt_sigaction); + ALLOW_RULE (rt_sigprocmask); + ALLOW_RULE (sendmsg); + ALLOW_RULE (sendto); + ALLOW_RULE (select); + ALLOW_RULE (set_robust_list); + /* ALLOW_RULE (set_tid_address); */ + /* ALLOW_RULE (setsockopt); */ + ALLOW_RULE (shmat); + ALLOW_RULE (shmctl); + ALLOW_RULE (shmdt); + ALLOW_RULE (shmget); + ALLOW_RULE (shutdown); + ALLOW_RULE (stat); + ALLOW_RULE (statfs); + /* ALLOW_RULE (socket); */ + ALLOW_RULE (sysinfo); + ALLOW_RULE (uname); + ALLOW_RULE (unlink); + ALLOW_RULE (write); /* specified below (zathura needs to write files)*/ + ALLOW_RULE (writev); + ALLOW_RULE (wait4); /* trying to open links should not crash the app */ + + + /* Special requirements for ioctl, allowed on stdout/stderr */ + if (seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS(ioctl), 1, + SCMP_CMP(0, SCMP_CMP_EQ, 1)) < 0) + goto out; + if (seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS(ioctl), 1, + SCMP_CMP(0, SCMP_CMP_EQ, 2)) < 0) + goto out; + + + /* needed by gtk??? (does not load content without) */ + + /* special restrictions for prctl, only allow PR_SET_NAME/PR_SET_PDEATHSIG */ + if (seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS(prctl), 1, + SCMP_CMP(0, SCMP_CMP_EQ, PR_SET_NAME)) < 0) + goto out; + + if (seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS(prctl), 1, + SCMP_CMP(0, SCMP_CMP_EQ, PR_SET_PDEATHSIG)) < 0) + goto out; + + + /* special restrictions for open, prevent opening files for writing */ + if (seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 1, + SCMP_CMP(1, SCMP_CMP_MASKED_EQ, O_WRONLY | O_RDWR, 0)) < 0) + goto out; + + if (seccomp_rule_add (ctx, SCMP_ACT_ERRNO (EACCES), SCMP_SYS(open), 1, + SCMP_CMP(1, SCMP_CMP_MASKED_EQ, O_WRONLY, O_WRONLY)) < 0) + goto out; + + if (seccomp_rule_add (ctx, SCMP_ACT_ERRNO (EACCES), SCMP_SYS(open), 1, + SCMP_CMP(1, SCMP_CMP_MASKED_EQ, O_RDWR, O_RDWR)) < 0) + goto out; + + /* special restrictions for openat, prevent opening files for writing */ + if (seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat), 1, + SCMP_CMP(1, SCMP_CMP_MASKED_EQ, O_WRONLY | O_RDWR, 0)) < 0) + goto out; + + if (seccomp_rule_add (ctx, SCMP_ACT_ERRNO (EACCES), SCMP_SYS(openat), 1, + SCMP_CMP(1, SCMP_CMP_MASKED_EQ, O_WRONLY, O_WRONLY)) < 0) + goto out; + + if (seccomp_rule_add (ctx, SCMP_ACT_ERRNO (EACCES), SCMP_SYS(openat), 1, + SCMP_CMP(1, SCMP_CMP_MASKED_EQ, O_RDWR, O_RDWR)) < 0) + goto out; + + + /* allowed for debugging: */ + + /* ALLOW_RULE (prctl); */ + /* ALLOW_RULE (ioctl); */ + + + /* TODO: test fcntl rules */ + /* if (seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS(fcntl), 1, */ + /* SCMP_CMP(0, SCMP_CMP_EQ, F_GETFL)) < 0) */ + /* goto out; */ + + /* if (seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS(fcntl), 1, */ + /* SCMP_CMP(0, SCMP_CMP_EQ, F_SETFL)) < 0) */ + /* goto out; */ + + /* if (seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS(fcntl), 1, */ + /* SCMP_CMP(0, SCMP_CMP_EQ, F_SETFD)) < 0) */ + /* goto out; */ + + /* if (seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS(fcntl), 1, */ + /* SCMP_CMP(0, SCMP_CMP_EQ, F_GETFD)) < 0) */ + /* goto out; */ + + /* if (seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS(fcntl), 1, */ + /* SCMP_CMP(0, SCMP_CMP_EQ, F_SETLK)) < 0) */ + /* goto out; */ + + + /* TODO: build detailed filter for prctl */ + /* needed by gtk??? (does not load content without) */ + + /* /\* special restrictions for prctl, only allow PR_SET_NAME/PR_SET_PDEATHSIG *\/ */ + /* if (seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS(prctl), 1, */ + /* SCMP_CMP(0, SCMP_CMP_EQ, PR_SET_NAME)) < 0) */ + /* goto out; */ + + /* if (seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS(prctl), 1, */ + /* SCMP_CMP(0, SCMP_CMP_EQ, PR_SET_PDEATHSIG)) < 0) */ + /* goto out; */ + + + /* when zathura is run on wayland, with X11 server available but blocked, unset the DISPLAY variable */ + /* otherwise it will try to connect to X11 using inet socket protocol */ + + + /* ------------ experimental filters --------------- */ + + /* /\* this filter is susceptible to TOCTOU race conditions, providing limited use *\/ */ + /* /\* allow opening only specified files identified by their file descriptors*\/ */ + + /* this requires either a list of all files to open (A LOT!!!) */ + /* or needs to be applied only after initialisation, right before parsing */ + /* if(seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 1, */ + /* SCMP_CMP(SCMP_CMP_EQ, fd)) < 0) /\* or < 1 ??? *\/ */ + /* goto out; */ + + + /* /\* restricting write access *\/ */ + + /* /\* allow stdin *\/ */ + /* if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1, */ + /* SCMP_CMP(0, SCMP_CMP_EQ, 0)) < 0 ) */ + /* goto out; */ + + /* /\* allow stdout *\/ */ + /* if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1, */ + /* SCMP_CMP(0, SCMP_CMP_EQ, 1)) < 0 ) */ + /* goto out; */ + + + /* /\* allow stderr *\/ */ + /* if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1, */ + /* SCMP_CMP(0, SCMP_CMP_EQ, 2)) < 0 ) */ + /* goto out; */ + + + /* /\* restrict writev (write a vector) access *\/ */ + /* this does not seem reliable but it surprisingly is. investigate more */ + /* if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(writev), 1, */ + /* SCMP_CMP(0, SCMP_CMP_EQ, 3)) < 0 ) */ + /* goto out; */ + + /* test if repeating this after some time or denying it works */ + + + /* first attempt to filter poll requests */ + /* if (seccomp_rule_add (ctx, SCMP_ACT_ALLOW, SCMP_SYS(poll), 1, */ + /* SCMP_CMP(0, SCMP_CMP_MASKED_EQ, POLLIN | POLL, 0)) < 0) */ + /* goto out; */ + + + /* /\* restrict fcntl calls *\/ */ + /* this syscall sets the file descriptor to read write */ + /* if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fcntl), 1, */ + /* SCMP_CMP(0, SCMP_CMP_EQ, 3)) < 0 ) */ + /* goto out; */ + /* fcntl(3, F_GETFL) = 0x2 (flags O_RDWR) */ + /* fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK) = 0 */ + /* fcntl(3, F_SETFD, FD_CLOEXEC) = 0 */ + + + /* ------------------ end of experimental filters ------------------ */ + + + /* applying filter... */ + if (seccomp_load (ctx) >= 0){ + /* free ctx after the filter has been loaded into the kernel */ + seccomp_release(ctx); + return 0; + } + + out: + /* something went wrong */ + seccomp_release(ctx); + return -1; +} + +#endif /* WITH_SECCOMP */ diff --git a/zathura/libsec.h b/zathura/libsec.h new file mode 100644 index 0000000..3b1af19 --- /dev/null +++ b/zathura/libsec.h @@ -0,0 +1,13 @@ +#ifndef SECCOMP_H +#define SECCOMP_H + +/* basic filter */ +/* this mode allows normal use */ +/* only dangerous syscalls are blacklisted */ +int seccomp_enable_basic_filter(void); + +/* strict filter before document parsing */ +/* this filter is to be enabled after most of the initialisation of zathura has finished */ +int seccomp_enable_strict_filter(void); + +#endif diff --git a/zathura/links.c b/zathura/links.c index 293691c..aa7d03a 100644 --- a/zathura/links.c +++ b/zathura/links.c @@ -14,6 +14,10 @@ #include "page.h" #include "render.h" +#ifdef WITH_SECCOMP +#include "libsec.h" +#endif + struct zathura_link_s { zathura_rectangle_t position; /**< Position of the link */ zathura_link_type_t type; /**< Link type */ @@ -131,6 +135,10 @@ zathura_link_evaluate(zathura_t* zathura, zathura_link_t* link) bool link_zoom = true; girara_setting_get(zathura->ui.session, "link-zoom", &link_zoom); + /* required below to prevent opening hyperlinks in strict sandbox mode */ + char* sandbox = NULL; + girara_setting_get(zathura->ui.session, "sandbox", &sandbox); + switch (link->type) { case ZATHURA_LINK_GOTO_DEST: if (link->target.destination_type != ZATHURA_LINK_DESTINATION_UNKNOWN) { @@ -199,8 +207,12 @@ zathura_link_evaluate(zathura_t* zathura, zathura_link_t* link) link_remote(zathura, link->target.value); break; case ZATHURA_LINK_URI: - if (girara_xdg_open(link->target.value) == false) { - girara_notify(zathura->ui.session, GIRARA_ERROR, _("Failed to run xdg-open.")); + if (g_strcmp0(sandbox, "strict") == 0) { + girara_notify(zathura->ui.session, GIRARA_ERROR, _("Opening external applications in strict sandbox mode is not permitted")); + } else { + if (girara_xdg_open(link->target.value) == false) { + girara_notify(zathura->ui.session, GIRARA_ERROR, _("Failed to run xdg-open.")); + } } break; case ZATHURA_LINK_LAUNCH: @@ -209,6 +221,7 @@ zathura_link_evaluate(zathura_t* zathura, zathura_link_t* link) default: break; } + g_free(sandbox); } void diff --git a/zathura/main.c b/zathura/main.c index d9ca48f..40aea4f 100644 --- a/zathura/main.c +++ b/zathura/main.c @@ -19,6 +19,10 @@ #include "synctex.h" #endif +#ifdef WITH_SECCOMP +#include "libsec.h" +#endif + /* Init locale */ static void init_locale(void) @@ -122,6 +126,7 @@ init_zathura(const char* config_dir, const char* data_dir, GIRARA_VISIBLE int main(int argc, char* argv[]) { + init_locale(); /* parse command line arguments */ @@ -288,6 +293,31 @@ main(int argc, char* argv[]) goto free_and_ret; } +#ifdef WITH_SECCOMP + + char* sandbox = NULL; + girara_setting_get(zathura->ui.session, "sandbox", &sandbox); + + if (g_strcmp0(sandbox, "none") == 0) { + girara_debug("Sandbox deactivated."); + } else if (g_strcmp0(sandbox, "normal") == 0) { + girara_debug("Basic sandbox allowing normal operation."); + ret = seccomp_enable_basic_filter(); + } else if (g_strcmp0(sandbox, "strict") == 0) { + girara_debug("Strict sandbox preventing write and network access."); + ret = seccomp_enable_strict_filter(); + } else { + girara_error("Invalid sandbox option"); + ret = -1; + } + + g_free(sandbox); + if (ret){ + goto free_and_ret; + } + +#endif + /* open document if passed */ if (file_idx != 0) { if (page_number > 0) {