diff --git a/Makefile b/Makefile index ce82cd8..6dbe62d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.SUFFIXES: .3 .3.html +.SUFFIXES: .3 .3.html .dot .svg .gnuplot .png # Comment if you don't need statically linked. # This is only for the sample program! @@ -126,6 +126,9 @@ REGRESS_SRCS = regress/regress.c \ regress/test-file-get.c \ regress/test-ping.c \ regress/test-upload.c +SVGS = figure1.svg \ + figure2.png \ + figure3.png all: libkcgi.a libkcgihtml.a libkcgijson.a libkcgixml.a libkcgiregress.a @@ -212,11 +215,11 @@ install: all sample: sample.o libkcgi.a libkcgihtml.a $(CC) -o $@ $(STATIC) sample.o -L. libkcgihtml.a libkcgi.a -lz -www: index.html kcgi.tgz kcgi.tgz.sha512 $(HTMLS) +www: $(SVGS) index.html kcgi.tgz kcgi.tgz.sha512 $(HTMLS) installwww: www mkdir -p $(PREFIX)/snapshots - install -m 0444 index.html index.css $(HTMLS) $(PREFIX) + install -m 0444 index.html index.css $(SVGS) $(HTMLS) $(PREFIX) install -m 0444 sample.c $(PREFIX)/sample.c.txt install -m 0444 kcgi.tgz kcgi.tgz.sha512 $(PREFIX)/snapshots/ install -m 0444 kcgi.tgz $(PREFIX)/snapshots/kcgi-$(VERSION).tgz @@ -245,8 +248,14 @@ kcgi.tgz: (cd .dist && tar zcf ../$@ kcgi-$(VERSION)) rm -rf .dist +.dot.svg: + dot -Tsvg -o $@ $< + +.gnuplot.png: + gnuplot $< + clean: - rm -f kcgi.tgz kcgi.tgz.sha512 index.html $(HTMLS) sample sample.o + rm -f kcgi.tgz kcgi.tgz.sha512 index.html $(SVGS) $(HTMLS) sample sample.o rm -f libkcgi.a $(LIBOBJS) rm -f libkcgihtml.a kcgihtml.o rm -f libkcgijson.a kcgijson.o diff --git a/figure1.dot b/figure1.dot new file mode 100644 index 0000000..dc128d1 --- /dev/null +++ b/figure1.dot @@ -0,0 +1,21 @@ +digraph { + node [shape="ellipse"; width="0"; height="0"; margin="0.05,0.05"]; + edge [arrowsize="0.5";]; + + subgraph cluster0 { + color="black"; + client [label="application logic"]; + subgraph cluster1 { + parser [shape="record"; label="{ HTTP parser | input validator }"]; + color="red"; + label="sandboxed child"; + labelloc="b"; + } + label="CGI client"; + } + evil [label="CGI server"]; + + parser -> client [label="validated pairs"]; + evil -> parser [label="CGI"]; + client -> evil [label="HTTP"]; +} diff --git a/figure2.gnuplot b/figure2.gnuplot new file mode 100644 index 0000000..b4506c0 --- /dev/null +++ b/figure2.gnuplot @@ -0,0 +1,27 @@ +#!/usr/local/bin/gnuplot +set terminal pngcairo crop enhanced font "Times,15" +set output 'figure2.png' +set border linewidth 1.5 +set style line 1 lc rgb '#800000' lt 1 lw 2 +set style line 2 lc rgb '#ff0000' lt 1 lw 2 +set style line 3 lc rgb '#ff4500' lt 1 lw 2 +set style line 4 lc rgb '#ffa500' lt 1 lw 2 +set style line 5 lc rgb '#006400' lt 1 lw 2 +set style line 6 lc rgb '#0000ff' lt 1 lw 2 +set style line 7 lc rgb '#9400d3' lt 1 lw 2 +set style line 11 lc rgb '#808080' lt 1 +set border 19 back ls 11 +set tics nomirror out scale 0.75 +set style line 12 lc rgb'#808080' lt 0 lw 1 +set grid back ls 12 +######################################################### +set xlabel 'milliseconds' +set size 0.8,0.8 +set key outside right +set ytics 0.25 +unset title +plot 'perf-time-openbsd-base.dat' u 2:($1/100) w l lw 3 ti '(1)', \ + 'perf-time-openbsd-base-cgi.dat' u 2:($1/100) w l lw 3 ti '(2)', \ + 'perf-time-openbsd-kcgi-nosandbox.dat' u 2:($1/100) w l lw 3 ti '(3)', \ + 'perf-time-openbsd-kcgi-nocompress.dat' u 2:($1/100) w l lw 3 ti '(4)', \ + 'perf-time-openbsd-kcgi.dat' u 2:($1/100) w l lw 3 ti '(5)' diff --git a/figure3.gnuplot b/figure3.gnuplot new file mode 100644 index 0000000..bdb4f65 --- /dev/null +++ b/figure3.gnuplot @@ -0,0 +1,29 @@ +#!/usr/local/bin/gnuplot +set terminal pngcairo crop enhanced font "Times,15" +set output 'figure3.png' +set border linewidth 1.5 +set style line 1 lc rgb '#800000' lt 1 lw 2 +set style line 2 lc rgb '#ff0000' lt 1 lw 2 +set style line 3 lc rgb '#ff4500' lt 1 lw 2 +set style line 4 lc rgb '#ffa500' lt 1 lw 2 +set style line 5 lc rgb '#006400' lt 1 lw 2 +set style line 6 lc rgb '#0000ff' lt 1 lw 2 +set style line 7 lc rgb '#9400d3' lt 1 lw 2 +set style line 11 lc rgb '#808080' lt 1 +set border 19 back ls 11 +set tics nomirror out scale 0.75 +set style line 12 lc rgb'#808080' lt 0 lw 1 +set grid back ls 12 +######################################################### +set size 0.8,0.8 +set ylabel "ms" +set xtics rotate by -45 +set style fill solid border -1 +set xtics ("static" 4, "slowcgi(8)" 3, "kcgi(3)" 2, "no sandbox" 1, "no compress" 0) +plot '-' w hist title '' +2.947 +2.481 +2.916 +1.864 +0.22 +e diff --git a/index.css b/index.css index 5c1d841..de4f2e7 100644 --- a/index.css +++ b/index.css @@ -1,77 +1,85 @@ -html, body { margin: 0; - padding: 0; } -body { color: #333; - font-family: Times,serif; - font-size: 14pt; - line-height: 120%; } -code, pre { font-size: 11pt; } -a { text-decoration: none; } -header { box-shadow: 0 1px 4px #888; } -header nav ul { margin: auto 0; - padding: 0; - list-style-type: none; } -header nav li + li { margin-top: 0.5em; } -#header { background-color: #333; - padding: 3em 2% 1ex 2%; - color: #fff; } -#header a { color: #ddd; } -#header a:hover { color: #fff; } -#header nav { padding: 0.5ex 0 1ex 0; } -#version { background-color: #ddd; - padding: 1em 2%; } +html, body { margin: 0; + padding: 0; } +body { color: #333; + font-family: Times,serif; + font-size: 12pt; + line-height: 120%; } +code, pre { font-size: 11pt; } +a { text-decoration: none; } +header { box-shadow: 0 1px 4px #888; } +header nav ul { margin: auto 0; + padding: 0; + list-style-type: none; } +header nav li { margin: 0; } +header nav li + li { margin-top: 0.5em; } +#header { background-color: #333; + padding: 3em 5% 1ex 5%; + color: #fff; } +#header a { color: #ddd; } +#header a:hover { color: #fff; } +#header nav { padding: 0.5ex 0 1ex 0; } +#version { background-color: #ddd; + padding: 1em 5% 0 5%; } #version a, #version time, -#version span.version { font-style: normal; } -h1 { font-size: 32pt; - margin: 0 0 1ex 0; - font-weight: normal; } -h1 .nm { text-shadow: 1px 1px #666; } -h1 .nd { font-size: 28pt; - text-shadow: 1px 1px #333; } -footer { margin-top: 1em; - text-align: center; - color: #666; } -footer a { color: #000; } -article h2 { font-size: x-large; - font-weight: bolder; - color: #333; } -article h3 { font-size: large; - color: #333; } -figure { margin: 0; - color: #444; } -figcaption { padding: 0 1% 0.5ex 1%; } -figcaption span { font-weight: bolder; } -#intro { padding: 0 2%; } -#intro figure { width: 100%; - text-align: center; } -#intro img { max-width: 480px; - width: 100%; } -#sample { padding: 1ex 1%; } -#sample nav i { font-size: 16pt; } -#sample nav { padding: 0.5ex 1em; } -#sample nav a { color: #333; } -#sample nav a:hover i { text-shadow: 1px 1px #aaa; } -#comments { max-height: 0; - padding: 0; } -#nocomments { max-height: 100em; - padding: 1em; } -pre.prettyprint { border: none; - line-height: 150%; - margin: 0; - overflow: hidden; - border-radius: 1ex; - box-shadow: 1px 1px 2px #666; - width: auto; } -pre.prettyprint a { font-style: italic; } -pre.prettyprint a:hover { font-style: italic; } -pre.prettyprint ol.linenums > li - { list-style-type: decimal; } -#features { padding: 1ex 2%; } -footer { padding: 1ex; - margin-top: 1em; - margin-bottom: 1em; } +#version span.version { font-style: normal; } +#version > a { font-size: 11pt; + color: #888; + display: block; + margin-top: 1ex; + text-shadow: 0 0 0px #000; } +#version > a:hover { color: #666; } +h1 { font-size: 32pt; + margin: 0 0 1ex 0; + font-weight: normal; } +.nm, .file { font-style: italic; } +h1 .nm { text-shadow: 1px 1px #666; + font-style: inherit; } +h1 .nd { font-size: 28pt; + text-shadow: 1px 1px #333; } +#intro { font-size: 14pt; } +footer { margin-top: 1em; + text-align: center; + color: #666; } +footer a { color: #000; } +article h2 { font-size: x-large; + font-weight: bolder; + color: #333; } +article h3 { font-size: large; + color: #333; } +article section, +article nav, +article figcaption { padding: 1ex 10%; + margin: 0; } +figure { margin: 0; + text-align: center; + color: #444; } +figcaption span { font-weight: bolder; } +dd { margin: 0 0 1em 0; } +.sample { text-align: left; } +.sample nav i { font-size: 16pt; } +.sample nav a { color: #333; } +.sample nav a:hover i { text-shadow: 1px 1px #aaa; } +nav[data-sblg-nav] { overflow: hidden; } +#longnews, +#comments { max-height: 0; } +#nolongnews, +#nocomments { max-height: 100em; } +pre.prettyprint { border: none; + line-height: 150%; + margin: 0; + overflow: hidden; + box-shadow: 1px 1px 2px #666; + width: auto; } +pre.prettyprint a { font-style: italic; } +pre.prettyprint a:hover { font-style: italic; } +#sample pre.prettyprint ol { padding: 0 10%; } +pre.prettyprint ol.linenums > li { list-style-type: decimal; } +footer { padding: 1ex; + margin-top: 1em; + margin-bottom: 1em; } @media only screen and (max-width: 480px), only screen and (max-device-width: 480px) { -h1 { font-size: 16pt; } -h1 .nd { font-size: 14pt; } -pre { font-size: 8pt; } +h1 { font-size: 16pt; } +h1 .nd { font-size: 14pt; } +pre { font-size: 8pt; } } diff --git a/index.xml b/index.xml index e6605cf..fbc2cde 100644 --- a/index.xml +++ b/index.xml @@ -8,25 +8,21 @@ @@ -46,9 +42,15 @@
-
@@ -57,19 +59,17 @@ kcgi is a minimal CGI library for web applications in ISC licensed ISO C. It was designed to be secure and auditable. - To start, download kcgi.tgz and run make install into your - PREFIX of choice, then read kcgi(3). - The system also ships with the kcgihtml(3), kcgijson(3), kcgixml(3), and kcgi_regress(3) libraries. + To start, install the library then read the usage guide, + which links to the canonical manpage documentation. Contact Kristaps with questions or comments. kcgi is a BSD.lv project.

-
+
- Figure: this simple example implements a server that just echoes Hello, World! as an HTTP + The following simple example implements a server that just echoes Hello, World! as an HTTP response. + Click on any italicitised fields to link to the documentation.
#include <stdint.h>
 #include <stdlib.h>
@@ -129,36 +129,230 @@ int main(void) {
   return(EXIT_SUCCESS);
 }
-
+

- How does it work? + Installation

- The meat of kcgi lies in the khttp_parse(3) - function, which parses key-value input pairs from the HTTP request as well as determing the page - request itself, MIME type, and so on. + First, check if kcgi isn't already a third-part port for your system, such as + for OpenBSD or FreeBSD. + If so, install using that system. +

+

+ If not, you'll need a modern UNIX system. + To date, kcgi has been built and run on GNU/Linux machines, BSD (OpenBSD, FreeBSD), and Mac OSX (Snow Leopard, Lion) on i386 and AMD64. + It has been deployed under Apache, nginx, and OpenBSD's httpd(8) + (the latter two via the slowcgi wrapper). + Begin by downloading kcgi.tgz and verify the archive with kcgi.tgz.sha512. + Once downloaded, compile the software with make, which will automatically run a + configuration script to conditionally deploy portability glue. + Finally, install the software using make install, optionally specifying the + PREFIX if you don't intend to use /usr/local. +

+

+ If kcgi doesn't compile, please send me the config.log + file and the output of the failed compilation. + If you're running on an operating system with an unsupported sandbox, let me know and we can work + together to fit it into the configuration and portability layer. +

+
+
+

+ Usage +

+

+ While this section describes a fairly typical setup, you'll want to read kcgi(3) and related for a fuller description. + The most relevant component of kcgi is input parsing, described in khttp_parse(3). + All key-value pairs are parsed from input, as are non-key-value message bodies. + Consider a sample application that wishes to process two named HTML fields, string (being a + non-empty string) and integer, a signed integer. + First, assign these fields to numeric identifiers. + This simplifies later access of the field values. +

+
+
enum key {
+  KEY_STRING,
+  KEY_INTEGER,
+  KEY__MAX
+};
+
+

+ Next, connect the indices with validation functions and names. + The validation function is run when the value is parsed; the name is the HTML form name for the given + element. + Validation applies to cookies as well as form and query-string data. + You can provide your own validation functions instead of using the stock kcgi + ones described in kvalid_string(3), of course. + An empty string for the name will be applied to non-key-value message bodies. +

+
+
const struct kvalid keys[KEY__MAX] = {
+  { kvalid_stringne, "string" }, 
+  { kvalid_int, "integer" }, 
+};
+
+

+ Next, define a function that acts upon parsed fields. + Note that this is application logic, and thus constitutes only an example: each application will + handle its input differently. + For simplicity, I focus only on the string input. + If the value is found, it is assigned into the fieldmap array. + If it was found but did not validate, it is assigned into the fieldnmap array. + In this trivial example, the function emits the string values if found or indicates that they're not + found (or not valid). + There can be multiple inputs matching the same name, such as for HTML checkboxes. +

+

+ Beyond directly writing to the HTTP document with khttp_write(3) and + templating with khttp_template(3), kcgi + provides the kcgihtml(3), kcgijson(3), and + kcgixml(3) libraries for media-specific output functions. + These are intended only for the simplest use: complex applications will probably use their own. +

+
+
void process(struct kreq *r) {
+  struct kpair *p;
+  khttp_puts(r, "string = ");
+  if ((p = r->fieldmap[KEY_STRING]))
+    khttp_puts(r, p->parsed.s);
+  else if (r->fieldnmap[KEY_STRING])
+    khttp_puts(r, "failed parse");
+  else 
+    khttp_puts(r, "not provided");
+}
+
+

+ Before doing any parsing, sanitise the HTTP context. + To begin, I provide an array of indexed page identifiers. + These define the page requests accepted by the application, in this case being only + /index.html. +

+
+
enum page {
+  PAGE_INDEX,
+  PAGE__MAX
+};
+const char *const pages[PAGE__MAX] = {
+  "index",
+};
+
+

+ Now, validate the page request and HTTP context based upon pre-parsed components. + This function checks the page request (it must be /index), MIME type (expanding to + /index.html), and HTTP method (it must be an HTTP GET, such as + /index.html?string=foo). +

+
+
int sanitise(struct kreq *r) {
+  if (PAGE_INDEX != r->page)
+    return(0);
+  else if (KMIME_TEXT_HTML != r->mime)
+    return(0);
+  return (KMETHOD_GET == r->method);
+}
+
+

+ Putting all of these together, parse the HTTP context, validate it, process it, then free the resources. + This simple example emits an HTTP 404 error regardless of the cause. + In reality, this will switch on HTTP 405 for unsupported methods and empty output for non-text media. + Headers are output using khttp_head(3), with the document body started + with khttp_body(3). + The HTTP context is closed with khttp_free(3). +

+
+
int main(void) {
+  struct kreq r;
+  if (KCGI_OK != khttp_parse(&r, keys, KEY__MAX,
+      pages, PAGE__MAX, PAGE_INDEX))  
+    return(EXIT_FAILURE);
+  if ( ! sanitise(&r)) {
+    khttp_head(&r, kresps[KRESP_STATUS],
+      "%s", khttps[KHTTP_404]);
+    khttp_head(&r, kresps[KRESP_CONTENT_TYPE],
+      "%s", kmimetypes[r.mime]);
+    khttp_body(&r);
+    khttp_puts(&r, "Page not found.");
+  } else {
+    khttp_head(&r, kresps[KRESP_STATUS],
+      "%s", khttps[KHTTP_200]);
+    khttp_head(&r, kresps[KRESP_CONTENT_TYPE],
+      "%s", kmimetypes[r.mime]);
+    khttp_body(&r);
+    process(&r);
+  }
+  khttp_free(&r);
+  return(EXIT_SUCCESS);
+};
+
+

+ Usually, web applications for HTML content will use the page and input maps quite heavily, switching on + the page requested (which is usually known beforehand) and data sent along with the page. + CMS-style web applications will not parse the page identifier, leaving that to a database lookup. + Moreover, while HTML applications can generally disregard the HTTP method, a DAV implementation will + focus much more on the method itself. +

+
+
+

+ Deploying +

+

+ Applications using kcgi behave just like any other application. + To compile kcgi applications, just include the kcgi.h + header file and make sure it appears in the compiler inclusion path. + (According to C99, you'll need to include stdint.h before it for the + int64_t type used for parsing integers.) + Linking is similarly normative: link to libkcgi and, if your system has + compression support, libz. +

+

+ Well-deployed web servers, such as the default OpenBSD server, by + default are deployed within a chroot(2). If + this is the case, you'll need to statically link your binary. + If running within a chroot(2) and on + OpenBSD, be aware that the sandbox method requires /dev/systrace within the + server root. + By default, this file does not exist in the web server root. + Moreover, the default web server root mount-point, /var, is mounted + nodev. + This complication does not exist for the other sandboxes. +

+
+
+

+ Implementation Details +

+

+ The bulk of kcgi lies in khttp_parse(3), which + fully parses the HTTP context. Application developers must invoke this function before all others. - As mentioned in security section, the subroutines of - khttp_parse(3) are invoked inside of a sandboxed child process – - this isolates unvalidated, adversarial input. - It must be matched by an khttp_free(3). + It must be matched by an khttp_free(3), which frees all resources.

- Once khttp_parse(3) has been successfully invoked, the calling - application is free to respond to the parsed and validated request. -

-

- Security -

-

- As a security precaution, the kcgi library parses and validates untrusted - network data in a sandboxed child process by forking within khttp_parse(3), where child process is responsible for reading and - parsing form data from the web server. + The khttp_parse(3) function isolates its parsing and validation of + untrusted network data within a sandboxed child process. + Sandboxes limit the environment available to a process, so exploitable errors in the parsing process (or + validation with third-party libraries) cannot touch the system environment. This parsed data is returned to the parent process over a socket. +

+
+ Implementation Details +
+

This method of sandboxing the untrusted child process follows OpenSSH, and requires special handling for each operating system:

@@ -197,76 +391,11 @@ int main(void) { abruptly killed by the sandbox facility. If required, this kind of validation can take place after the parse validation sequence.

-

- Portability -

-

- kcgi should run on any modern UNIX systems and with any web server. - To date, it has been built and run on GNU/Linux machines, BSD (OpenBSD, FreeBSD), and Mac OSX - (Snow Leopard, Lion) on i386 and AMD64. - It has been deployed under Apache, nginx, and OpenBSD's httpd(8) - (the latter two via the slowcgi wrapper). -

-

- Portability across UNIX systems is made possible by a small configure script that checks - for minor inconsistencies such as strlcpy(3), the Security mechanisms, - and for Compression support. -

-

- Extensibility -

-

- While page maps and input validation are entirely driven by the interfacing application, kcgi also allows for extension of the default HTTP headers, schemas, MIME - types, and so on. - Reasonable default have been provided for convenience. - For specifics, see khttp_parse(3). -

-

- The library can also be extended for different output modes. - Two such modes, kcgihtml(3) and kcgijson(3), - are bundled with the system. - It allows a mechanism for building HTML5 trees around the usual - khttp_write(3) family of functions. -

-

- Compression -

-

- If HAVE_ZLIB is enabled during compilation (via the Portability - mechanism), khttp_body(3) will signal use of zlib to compress the HTTP body. - Compression is only enabled if the client provides the correct (gzip) HTTP request header. -

-

- Input Processing -

-

- All common input methods—query string, cookie, and form (multipart form-data and mixed, - urlencoded, and plain—are supported by kcgi. - As described in the Security section, these fields are all parsed and validated - from network data in a child process. - Each input key-value pair can be matched (by key name) to a validator, which is run when fields are - parsed. - You can then look up key-value pairs constant-time in a table indexed by that key. -

-

- Templating -

-

- Many application will want just to fill in an output template instead of creating complex output trees - (relegating most work to JavaScript and JSON). - kcgi provides the khttp_template(3) family - of functions to fill in files or memory buffers with data. - Templates are the most common usage of kcgi, as they allow for a strong - disconnect between prsentation and logic. -

-

+

+
+

Testing -

+

kcgi is shipped with a fully automated testing framework executed with make regress. @@ -292,6 +421,49 @@ int main(void) { afl-urlencoded binaries using the test cases (and dictionaries, for the first) provided.

+
+

+ Performance +

+

+ Security comes at a price. + By design, kcgi incurs overhead in three ways: first, spawning a child to + process the untrusted network data; second, enacting the sandbox framework; and third, passing parsed + pairs back to the parent context. +

+
+ +
+

+ This figure illustrates the cost of running kcgi against a baseline on OpenBSD + 5.5 running nginx and slowcgi(8), i386. + It shows the empirical cumulative distribution of a statisically-significant number of page requests + (>1000) as measured by ab(1). +

+

+ Line (1) shows a static file being served by the web server. + The high speed is due to the file (most likely) being cached by the web server and/or kernel. + Moving right, line (2) shows a basic CGI request producing no content. + The CGI simply exits with an HTTP 200. + This reflects the cost of invoking slowcgi(8). + Line (3) is a simple kcgi that simply emits an HTTP 200. + This instance is neither sandboxed nor does it compress output. + The overhead from line (2) is due to the additional child being spawned. + Line (4) shows the addition of sandboxing without compression, in this case via + systrace(4). + Lastly, line (5) shows both compression and sandboxing. +

+
+ +
+

+ In this bar chart, I show the same but for relative comparison of the distribution means. + The similarity between the compressed and non-compressed versions are due to the small amount of data + being transmitted in the response body. +

+