commit 881a9dd9c2aebbf73f333dd3d8be4ce5400f717f from: Omar Polo date: Sat Jan 16 19:41:34 2021 UTC split into two processes: listener and executor this way, we can sandbox the listener with seccomp (todo) or capsicum (already done) and still have CGI scripts. When we want to exec, we tell the executor what to do, the executor executes the scripts and send the fd backt to the listener. commit - bd726b55be4df8535a2b200a252193649566007a commit + 881a9dd9c2aebbf73f333dd3d8be4ce5400f717f blob - d2e6c7b1d6c3249de0298437cdc454de069ffff9 blob + 6301b4682fee97651d2b72dfeab8a87065f65207 --- Makefile +++ Makefile @@ -14,12 +14,12 @@ lex.yy.c: lex.l y.tab.c y.tab.c: parse.y ${YACC} -b y -d parse.y -OBJS = gmid.o iri.o utf8.o lex.yy.o y.tab.o cgi.o sandbox.o +OBJS = gmid.o iri.o utf8.o lex.yy.o y.tab.o ex.o cgi.o sandbox.o gmid: ${OBJS} ${CC} ${OBJS} -o gmid ${LDFLAGS} -TAGS: gmid.c iri.c utf8.c - -etags gmid.c iri.c utf8.c || true +TAGS: gmid.c iri.c utf8.c ex.c cgi.c sandbox.c + -etags gmid.c iri.c utf8.c ex.c cgi.c sandbox.c || true clean: rm -f *.o lex.yy.c y.tab.c y.tab.h y.output gmid iri_test blob - 658208fced0275052cc1b87f657a2d5675ea3f37 blob + 334315b6b01aab28576ef29e8108b4ff7c363a7c --- README.md +++ README.md @@ -2,16 +2,15 @@ > dead simple, zero configuration Gemini server -gmid is a simple and minimal Gemini server. It requires no -configuration whatsoever so it's well suited for local development -machines. +gmid is a simple and minimal Gemini server. It can run without +configuration, so it's well suited for local development, but at the +same time has a configuration file flexible enough to meet the +requirements of most capsules. -Care has been taken to assure that gmid doesn't serve files outside -the given directory, and it won't follow symlinks. Furthermore, on -OpenBSD, gmid is also `pledge(2)`ed and `unveil(2)`ed: the set of -pledges are `stdio rpath inet`, with the addition of `proc exec` if -CGI scripts are enabled, while the given directory is unveiled with -`rx`. +gmid was initially written to serve static files, but can also +optionally execute CGI scripts. It was also written with security in +mind: on FreeBSD and OpenBSD is sandboxed via `capsicum(4)`and +`pledge(2)`/`unveil(2)` respectively. ## Features @@ -22,7 +21,7 @@ CGI scripts are enabled, while the given directory is - (very) low memory footprint - small codebase, easily hackable - virtual hosts - - sandboxed on OpenBSD and FreeBSD + - sandboxed by default on OpenBSD and FreeBSD ## Drawbacks @@ -31,10 +30,6 @@ CGI scripts are enabled, while the given directory is connection per-second you'd probably want to run multiple gmid instances behind relayd/haproxy or a different server. - - the sandbox on FreeBSD is **NOT** activated if CGI scripts are - enabled: CGI script cannot be used with the way `capsicum(4)` works - - ## Building gmid depends a POSIX libc and libtls. It can probably be linked @@ -53,3 +48,20 @@ The Makefile isn't able to produce a statically linked strip gmid to enjoy your ~2.3M statically-linked gmid. + + +## Architecture/Security considerations + +gmid is composed by two processes: a listener and an executor. The +listener process is the only one that needs internet access and is +sandboxed. When a CGI script needs to be executed, the executor +(outside of the sandbox) sets up a pipe and gives one end to the +listener, while the other is bound to the CGI script standard output. +This way, is still possible to execute CGI scripts without restriction +even if the presence of a sandbox. + +On OpenBSD, the listener process runs with the `stdio recvfd rpath +inet` pledges and has `unveil(2)`ed only the directories that it +serves; the executor has `stdio sendfd proc exec` as pledges. + +On FreeBSD, the executor process is sandboxed with `capsicum(4)`. blob - 30f554ccbd5631180837cf6d545c069f17529de9 blob + b25aa5d7e844d05820da24098072ea9ab419f316 --- cgi.c +++ cgi.c @@ -16,19 +16,12 @@ #include +#include #include #include #include "gmid.h" -static inline void -safe_setenv(const char *name, const char *val) -{ - if (val == NULL) - val = ""; - setenv(name, val, 1); -} - /* * the inverse of this algorithm, i.e. starting from the start of the * path + strlen(cgi), and checking if each component, should be @@ -76,94 +69,53 @@ int start_cgi(const char *spath, const char *relpath, const char *query, struct pollfd *fds, struct client *c) { - pid_t pid; - int p[2]; /* read end, write end */ + char addr[NI_MAXHOST]; + const char *ruser, *cissuer, *chash; + int e; - if (pipe(p) == -1) + e = getnameinfo((struct sockaddr*)&c->addr, sizeof(c->addr), + addr, sizeof(addr), + NULL, 0, + NI_NUMERICHOST); + if (e != 0) goto err; - switch (pid = fork()) { - case -1: - goto err; - - case 0: { /* child */ - char *ex, *requri, *portno; - char addr[NI_MAXHOST]; - char *argv[] = { NULL, NULL, NULL }; - int ec; - - close(p[0]); - if (dup2(p[1], 1) == -1) - goto childerr; - - ec = getnameinfo((struct sockaddr*)&c->addr, sizeof(c->addr), - addr, sizeof(addr), - NULL, 0, - NI_NUMERICHOST | NI_NUMERICSERV); - if (ec != 0) - goto childerr; - - if (asprintf(&portno, "%d", conf.port) == -1) - goto childerr; - - if (asprintf(&ex, "%s/%s", c->host->dir, spath) == -1) - goto childerr; - - if (asprintf(&requri, "%s%s%s", spath, - *relpath == '\0' ? "" : "/", relpath) == -1) - goto childerr; - - argv[0] = argv[1] = ex; - - /* fix the env */ - safe_setenv("GATEWAY_INTERFACE", "CGI/1.1"); - safe_setenv("SERVER_SOFTWARE", "gmid"); - safe_setenv("SERVER_PORT", portno); - - if (!strcmp(c->host->domain, "*")) - safe_setenv("SERVER_NAME", c->host->domain) - - safe_setenv("SCRIPT_NAME", spath); - safe_setenv("SCRIPT_EXECUTABLE", ex); - safe_setenv("REQUEST_URI", requri); - safe_setenv("REQUEST_RELATIVE", relpath); - safe_setenv("QUERY_STRING", query); - safe_setenv("REMOTE_HOST", addr); - safe_setenv("REMOTE_ADDR", addr); - safe_setenv("DOCUMENT_ROOT", c->host->dir); - - if (tls_peer_cert_provided(c->ctx)) { - safe_setenv("AUTH_TYPE", "Certificate"); - safe_setenv("REMOTE_USER", tls_peer_cert_subject(c->ctx)); - safe_setenv("TLS_CLIENT_ISSUER", tls_peer_cert_issuer(c->ctx)); - safe_setenv("TLS_CLIENT_HASH", tls_peer_cert_hash(c->ctx)); - } - - execvp(ex, argv); - goto childerr; + if (tls_peer_cert_provided(c->ctx)) { + ruser = tls_peer_cert_subject(c->ctx); + cissuer = tls_peer_cert_issuer(c->ctx); + chash = tls_peer_cert_hash(c->ctx); + } else { + ruser = NULL; + cissuer = NULL; + chash = NULL; } - default: /* parent */ - close(p[1]); - close(c->fd); - c->fd = p[0]; - c->child = pid; - mark_nonblock(c->fd); - c->state = S_SENDING; - handle_cgi(fds, c); - return 0; - } + if (!send_string(exfd, spath) + || !send_string(exfd, relpath) + || !send_string(exfd, query) + || !send_string(exfd, addr) + || !send_string(exfd, ruser) + || !send_string(exfd, cissuer) + || !send_string(exfd, chash) + || !send_vhost(exfd, c->host)) + goto err; -err: - if (!start_reply(fds, c, TEMP_FAILURE, "internal server error")) + close(c->fd); + if ((c->fd = recv_fd(exfd)) == -1) { + if (!start_reply(fds, c, TEMP_FAILURE, "internal server error")) + return 0; + goodbye(fds, c); return 0; - goodbye(fds, c); + } + c->child = 1; + c->state = S_SENDING; + cgi_poll_on_child(fds, c); + /* handle_cgi(fds, c); */ return 0; -childerr: - dprintf(p[1], "%d internal server error\r\n", TEMP_FAILURE); - close(p[1]); - _exit(1); +err: + /* fatal("cannot talk to the executor process: %s", strerror(errno)); */ + err(1, "cannot talk to the executor process"); } void blob - /dev/null blob + 7ae4e9395827f73f056049f42a307889793ddc63 (mode 644) --- /dev/null +++ ex.c @@ -0,0 +1,293 @@ +/* + * Copyright (c) 2021 Omar Polo + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +#include +#include +#include + +#include "gmid.h" + +int +send_string(int fd, const char *str) +{ + ssize_t len; + + if (str == NULL) + len = 0; + else + len = strlen(str); + + if (write(fd, &len, sizeof(len)) != sizeof(len)) + return 0; + + if (len != 0) + if (write(fd, str, len) != len) + return 0; + + return 1; +} + +int +recv_string(int fd, char **ret) +{ + ssize_t len; + + if (read(fd, &len, sizeof(len)) != sizeof(len)) + return 0; + + if (len == 0) { + *ret = NULL; + return 1; + } + + if ((*ret = calloc(1, len+1)) == NULL) + return 0; + + if (read(fd, *ret, len) != len) + return 0; + return 1; +} + +int +send_vhost(int fd, struct vhost *vhost) +{ + ssize_t n; + + if (vhost < hosts || vhost > hosts + HOSTSLEN) + return 0; + + n = hosts - vhost; + return write(fd, &n, sizeof(n)) == sizeof(n); +} + +int +recv_vhost(int fd, struct vhost **vhost) +{ + ssize_t n; + + if (read(fd, &n, sizeof(n)) != sizeof(n)) + return 0; + + if (n < 0 || n > HOSTSLEN) + return 0; + + *vhost = &hosts[n]; + if ((*vhost)->domain == NULL) + return 0; + return 1; +} + +/* send d though fd. see /usr/src/usr.sbin/syslogd/privsep_fdpass.c + * for an example */ +int +send_fd(int fd, int d) +{ + struct msghdr msg; + union { + struct cmsghdr hdr; + unsigned char buf[CMSG_SPACE(sizeof(int))]; + } cmsgbuf; + struct cmsghdr *cmsg; + struct iovec vec; + int result = 1; + ssize_t n; + + memset(&msg, 0, sizeof(msg)); + + if (d >= 0) { + msg.msg_control = &cmsgbuf.buf; + msg.msg_controllen = sizeof(cmsgbuf.buf); + cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + *(int*)CMSG_DATA(cmsg) = d; + } else + result = 0; + + vec.iov_base = &result; + vec.iov_len = sizeof(int); + msg.msg_iov = &vec; + msg.msg_iovlen = 1; + + if ((n = sendmsg(fd, &msg, 0)) == -1 || n != sizeof(int)) { + fprintf(stderr, "sendmsg: got %zu but wanted %zu: (errno) %s", + n, sizeof(int), strerror(errno)); + return 0; + } + return 1; +} + +/* receive a descriptor via fd */ +int +recv_fd(int fd) +{ + struct msghdr msg; + union { + struct cmsghdr hdr; + char buf[CMSG_SPACE(sizeof(int))]; + } cmsgbuf; + struct cmsghdr *cmsg; + struct iovec vec; + ssize_t n; + int result; + + memset(&msg, 0, sizeof(msg)); + vec.iov_base = &result; + vec.iov_len = sizeof(int); + msg.msg_iov = &vec; + msg.msg_iovlen = 1; + msg.msg_control = &cmsgbuf.buf; + msg.msg_controllen = sizeof(cmsgbuf.buf); + + if ((n = recvmsg(fd, &msg, 0)) != sizeof(int)) { + fprintf(stderr, "read %zu bytes bu wanted %zu\n", n, sizeof(int)); + return -1; + } + + if (result) { + cmsg = CMSG_FIRSTHDR(&msg); + if (cmsg == NULL || cmsg->cmsg_type != SCM_RIGHTS) + return -1; + return (*(int *)CMSG_DATA(cmsg)); + } else + return -1; +} + +static inline void +safe_setenv(const char *name, const char *val) +{ + if (val == NULL) + val = ""; + setenv(name, val, 1); +} + +/* fd or -1 on error */ +static int +launch_cgi(const char *spath, const char *relpath, const char *query, + const char *addr, const char *ruser, const char *cissuer, const char *chash, + struct vhost *vhost) +{ + int p[2]; /* read end, write end */ + + if (pipe2(p, O_NONBLOCK) == -1) + return -1; + + switch (fork()) { + case -1: + return -1; + + case 0: { /* child */ + char *portno, *ex, *requri; + char *argv[] = { NULL, NULL, NULL }; + + close(p[0]); + if (dup2(p[1], 1) == -1) + goto childerr; + + if (asprintf(&portno, "%d", conf.port) == -1) + goto childerr; + + if (asprintf(&ex, "%s/%s", vhost->dir, spath) == -1) + goto childerr; + + if (asprintf(&requri, "%s%s%s", spath, + (relpath != NULL && *relpath == '\0') ? "" : "/", relpath) == -1) + goto childerr; + + argv[0] = argv[1] = ex; + + safe_setenv("GATEWAY_INTERFACE", "CGI/1.1"); + safe_setenv("SERVER_SOFTWARE", "gmid"); + safe_setenv("SERVER_PORT", portno); + + if (!strcmp(vhost->domain, "*")) + safe_setenv("SERVER_NAME", vhost->domain); + + safe_setenv("SCRIPT_NAME", spath); + safe_setenv("SCRIPT_EXECUTABLE", ex); + safe_setenv("REQUEST_URI", requri); + safe_setenv("REQUEST_RELATIVE", relpath); + safe_setenv("QUERY_STRING", query); + safe_setenv("REMOTE_HOST", addr); + safe_setenv("REMOTE_ADDR", addr); + safe_setenv("DOCUMENT_ROOT", vhost->dir); + + if (ruser != NULL) { + safe_setenv("AUTH_TYPE", "Certificate"); + safe_setenv("REMOTE_USER", ruser); + safe_setenv("TLS_CLIENT_ISSUER", cissuer); + safe_setenv("TLS_CLIENT_HASH", chash); + } + + execvp(ex, argv); + goto childerr; + } + + default: + close(p[1]); + return p[0]; + } + +childerr: + dprintf(p[1], "%d internal server error\r\n", TEMP_FAILURE); + _exit(1); +} + +int +executor_main(int fd) +{ + char *spath, *relpath, *query, *addr, *ruser, *cissuer, *chash; + struct vhost *vhost; + int d; + +#ifdef __OpenBSD__ + pledge("stdio sendfd proc exec", NULL); +#endif + + for (;;) { + if (!recv_string(fd, &spath) + || !recv_string(fd, &relpath) + || !recv_string(fd, &query) + || !recv_string(fd, &addr) + || !recv_string(fd, &ruser) + || !recv_string(fd, &cissuer) + || !recv_string(fd, &chash) + || !recv_vhost(fd, &vhost)) + break; + + d = launch_cgi(spath, relpath, query, + addr, ruser, cissuer, chash, vhost); + if (!send_fd(fd, d)) + break; + + free(spath); + free(relpath); + free(query); + free(addr); + free(ruser); + free(cissuer); + free(chash); + } + + /* kill all process in my group. This means the listener and + * every pending CGI script. */ + kill(0, SIGINT); + return 1; +} blob - 7bd722b609bf1105d2ccb9431e3b7645685f0dd1 blob + 56250f46649c67604df41cf2164e1a96ab8de5ff --- gmid.c +++ gmid.c @@ -33,6 +33,8 @@ struct vhost hosts[HOSTSLEN]; int connected_clients; int goterror; + +int exfd; struct conf conf; @@ -458,7 +460,7 @@ handle(struct pollfd *fds, struct client *client) /* fallthrough */ case S_SENDING: - if (client->child != -1) + if (client->child) handle_cgi(fds, client); else send_file(NULL, NULL, fds, client); @@ -567,7 +569,8 @@ do_accept(int sock, struct tls *ctx, struct pollfd *fd clients[i].state = S_HANDSHAKE; clients[i].fd = -1; - clients[i].child = -1; + clients[i].child = 0; + clients[i].waiting_on_child = 0; clients[i].buf = MAP_FAILED; clients[i].af = AF_INET; clients[i].addr = addr; @@ -728,6 +731,45 @@ load_vhosts(struct tls_config *tlsconf) if ((h->dirfd = open(h->dir, O_RDONLY | O_DIRECTORY)) == -1) err(1, "open %s for domain %s", h->dir, h->domain); } +} + +int +listener_main() +{ + int sock4, sock6; + struct tls *ctx = NULL; + struct tls_config *tlsconf; + + if ((tlsconf = tls_config_new()) == NULL) + err(1, "tls_config_new"); + + /* optionally accept client certs, but don't try to verify them */ + tls_config_verify_client_optional(tlsconf); + tls_config_insecure_noverifycert(tlsconf); + + if (tls_config_set_protocols(tlsconf, conf.protos) == -1) + err(1, "tls_config_set_protocols"); + + if ((ctx = tls_server()) == NULL) + errx(1, "tls_server failure"); + + load_vhosts(tlsconf); + + if (tls_configure(ctx, tlsconf) == -1) + errx(1, "tls_configure: %s", tls_error(ctx)); + + if (!conf.foreground && daemon(0, 1) == -1) + exit(1); + + sock4 = make_socket(conf.port, AF_INET); + sock6 = -1; + if (conf.ipv6) + sock6 = make_socket(conf.port, AF_INET6); + + sandbox(); + loop(ctx, sock4, sock6); + + return 0; } void @@ -742,9 +784,7 @@ usage(const char *me) int main(int argc, char **argv) { - struct tls *ctx = NULL; - struct tls_config *tlsconf; - int sock4, sock6, ch; + int ch, p[2]; const char *config_path = NULL; size_t i; int conftest = 0; @@ -838,41 +878,21 @@ main(int argc, char **argv) if (!conf.foreground) signal(SIGHUP, SIG_IGN); - if ((tlsconf = tls_config_new()) == NULL) - err(1, "tls_config_new"); + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, p) == -1) + fatal("socketpair: %s", strerror(errno)); - /* optionally accept client certs, but don't try to verify them */ - tls_config_verify_client_optional(tlsconf); - tls_config_insecure_noverifycert(tlsconf); + switch (fork()) { + case -1: + fatal("fork: %s", strerror(errno)); - if (tls_config_set_protocols(tlsconf, conf.protos) == -1) - err(1, "tls_config_set_protocols"); + case 0: /* child */ + close(p[0]); + exfd = p[1]; + listener_main(); + _exit(0); - load_vhosts(tlsconf); - - if ((ctx = tls_server()) == NULL) - err(1, "tls_server"); - - if (tls_configure(ctx, tlsconf) == -1) - errx(1, "tls_configure: %s", tls_error(ctx)); - - sock4 = make_socket(conf.port, AF_INET); - if (conf.ipv6) - sock6 = make_socket(conf.port, AF_INET6); - else - sock6 = -1; - - if (!conf.foreground && daemon(0, 1) == -1) - exit(1); - - sandbox(); - - loop(ctx, sock4, sock6); - - close(sock4); - close(sock6); - tls_free(ctx); - tls_config_free(tlsconf); - - return 0; + default: /* parent */ + close(p[1]); + return executor_main(p[0]); + } } blob - b595aabb87137c4f2958fbd36c8569e392d6fce1 blob + ed2a6f023dd1d1231cc6c03d473e21739014bd55 --- gmid.h +++ gmid.h @@ -72,6 +72,7 @@ struct conf { }; extern struct conf conf; +extern int exfd; enum { S_HANDSHAKE, @@ -87,7 +88,7 @@ struct client { int code; const char *meta; int fd, waiting_on_child; - pid_t child; + int child; char sbuf[1024]; /* static buffer */ void *buf, *i; /* mmap buffer */ ssize_t len, off; /* mmap/static buffer */ @@ -149,6 +150,8 @@ int parse_portno(const char*); void parse_conf(const char*); void load_vhosts(struct tls_config*); +int listener_main(); + void usage(const char*); /* provided by lex/yacc */ @@ -157,6 +160,15 @@ extern int yylineno; extern int yyparse(void); extern int yylex(void); +/* ex.c */ +int send_string(int, const char*); +int recv_string(int, char**); +int send_vhost(int, struct vhost*); +int recv_vhost(int, struct vhost**); +int send_fd(int, int); +int recv_fd(int); +int executor_main(int); + /* cgi.c */ int check_for_cgi(char *, char*, struct pollfd*, struct client*); int start_cgi(const char*, const char*, const char*, struct pollfd*, struct client*); blob - 6618e5dcce42d7857979866b6b22fe504b86a57c blob + a8e301db2c554b8c7458a070d392692f8242d9f5 --- sandbox.c +++ sandbox.c @@ -15,11 +15,6 @@ sandbox() if (h->cgi != NULL) has_cgi = 1; - if (has_cgi) { - LOGW(NULL, "disabling sandbox because CGI scripts are enabled"); - return; - } - if (cap_enter() == -1) err(1, "cap_enter"); } @@ -41,23 +36,14 @@ void sandbox() { struct vhost *h; - int has_cgi = 0; for (h = hosts; h->domain != NULL; ++h) { if (unveil(h->dir, "rx") == -1) err(1, "unveil %s for domain %s", h->dir, h->domain); - - if (h->cgi != NULL) - has_cgi = 1; } - if (pledge("stdio rpath inet proc exec", NULL) == -1) + if (pledge("stdio recvfd rpath inet", NULL) == -1) err(1, "pledge"); - - /* drop proc and exec if cgi isn't enabled */ - if (!has_cgi) - if (pledge("stdio rpath inet", NULL) == -1) - err(1, "pledge"); } #else