Begin sandboxing support, README updates.

This commit is contained in:
Albert S. 2019-08-11 20:10:38 +02:00
parent 232ff75ae0
commit 5656d3208f
9 changed files with 416 additions and 27 deletions

View File

@ -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))

View File

@ -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

2
cache/fscache.cpp vendored
View File

@ -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;
}

View File

@ -24,6 +24,7 @@ SOFTWARE.
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <filesystem>
#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)

272
sandbox/sandbox-linux.cpp Normal file
View File

@ -0,0 +1,272 @@
#include <sys/prctl.h>
#include <seccomp.h>
#include <vector>
#include <initializer_list>
#include <string.h>
#include <sched.h>
#include <unistd.h>
#include <fstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <filesystem>
#include <sys/mount.h>
#include <sys/capability.h>
#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<int> 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<std::string> 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<std::string> 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<std::string> 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;
}

19
sandbox/sandbox-linux.h Normal file
View File

@ -0,0 +1,19 @@
#ifndef SANDBOXLINUX_H
#define SANDBOXLINUX_H
#include <memory>
#include <vector>
#include "sandbox.h"
class SandboxLinux : public Sandbox
{
public:
using Sandbox::Sandbox;
bool supported() override;
bool enableForInit() override;
bool enablePreWorker(std::vector<std::string> fsPaths) override;
bool enableForWorker() override;
private :
bool isolateNamespaces(std::vector<std::string> fsPaths);
bool seccomp_blacklist(std::initializer_list<int> syscalls);
bool bindMountPaths(std::string target_root, std::initializer_list<std::string> paths);
};
#endif

15
sandbox/sandbox-openbsd.h Normal file
View File

@ -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<int> syscalls);
};
#endif

27
sandbox/sandbox.h Normal file
View File

@ -0,0 +1,27 @@
#ifndef SANDBOX_H
#define SANDBOX_H
#include <vector>
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<std::string> 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

11
sandbox/sandboxfactory.h Normal file
View File

@ -0,0 +1,11 @@
#ifndef SANDBOXFACTORY_H
#define SANDBOXFACTORY_H
#include <memory>
#include "sandbox.h"
#include "sandbox-linux.h"
#include "sandbox-openbsd.h"
inline std::unique_ptr<Sandbox> createSandbox()
{
return std::make_unique<SandboxLinux>();
}
#endif