Best practises for pledge(2) security

Theory

Internally, kcgi makes considerable use of available security tools. But it's also designed to be invoked in a secure environment. We'll start with pledge(2), which has been around on OpenBSD since version 5.9. If you're reading this tutorial, you're probably on OpenBSD, and you probably have knowledge of pledge(2).

How to begin? Read kcgi(3). It includes canonical information on which pledge(2) promises you'll need for each function in the library. This is just a tutorial—the manpage is canonical and overrides what you may read here.

Next, assess the promises that your application needs. From kcgi(3), it's easy to see which promises we'll need to start. You'll need to augment this list with whichever tools you're also using. The general push is to start with the broadest set of required promises, then restrict as quickly as possible. Sometimes this can be done in a single pledge(2), but other times it takes a few.

Source Code: CGI

Let's start with a trivial CGI application. This parses the HTTP context with khttp_parse(3), then emits output using the writing functions. Most applications will perform some sort of work based upon the context, such as with form input, but it's irrelevant for the purposes of explanation.

I avoid error checking for brevity, but each function needs to be checked for return values. This goes for all examples, all the time!

#include <sys/types.h> /* size_t, ssize_t */
#include <stdarg.h> /* va_list */
#include <stddef.h> /* NULL */
#include <stdint.h> /* int64_t */
#include <kcgi.h>

int 
main(void) {
    struct kreq r;
    const char *const pages[1] = { "index" };

    if (khttp_parse(&r, NULL, 0, pages, 1, 0) != KCGI_OK)
        return 0;
    khttp_head(&r, kresps[KRESP_STATUS], 
        "%s", khttps[KHTTP_200]);
    khttp_head(&r, kresps[KRESP_CONTENT_TYPE], 
        "%s", kmimetypes[KMIME_TEXT_PLAIN]);
    khttp_body(&r);
    khttp_puts(&r, "Hello, world!\n");
    khttp_free(&r);
    return 0;
}

Obviously, this does very little. And that's fine, because pledge(2) doesn't really care about how complex your application is—just which system resources are used. Our main focus is going to be khttp_parse(3), which requires the system resources to create its parsing contexts and safely get our data extracted and processed. After that, all we're doing is constructing a response.

To use this security tool, all we need is to include <unistd.h> and to understand the pledge(2) system call. We'll start with the most significant protection: by constraining our application after the context has been parsed.

if (khttp_parse(&r, NULL, 0, pages, 1, 0) != KCGI_OK)
    return 0;
if (pledge("stdio", NULL) == -1)
    return 0;

This pledge(2) promise restricts the process to simply using available I/O channels. The only kcgi functions requiring more than "stdio" are the file-based template functions described in khttp_template(3) and khttp_templatex(3), which also require "rpath".

We can further secure our systems by pushing pledge(2) before the call to khttp_parse(3). The only promises khttp_parse(3) requires are to fork(2) its internal handlers.

if (pledge("stdio proc", NULL) == -1)
    return 0;
if (khttp_parse(&r, NULL, 0, pages, 1, 0) != KCGI_OK)
    return 0;
if (pledge("stdio", NULL) == -1)
    return 0;

Or if one is using khttp_template(3) to marshall a response:

if (pledge("stdio rpath proc", NULL) == -1)
    return 0;
if (khttp_parse(&r, NULL, 0, pages, 1, 0) != KCGI_OK)
    return 0;
if (pledge("stdio rpath", NULL) == -1)
    return 0;

That's it! Obviously, you'll need to expand the set of promises proportionate to your application's needs. Let's put it all together:

#include <sys/types.h> /* size_t, ssize_t */
#include <stdarg.h> /* va_list */
#include <stddef.h> /* NULL */
#include <stdint.h> /* int64_t */
#include <unistd.h> /* pledge */
#include <kcgi.h>

int 
main(void) {
    struct kreq r;
    const char *const pages[1] = { "index" };

    if (pledge("stdio proc", NULL) == -1)
        return 0;
    if (khttp_parse(&r, NULL, 0, pages, 1, 0) != KCGI_OK)
        return 0;
    if (pledge("stdio", NULL) == -1)
        return 0;
    khttp_head(&r, kresps[KRESP_STATUS], 
        "%s", khttps[KHTTP_200]);
    khttp_head(&r, kresps[KRESP_CONTENT_TYPE], 
        "%s", kmimetypes[KMIME_TEXT_PLAIN]);
    khttp_body(&r);
    khttp_puts(&r, "Hello, world!\n");
    khttp_free(&r);
    return 0;
}

One last note: portability. If you're going to be writing CGI scripts that must be portable across architectures, consider using oconfigure for a selection of portable OpenBSD functions and feature tests.

Source Code: FastCGI

This follows the logic of the CGI example, but we need to have extra promises to account for the increased complexity of FastCGI processing. First, start with our simple example without any header inclusions—all of which are the same as in the CGI example.

Again, each function needs to be checked for return values. This would otherwise clutter up the examples, but production systems must error check.

int 
main(void) {
    struct kreq r;
    const char *const pages[1] = { "index" };
    struct kfcgi *fcgi;
    enum kcgi_err er;

    if (khttp_fcgi_init(&fcgi, NULL, 0, pages, 1, 0) != KCGI_OK)
        return 0;
    for (;;) {
        if (khttp_fcgi_parse(fcgi, &req) != KCGI_OK)
            break;
        khttp_head(&r, kresps[KRESP_STATUS], 
            "%s", khttps[KHTTP_200]);
        khttp_head(&r, kresps[KRESP_CONTENT_TYPE], 
            "%s", kmimetypes[KMIME_TEXT_PLAIN]);
        khttp_body(&r);
        khttp_puts(&r, "Hello, world!\n");
        khttp_free(&r);
    }
    return 0;
}

The FastCGI-specific functions we need to manage are khttp_fcgi_init(3) and khttp_fcgi_parse(3). The promises required are all noted in kcgi(3). The rest follow from the CGI example.

int 
main(void) {
    struct kreq r;
    const char *const pages[1] = { "index" };
    struct kfcgi *fcgi;
    enum kcgi_err er;

    if (pledge("unix sendfd recvfd proc stdio", NULL) == -1)
        return 0;
    if (khttp_fcgi_init(&fcgi, NULL, 0, pages, 1, 0) != KCGI_OK)
        return 0;
    if (pledge("stdio recvfd", NULL) == -1)
        return 0;
    for (;;) {
        if (khttp_fcgi_parse(fcgi, &req) != KCGI_OK)
            break;
        khttp_head(&r, kresps[KRESP_STATUS], 
            "%s", khttps[KHTTP_200]);
        khttp_head(&r, kresps[KRESP_CONTENT_TYPE], 
            "%s", kmimetypes[KMIME_TEXT_PLAIN]);
        khttp_body(&r);
        khttp_puts(&r, "Hello, world!\n");
        khttp_free(&r);
    }
    khttp_fcgi_free(fcgi);
    return 0;
}