diff options
author | Omar Polo <op@omarpolo.com> | 2021-01-16 19:41:34 +0000 |
---|---|---|
committer | Omar Polo <op@omarpolo.com> | 2021-01-16 19:41:34 +0000 |
commit | 881a9dd9c2aebbf73f333dd3d8be4ce5400f717f (patch) | |
tree | f3177f9b6c2d956f25c6ca46fd036eadf96370ec | |
parent | bd726b55be4df8535a2b200a252193649566007a (diff) |
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.
-rw-r--r-- | Makefile | 6 | ||||
-rw-r--r-- | README.md | 40 | ||||
-rw-r--r-- | cgi.c | 128 | ||||
-rw-r--r-- | ex.c | 293 | ||||
-rw-r--r-- | gmid.c | 98 | ||||
-rw-r--r-- | gmid.h | 14 | ||||
-rw-r--r-- | sandbox.c | 16 |
7 files changed, 435 insertions, 160 deletions
@@ -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 @@ -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 unveiled with - (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 unveiled with 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 executable 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)`. @@ -16,19 +16,12 @@ #include <netdb.h> +#include <err.h> #include <errno.h> #include <string.h> #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 */ - - if (pipe(p) == -1) + char addr[NI_MAXHOST]; + const char *ruser, *cissuer, *chash; + int e; + + 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 @@ -0,0 +1,293 @@ +/* + * Copyright (c) 2021 Omar Polo <op@omarpolo.com> + * + * 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 <err.h> +#include <errno.h> + +#include <fcntl.h> +#include <signal.h> +#include <string.h> + +#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; +} @@ -34,6 +34,8 @@ struct vhost hosts[HOSTSLEN]; int connected_clients; int goterror; +int exfd; + struct conf conf; struct etm { /* file extension to mime */ @@ -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 *fds, struct client *clients) 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; @@ -730,6 +733,45 @@ load_vhosts(struct tls_config *tlsconf) } } +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 usage(const char *me) { @@ -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"); - - /* 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"); - - load_vhosts(tlsconf); - - if ((ctx = tls_server()) == NULL) - err(1, "tls_server"); + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, p) == -1) + fatal("socketpair: %s", strerror(errno)); - 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); + switch (fork()) { + case -1: + fatal("fork: %s", strerror(errno)); - close(sock4); - close(sock6); - tls_free(ctx); - tls_config_free(tlsconf); + case 0: /* child */ + close(p[0]); + exfd = p[1]; + listener_main(); + _exit(0); - return 0; + default: /* parent */ + close(p[1]); + return executor_main(p[0]); + } } @@ -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*); @@ -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 |