CORS and kcgi

Background

Read the MDN article on CORS to get started, or jump directly to its definition in the fetch spec (the CORS spec is obsolete). In short, CORS (Cross-Origin Resource Sharing) is a way to specify how a user visiting one address (say, https://bsd.lv), is allowed to make a request of another page on a different domain (say, https://kristaps.bsd.lv).

Why is CORS important? Without it, a sneaky page might make requests of a different domain (say, your bank), masquerading as you. CORS is a technology for protecting users, not websites.

CORS is an evolving concept, so it's wise to make sure nothing has changed in the field since I've written this article. Or better yet, make a pull request with any changes.

Simple Pre-flight Handling

Let's start with OPTIONS handling, documented lightly in MDN or described completely in the RFC. A CORS pre-flight is an OPTIONS HTTP request with the Origin request header (MDN, standard) set. The browser sends this to determine what capabilities a client from an origin host be permitted. The simplest reply to a CORS request is to wildcard permission.

#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;
    if (r.method == KMETHOD_OPTIONS &&
        r.reqmap[KREQU_ORIGIN] != NULL) {
	/* This is a CORS pre-flight request. */
        khttp_head(&r,
            kresps[KRESP_ACCESS_CONTROL_ALLOW_ORIGIN],
            "%s", "*");
        khttp_head(&r, kresps[KRESP_VARY],
	    "%s", "Origin");
        khttp_head(&r, kresps[KRESP_STATUS], 
            "%s", khttps[KHTTP_204]);
	khttp_body(&r);
    } else {
        /* Do other processing here... */
    }
    khttp_free(&r);
    return 0;
}

A wildcard response with Access-Control-Allow-Origin header (MDN, standard) instructs the browser that any request is allowed. There are some limitations not covered in this document: for instance, credentials are not allowed.

This uses the HTTP error code 204 (MDN, RFC) instead of code 200. There exist warnings suggesting that code 204 for this purpose is not handled properly by all browsers, but it's not exactly clear by which browser and in which circumstances.

Use of the Vary header (MDN, RFC) may not be relevant for simple cases, but as per the specification, it should be considered best practice.

Another common invocation that reduce some of the aforementioned limitations is to specifically assign possible responses to the origin, but not to investigate the origin in any way. This is similar to the Bad Old Days, when arbitrary requests were accepted from arbitrary origins.

#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;
    if (r.method == KMETHOD_OPTIONS &&
        r.reqmap[KREQU_ORIGIN] != NULL) {
	/* This is a CORS pre-flight request. */
        khttp_head(&r,
            kresps[KRESP_ACCESS_CONTROL_ALLOW_ORIGIN],
	    "%s", r.reqmap[KREQU_ORIGIN]->val);
        khttp_head(&r,
            kresps[KRESP_ACCESS_CONTROL_ALLOW_METHODS],
	    "%s", "GET, POST");
        khttp_head(&r, kresps[KRESP_VARY],
	    "%s", "Origin");
        khttp_head(&r, kresps[KRESP_STATUS], 
            "%s", khttps[KHTTP_204]);
	khttp_body(&r);
    } else {
        /* Do other processing here... */
    }
    khttp_free(&r);
    return 0;
}

This further leverages the Access-Control-Allow-Methods header (MDN, standard) to limit available HTTP methods to those given. All possible Access-Control-Allow-XXX headers are supported by kcgi.

Complex Pre-flight Handling

All of the above can just as easily be enacted by using a reverse proxy such as relayd(8) or, for heavy-weight users married to Docker, nginx. In most web applications, responses are going to be mapped from specific origins to specific operations.

In the general case, a strategy is to contain all possible responses in a well-defined structure.

struct resp {
    const char *headers;
    const char *methods;
    const char *credentials;
};

This can then be defined over all possible resources.

const char *const pages[1] = { "index" };
const struct resp resps[1] = {
    { NULL, "GET, POST", "true" } /* index */
};

The response can then condition on the page field in struct kreq and deliver any matching headers (KRESP_ACCESS_CONTROL_ALLOW_HEADERS, KRESP_ACCESS_CONTROL_ALLOW_METHODS, and KRESP_ACCESS_CONTROL_ALLOW_CREDENTIALS, respectively), or inhibit the header if the value is NULL as appears above with the headers variable. This assumes that the origin is being set opaquely from the request.

Filtering on an origin request is just as easy. If origins may be hard-coded or read from a file at startup, the KREQU_ORIGIN value may be compared with an array of known origins.

In the most complex cases, the resps array above may be rendered multi-dimensional to map individual origins to individual resources. If a given origin is not known, simply do not respond with a KRESP_ACCESS_CONTROL_ALLOW_ORIGIN header.