diff --git a/Makefile b/Makefile index 6529996..7ea09c9 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ CXXFLAGS=-std=c++17 -O0 -g -no-pie -pipe -MMD -Wall -Wextra RELEASE_CXXFLAGS=-std=c++17 -O3 -pipe -MMD -Wall -Wextra -LDFLAGS=-lsqlite3 -lpthread -lcrypto -lboost_regex -lstdc++fs +LDFLAGS=-lsqlite3 -lpthread -lcrypto -lboost_regex -lstdc++fs -lseccomp CXX=g++ @@ -12,12 +12,14 @@ SOURCES+=$(wildcard gateway/*.cpp) SOURCES+=$(wildcard handlers/*.cpp) SOURCES+=$(wildcard database/*.cpp) SOURCES+=$(wildcard cache/*.cpp) +SOURCES+=$(wildcard sandbox/*.cpp) HEADERS=$(wildcard *.h) HEADERS+=$(wildcard gateway/*.h) HEADERS+=$(wildcard handlers/*.h) HEADERS+=$(wildcard database/*.h) HEADERS+=$(wildcard cache/*.h) +HEADERS+=$(wildcard sandbox/*.h) OBJECTS=$(patsubst %.cpp, %.o, $(SOURCES)) diff --git a/README.md b/README.md index a262c85..ed20557 100644 --- a/README.md +++ b/README.md @@ -9,35 +9,40 @@ History ==== A couple of years ago, I wanted to setup a personal wiki on my raspberry pi. However, the distribution I used back then did not have a PHP package -for ARM. So I decided I would write one in C. Yes, that's an odd way -to approach the problem and indeed, I may have had too much time back -then. Also, I wanted to see how it's like to write a "web app" in C -and wanted to sharpen my C a little bit. +for ARM. So instead of switching distributions or searching for other +wikis that I could use, I decided I would write one in C. Yes, +that's an odd way to approach the problem and indeed, I may have had too +much time back then. Also, I wanted to see how it's like to write a +"web app" in C and wanted to sharpen my C skills a little bit. -Of course, it's pretty straightforward at first. No really. Just use CGI. -And indeed, that's probably more than enough. Then I decided to play -around and started using FastCGI (with the official library from now -defunct fastcgi.com) and created a multi-threaded version. It initially -used a "pile of files database", but that became too painful, so then -I started using sqlite. +Of course, it's pretty straightforward at first. No really: Just use CGI. +And indeed, that would have been more than enough for my use cases. +Then I decided to play around and started using FastCGI (with the official +library from now defunct fastcgi.com) and created a multi-threaded version. +It initially used a "pile of files database", but that became too painful, +so then I started using sqlite. C++ --- -Eventually the code became unmaintainable. Initially, I wanted something -quick. I did not care about memory leaks (as it was CGI initially). -After FastCGI, they became an issue. In the end, the task of avoiding -memory leaks became too annoying. And of course, C does not include any -"batteries" and while I could manage, this too was another good reason. +Eventually, since it was mostly a playground for me, the code became +unmaintainable. Furthermore, I wanted something quick and given that +it was CGI, I didn't bother taking care of memory leaks. +After initiating a FastCGI interface, they became an issue and then the +task of avoiding memory leaks became too annoying. And of course, C does n +ot include any "batteries" and while I could manage, this too was another +good reason. Overall, I am just continuing the experiment with C++17 now. It's not nearly as bad as you would expect perhaps. Some things are surprisingly convenient even. Still, the standard library is lacking and -I would hope for a some better built-in Unicode support in the future. +I would hope for a some better built-in Unicode support in future C++ +standards. Features ======== To be fair, at this point it doesn't even have a "diff" between revisions -yet and does not have features that make you prefer it over other wikis. +yet and does not have features that would make you prefer it over other +wikis. - CGI - HTTP server using the header only library cpp-httplib. It's more @@ -55,23 +60,26 @@ yet and does not have features that make you prefer it over other wikis. Security ======== -The most reasonable way would have been to add some sort sandboxing -support right away, but this is lacking so far. As for "web security", -all POST requests are centrally protected against CSRF attacks and all -input is escaped against XSS attacks. +On Linux namespaces are used to restrict the process to only access +files it needs. It doesn't have access to other paths in the system. +In addition, Seccomp is used to restrict the syscalls the qswiki process +can call. As for "web security", all POST requests are centrally +protected against CSRF attacks and all input is escaped against XSS +attacks. Building ======== Dependencies: - cpp-httplib: https://github.com/yhirose/cpp-httplib - SqliteModernCpp: https://github.com/SqliteModernCpp - -Given the fact those are header-only libraries, they are already -included here, so you only need to run: + - libseccomp: https://github.com/seccomp/libseccomp + - sqlite3: https://sqlite.org/index.html + +The first two are header-only libraries that are already included here. +If all dependencies are available, run: ```make release``` - Setup ===== To be written diff --git a/cache/fscache.cpp b/cache/fscache.cpp index aef4fbc..ad4a803 100644 --- a/cache/fscache.cpp +++ b/cache/fscache.cpp @@ -7,7 +7,7 @@ FsCache::FsCache(std::string path) { if(!std::filesystem::exists(path)) { - throw std::runtime_error { "Directory does not exist" }; + throw std::runtime_error { "Cache directory does not exist" }; } this->path = path; } diff --git a/qswiki.cpp b/qswiki.cpp index 78521b6..d2086cc 100644 --- a/qswiki.cpp +++ b/qswiki.cpp @@ -24,6 +24,7 @@ SOFTWARE. #include #include #include +#include #include "gateway/gatewayinterface.h" #include "gateway/gatewayfactory.h" #include "handlers/handlerfactory.h" @@ -35,6 +36,7 @@ SOFTWARE. #include "urlprovider.h" #include "requestworker.h" #include "cache/fscache.h" +#include "sandbox/sandboxfactory.h" void sigterm_handler(int arg) { //TODO: proper shutdown. @@ -69,6 +71,19 @@ int main(int argc, char **argv) std::cerr << "Do not run this as root!" << std::endl; return 1; } + auto sandbox = createSandbox(); + //TODO: do we want to keep it mandatory or configurable? + if(!sandbox->supported()) + { + Logger::error() << "Sandbox is not supported, exiting"; + exit(EXIT_FAILURE); + } + if(!sandbox->enableForInit()) + { + Logger::error() << "Sandboxing for init mode could not be activated."; + exit(EXIT_FAILURE); + } + if(argc < 2) { std::cerr << "no path to config file provided" << std::endl; @@ -80,6 +95,20 @@ int main(int argc, char **argv) ConfigReader configreader(argv[1]); Config config = configreader.readConfig(); + //TODO: config.connectiontring only works as long as we only support sqlite of course + + + if(!sandbox->enablePreWorker({ + config.getConfig("cache_fs_dir"), + config.templatepath, + std::filesystem::path(config.logfile).parent_path(), + std::filesystem::path(config.connectionstring).parent_path(), + })) + { + Logger::error() << "Sandboxing for pre worker stage could not be activated."; + exit(EXIT_FAILURE); + } + setup_signal_handlers(); std::fstream logstream; @@ -114,6 +143,12 @@ int main(int argc, char **argv) RequestWorker requestWorker (*database, siteTemplate, urlprovider, *cache ); auto interface = createGateway(config); + + if(!sandbox->enableForWorker()) + { + Logger::error() << "Sandboxing for worker could not be enabled!"; + exit(EXIT_FAILURE); + } interface->work(requestWorker); } catch(const std::exception &e) diff --git a/sandbox/sandbox-linux.cpp b/sandbox/sandbox-linux.cpp new file mode 100644 index 0000000..2843b59 --- /dev/null +++ b/sandbox/sandbox-linux.cpp @@ -0,0 +1,272 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../logger.h" +#include "../utils.h" +#include "../random.h" + +#include "sandbox-linux.h" + +/* TODO: make a whitelist approach. So far we simply blacklist + * obvious systemcalls. To whitelist, we need to analyse our + * dependencies (http library, sqlite wrapper, sqlite lib etc.) */ + +/* TODO: cleanup our sandboxing directory (unmount and delete folders) after exit */ +bool SandboxLinux::seccomp_blacklist(std::initializer_list syscalls) +{ + scmp_filter_ctx ctx; + ctx = seccomp_init(SCMP_ACT_ALLOW); + if(ctx == NULL) + { + Logger::error() << "failed to init seccomp_init"; + return false; + } + + for(auto sc : syscalls) + { + if(seccomp_rule_add(ctx, SCMP_ACT_KILL_PROCESS, sc, 0) < 0) + { + Logger::error() << "Failed to add a seccomp rule"; + return false; + } + } + + bool success = seccomp_load(ctx) == 0; + if(!success) + { + Logger::error() << "Failed to load seccomp filter"; + return false; + } + return success; +} + +bool SandboxLinux::bindMountPaths(std::string target_root, std::initializer_list paths) +{ + for(const std::string &path : paths) + { + std::string chroot_target_path = target_root + path; + if(std::filesystem::exists(chroot_target_path)) + { + continue; + } + if(std::filesystem::is_regular_file(path)) + { + std::fstream f1; + f1.open(chroot_target_path, std::ios::out); + f1.close(); + } + else { + std::error_code ec; + //TODO: fails if the stuff already exists, but it shouldn't according to doc? + if(!std::filesystem::create_directories(chroot_target_path, ec)) + { + Logger::error() << "Error while trying to duplicate structure for sandbox. Dir creation failed. Path: " << chroot_target_path << " Error: " << ec.message(); + return false; + } + + if(mount(path.c_str(), chroot_target_path.c_str(), NULL, MS_BIND, NULL) == -1) + { + Logger::error() << "Bind mount failed! " << strerror(errno); + return false; + } + } + + } + return true; +} + +bool SandboxLinux::isolateNamespaces(std::vector fsPaths) +{ + + auto current_uid = getuid(); + auto current_gid = getgid(); + if(unshare(CLONE_NEWUSER) == -1) + { + Logger::error() << "Failed to unshare user namespace: " << strerror(errno); + return false; + } + + std::fstream setgroups; + setgroups.open("/proc/self/setgroups", std::ios::out | std::ios::app); + setgroups << "deny"; + setgroups.flush(); + setgroups.close(); + + std::fstream uid_map; + uid_map.open("/proc/self/uid_map", std::ios::out | std::ios::app); + uid_map << "0 " << current_uid << " 1\n"; + uid_map.flush(); + uid_map.close(); + + std::fstream gid_map; + gid_map.open("/proc/self/gid_map", std::ios::out); + uid_map << "0 " << current_gid << " 1\n"; + gid_map.flush(); + gid_map.close(); + + if(unshare(CLONE_NEWNS) == -1) + { + Logger::error() << "Failed to unshare mount namespace: " << strerror(errno); + return false; + } + + /* + * TODO: breaks server. + * TODO: fork? + * if(unshare(CLONE_NEWPID) == -1) + { + Logger::error() << "Failed to unshare pid namespace: " << strerror(errno); + return false; + }*/ + + + //The purpose is to start with a clean sandbox dir. + //We maybe could work with mkdirat, and check whether it exists alrady, to avoid + //some attacks where an attacker gueses the dir, but in that case the system is already compromised + //TODO: still, check, whether this is something we must consider here or not... + Random random; + std::string rootpath = "/tmp/qswiki_sandbox_" + random.getRandomHexString(10) + "/"; + if(!std::filesystem::create_directory(rootpath)) + { + Logger::error() << "Failed to create chroot directory for sandbox"; + return false; + } + + + for(std::string &path : fsPaths) + { + if(!bindMountPaths(rootpath, { path })) + { + Logger::error() << "Bind mount for " << path << " failed!"; + return false; + } + + } + + if(chroot(rootpath.c_str()) == -1) + { + Logger::error() << "chroot to sandbox failed!"; + return false; + } + if(chdir("/") == -1) + { + Logger::error() << "chdir to sandbox failed!"; + return false; + } + + return true; +} + +bool SandboxLinux::enableForInit() +{ + umask(0027); + + //TODO. there is execv for SPARC. Sigh... + if(!seccomp_blacklist({ SCMP_SYS(execveat), SCMP_SYS(execve) })) + { + Logger::error() << "Failed to install blacklisting seccomp filter"; + return false; + } + + return true; + + +} + +bool SandboxLinux::enablePreWorker(std::vector fsPaths) +{ + if(!isolateNamespaces(fsPaths)) + { + Logger::error() << "Failed to isolate namespaces"; + return false; + } + return true; + +} + +bool SandboxLinux::supported() +{ + return true; +} +bool SandboxLinux::enableForWorker() +{ + int cap = 0; + int res = 0; + while((res = prctl(PR_CAPBSET_DROP, cap++, 0, 0, 0)) == 0); + if(res == -1 && errno != EINVAL) + { + Logger::error() << "Failed to drop the capability bounding set!"; + return false; + } + + __user_cap_header_struct h = { 0 }; + h.pid = 0; + h.version = _LINUX_CAPABILITY_VERSION_3; + __user_cap_data_struct drop[2]; + drop[0].effective = 0; + drop[0].permitted = 0; + drop[0].inheritable = 0; + drop[1].effective = 0; + drop[1].permitted = 0; + drop[1].inheritable = 0; + if(capset(&h, drop) == -1) + { + Logger::error() << "Failed to drop capabilities: " << strerror(errno); + return false; + } + + + + + /* TODO: as said, a whitelist approach is better. As such, this list is bound to be incomplete in the + * sense that more could be listed here and some critical ones are probably missing */ + if(! seccomp_blacklist({ + SCMP_SYS(setuid), + SCMP_SYS(setuid32), + SCMP_SYS(connect), + SCMP_SYS(chroot), + SCMP_SYS(pivot_root), + SCMP_SYS(mount), + SCMP_SYS(setns), + SCMP_SYS(unshare), + SCMP_SYS(ptrace), + SCMP_SYS(personality) + + })) + { + Logger::error() << "Sandbox: Activation of seccomp blacklist failed!"; + return false; + } + if(prctl(PR_SET_DUMPABLE, 0) == -1) + { + Logger::error() << "prctl: PR_SET_DUMPABLE failed"; + return false; + } + if(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) + { + Logger::error() << "prctl: PR_SET_NO_NEW_PRIVS failed: " << strerror(errno); + return false; + } + + if(! seccomp_blacklist({ SCMP_SYS(prctl) })) + { + Logger::error() << "Sandbox: Activation of seccomp blacklist failed!"; + return false; + } + + return true; +} + diff --git a/sandbox/sandbox-linux.h b/sandbox/sandbox-linux.h new file mode 100644 index 0000000..cd5d814 --- /dev/null +++ b/sandbox/sandbox-linux.h @@ -0,0 +1,19 @@ +#ifndef SANDBOXLINUX_H +#define SANDBOXLINUX_H +#include +#include +#include "sandbox.h" +class SandboxLinux : public Sandbox +{ +public: + using Sandbox::Sandbox; + bool supported() override; + bool enableForInit() override; + bool enablePreWorker(std::vector fsPaths) override; + bool enableForWorker() override; +private : + bool isolateNamespaces(std::vector fsPaths); + bool seccomp_blacklist(std::initializer_list syscalls); + bool bindMountPaths(std::string target_root, std::initializer_list paths); +}; +#endif diff --git a/sandbox/sandbox-openbsd.h b/sandbox/sandbox-openbsd.h new file mode 100644 index 0000000..2027d56 --- /dev/null +++ b/sandbox/sandbox-openbsd.h @@ -0,0 +1,15 @@ +#ifndef SANDBOXOPENBSD_H +#define SANDBOXOPENBSD_H +#include "sandbox.h" + +class SandboxOpenBSD : public Sandbox +{ +public: + bool supported() override; + bool enableForInit() override; + bool enableForWorker() override; +private : + bool seccomp_blacklist(std::vector syscalls); + +}; +#endif diff --git a/sandbox/sandbox.h b/sandbox/sandbox.h new file mode 100644 index 0000000..f1d2a8c --- /dev/null +++ b/sandbox/sandbox.h @@ -0,0 +1,27 @@ +#ifndef SANDBOX_H +#define SANDBOX_H +#include +class Sandbox +{ +public: + Sandbox() + { + + } + /* Whether the platform has everything required to active all sandbnox modes */ + virtual bool supported() = 0; + + /* Activated early. At this point, we need more system calls + * than later on */ + virtual bool enableForInit() = 0; + + /* Activated after config has been read. Now we now which paths we need access to */ + virtual bool enablePreWorker(std::vector fsPaths) = 0; + + + /* Activated after we have acquired resources (bound to ports etc.) + * + * This should allow us to further restrcit the process */ + virtual bool enableForWorker() = 0; +}; +#endif diff --git a/sandbox/sandboxfactory.h b/sandbox/sandboxfactory.h new file mode 100644 index 0000000..bd0a4bb --- /dev/null +++ b/sandbox/sandboxfactory.h @@ -0,0 +1,11 @@ +#ifndef SANDBOXFACTORY_H +#define SANDBOXFACTORY_H +#include +#include "sandbox.h" +#include "sandbox-linux.h" +#include "sandbox-openbsd.h" +inline std::unique_ptr createSandbox() +{ + return std::make_unique(); +} +#endif