diff --git a/CHANGES b/CHANGES index 1de7c2a..0207ee2 100644 --- a/CHANGES +++ b/CHANGES @@ -1,136 +1,139 @@ +4.0: + Fix directory traversal vulnerability (fuzzie) + 3.1: - Add -. flag to disable vhosting - Support server push CGI - Handle busybox tcpsvd - Changed formatting of directory indexing - Handle read timeout - Fix if-modified-since bug with keepalive connections - Change to CGI's directory on exec + Add -. flag to disable vhosting + Support server push CGI + Handle busybox tcpsvd + Changed formatting of directory indexing + Handle read timeout + Fix if-modified-since bug with keepalive connections + Change to CGI's directory on exec 3.0: - More or less a ground-up rewrite. A few fnord parts remain - here and there. + More or less a ground-up rewrite. A few fnord parts remain + here and there. 2.0: - Replace poll with select, which is more portable and may be - slightly faster; however, it's only called for CGI and by - that point you've lost quite a bit in terms of speed - Remove Accept header parsing: it was broken and the result was - that the Accept header had no effect - Remove the .gz trick: I never used it, but I would not be averse - to adding it back if people liked it - Rename to "eris httpd" to acknowledge fork - Add regression test suite - Replace compile-time options with command-line ones - Fix segfault with directory listing of / - Replace buffer_1 and buffer_2 with stdio - Replace libowfat with libc - Add all patches from (defunct) Debian package - Fix if-modified-since date parsing - Make text content-types use charset=UTF-8 - Change default content-type to application/octet-stream - Makefile no longer overrides CC and CPP from parent makes - Don't send Content-type if there's no content - New maintainer: Neale Pickett + Replace poll with select, which is more portable and may be + slightly faster; however, it's only called for CGI and by + that point you've lost quite a bit in terms of speed + Remove Accept header parsing: it was broken and the result was + that the Accept header had no effect + Remove the .gz trick: I never used it, but I would not be averse + to adding it back if people liked it + Rename to "eris httpd" to acknowledge fork + Add regression test suite + Replace compile-time options with command-line ones + Fix segfault with directory listing of / + Replace buffer_1 and buffer_2 with stdio + Replace libowfat with libc + Add all patches from (defunct) Debian package + Fix if-modified-since date parsing + Make text content-types use charset=UTF-8 + Change default content-type to application/octet-stream + Makefile no longer overrides CC and CPP from parent makes + Don't send Content-type if there's no content + New maintainer: Neale Pickett 1.10: - have fallback in case sendfile fails + have fallback in case sendfile fails 1.9: - chdir to cgi's base dir (Kuba Winnicki) - set HTTP_ACCEPT_ENCODING environment variable (Kuba Winnicki) - We actually should export all HTTP headers as HTTP_[header] - Any takers? - Try not to send error message HTTP headers if we already sent the - headers from the CGI (Kuba Winnicki) - <=ims (Gerrit Pape) - 64-bit file I/O cleanliness - fix HTTP ranges (Joachim Berdal Haga via Gerrit Pape) + chdir to cgi's base dir (Kuba Winnicki) + set HTTP_ACCEPT_ENCODING environment variable (Kuba Winnicki) + We actually should export all HTTP headers as HTTP_[header] + Any takers? + Try not to send error message HTTP headers if we already sent the + headers from the CGI (Kuba Winnicki) + <=ims (Gerrit Pape) + 64-bit file I/O cleanliness + fix HTTP ranges (Joachim Berdal Haga via Gerrit Pape) 1.8: - keep current environment in CGI (Laurent Bercot) - make fnord-conf use the UID and not the user name (Fridtjof Busse) - fix typo in buffer_putulonglong (Gerrit Pape) - fix CGI POST off-by-two typo (Mark Hopf) - fix gif->png conversion (Thomas Seck) - remove == bashism from fnord-conf (Thomas Seck) - add bittorrent mime type - make authorization data available to CGIs for GET, too (Paul Jarc) - fix conversion of host name to lower case (Gerrit Pape) - add small test cgi: cgi-post.c - fix CGI POST bug (Moe Wibble) - fix CGI PATH_TRANSLATED bug (Nicolas George) - add optional authentication support (Nicolas George, see README.auth) - make sure error messages are text/html - move /. -> /: conversion before demangling so it can actually be - used as security measure for installations that don't use chroot + keep current environment in CGI (Laurent Bercot) + make fnord-conf use the UID and not the user name (Fridtjof Busse) + fix typo in buffer_putulonglong (Gerrit Pape) + fix CGI POST off-by-two typo (Mark Hopf) + fix gif->png conversion (Thomas Seck) + remove == bashism from fnord-conf (Thomas Seck) + add bittorrent mime type + make authorization data available to CGIs for GET, too (Paul Jarc) + fix conversion of host name to lower case (Gerrit Pape) + add small test cgi: cgi-post.c + fix CGI POST bug (Moe Wibble) + fix CGI PATH_TRANSLATED bug (Nicolas George) + add optional authentication support (Nicolas George, see README.auth) + make sure error messages are text/html + move /. -> /: conversion before demangling so it can actually be + used as security measure for installations that don't use chroot 1.7: - add .mov and .qt for quicktime, .mpg for video/mpeg and .wav for audio/x-wav - add mmap based file serving (should do zero-copy tcp just like sendfile) - add Pragma: no-cache to CGI responses - fix (apparently not exploitable) buffer overrun in do_cgi - This bug was found by Ralf Wildenhues. To my knowledge it is - impossible to exploit this bug on any platform known to me. - fix (harmless) access to uninitialized data + add .mov and .qt for quicktime, .mpg for video/mpeg and .wav for audio/x-wav + add mmap based file serving (should do zero-copy tcp just like sendfile) + add Pragma: no-cache to CGI responses + fix (apparently not exploitable) buffer overrun in do_cgi + This bug was found by Ralf Wildenhues. To my knowledge it is + impossible to exploit this bug on any platform known to me. + fix (harmless) access to uninitialized data 1.6: - add support for $PATH_INFO in CGI environment. - add .pac for netscape proxy autoconfig - add .sig for application/pgp-signature + add support for $PATH_INFO in CGI environment. + add .pac for netscape proxy autoconfig + add .sig for application/pgp-signature 1.5: - fix write timeout handling (found by Lukas Beeler) - fix fnord-conf to use the symbolic account name in run script - (Sebastian D.B. Krause) + fix write timeout handling (found by Lukas Beeler) + fix fnord-conf to use the symbolic account name in run script + (Sebastian D.B. Krause) 1.4: - add dangling symlink based whole-host redirection (see README). This - has the advantage that it can serve normal sites and redirect sites - on the same IP. - add support for non-TCP UCSPI environments (like ucspi-ssl). Please - get the latest version of my ucspi-tcp IPv6 patch as I violated the - UCSPI spec with all versions before 0.88-diff11. - change logging from "127.0.0.1 200 23 Links_(0.96;_Unix) none /index.html" - to "127.0.0.1 200 23 localhost Links_(0.96;_Unix) none /index.html" - (i.e. include the Host: header). Suggested by Thomas Bader. - add "immediate mode". If you give fnord a command line argument, it - will change to that directory and if no "default" directory is - given, it will assume there are no virtual hosts and serve from the - current directory. I have a shell script called "http" that does - tcpserver -RHl localhost 0 8000 /home/leitner/bin/fnord-idx . - to share some directory on my hard drive with some poor Windows - users without npoll (http://www.fefe.de/ncp/). fnord-idx is a new - target (a fnord with DIR_LIST) that is auto-built by make. + add dangling symlink based whole-host redirection (see README). This + has the advantage that it can serve normal sites and redirect sites + on the same IP. + add support for non-TCP UCSPI environments (like ucspi-ssl). Please + get the latest version of my ucspi-tcp IPv6 patch as I violated the + UCSPI spec with all versions before 0.88-diff11. + change logging from "127.0.0.1 200 23 Links_(0.96;_Unix) none /index.html" + to "127.0.0.1 200 23 localhost Links_(0.96;_Unix) none /index.html" + (i.e. include the Host: header). Suggested by Thomas Bader. + add "immediate mode". If you give fnord a command line argument, it + will change to that directory and if no "default" directory is + given, it will assume there are no virtual hosts and serve from the + current directory. I have a shell script called "http" that does + tcpserver -RHl localhost 0 8000 /home/leitner/bin/fnord-idx . + to share some directory on my hard drive with some poor Windows + users without npoll (http://www.fefe.de/ncp/). fnord-idx is a new + target (a fnord with DIR_LIST) that is auto-built by make. 1.3: - make directory listings use non-proportional fonts (thanks, Antonio Dias) - fnord will now optionally (default: enabled) normalize the incoming - host name, i.e. "www.domain.com" -> "www.domain.com:80". That - should cut down on the number of symbolic links. ;) - remove timeout error message. fnord will not drop the connection - without error message. Mozilla used to display the error message - when the user caused another request on the connection with the - timeout. - Uwe Ohse found two more compilation problems. + make directory listings use non-proportional fonts (thanks, Antonio Dias) + fnord will now optionally (default: enabled) normalize the incoming + host name, i.e. "www.domain.com" -> "www.domain.com:80". That + should cut down on the number of symbolic links. ;) + remove timeout error message. fnord will not drop the connection + without error message. Mozilla used to display the error message + when the user caused another request on the connection with the + timeout. + Uwe Ohse found two more compilation problems. 1.2: - Olaf: I changed my initial CGI-interface to NOT use the filesystem but - two pipes. - Add whole-host redirect (see README) - Olaf: added direcory-lists and "index.cgi" support (normal CGI only ! - "nph-index.cgi" is not supported). Fixed some problematic parts in the - CGI-interface (\n -> \r\n converter for http-header and CGI crash - handling) - Fix gzip encoding bug that only happened with keep-alive + Olaf: I changed my initial CGI-interface to NOT use the filesystem but + two pipes. + Add whole-host redirect (see README) + Olaf: added direcory-lists and "index.cgi" support (normal CGI only ! + "nph-index.cgi" is not supported). Fixed some problematic parts in the + CGI-interface (\n -> \r\n converter for http-header and CGI crash + handling) + Fix gzip encoding bug that only happened with keep-alive 1.1: - ship with the parts from libowfat that we actually use - minor speed-up. sendfile is a drag for very small files, so those are - now sent through the same buffer the header is sent through. That - sends the whole answer in one TCP packet if you are lucky, even - without the TCP_CORK magic from Linux. Major speed-up for - benchmarks ;) + ship with the parts from libowfat that we actually use + minor speed-up. sendfile is a drag for very small files, so those are + now sent through the same buffer the header is sent through. That + sends the whole answer in one TCP packet if you are lucky, even + without the TCP_CORK magic from Linux. Major speed-up for + benchmarks ;) 1.0: - initial release + initial release diff --git a/eris.c b/eris.c index c05a9b1..eefae95 100644 --- a/eris.c +++ b/eris.c @@ -72,18 +72,18 @@ /* * Options */ -int doauth = 0; -int docgi = 0; -int doidx = 0; -int nochdir = 0; -int redirect = 0; -int portappend = 0; +int doauth = 0; +int docgi = 0; +int doidx = 0; +int nochdir = 0; +int redirect = 0; +int portappend = 0; /* Variables that persist between requests */ -int cwd; -int keepalive = 0; -char *remote_addr = NULL; -char *remote_ident = NULL; +int cwd; +int keepalive = 0; +char *remote_addr = NULL; +char *remote_ident = NULL; /* * Things that are really super convenient to have globally. @@ -103,7 +103,7 @@ time_t ims; #define BUFFER_SIZE 8192 -char stdout_buf[BUFFER_SIZE]; +char stdout_buf[BUFFER_SIZE]; #include "strings.c" #include "mime.c" @@ -119,39 +119,39 @@ void cork(int enable) { #ifdef TCP_CORK - static int corked = 0; + static int corked = 0; - if (enable != corked) { - setsockopt(1, IPPROTO_TCP, TCP_CORK, &enable, sizeof(enable)); - corked = enable; - } + if (enable != corked) { + setsockopt(1, IPPROTO_TCP, TCP_CORK, &enable, sizeof(enable)); + corked = enable; + } #endif } /** Log a request */ static void dolog(int code, off_t len) -{ /* write a log line to stderr */ - sanitize(host); - sanitize(user_agent); - sanitize(refer); +{ /* write a log line to stderr */ + sanitize(host); + sanitize(user_agent); + sanitize(refer); - fprintf(stderr, "%s %d %lu %s %s %s %s\n", - remote_addr, code, (unsigned long) len, host, user_agent, refer, path); + fprintf(stderr, "%s %d %lu %s %s %s %s\n", + remote_addr, code, (unsigned long) len, host, user_agent, refer, path); } void header(unsigned int code, const char *httpcomment) { - printf("HTTP/1.%d %u %s\r\n", http_version, code, httpcomment); - printf("Server: " FNORD "\r\n"); - printf("Connection: %s\r\n", keepalive?"keep-alive":"close"); + printf("HTTP/1.%d %u %s\r\n", http_version, code, httpcomment); + printf("Server: " FNORD "\r\n"); + printf("Connection: %s\r\n", keepalive?"keep-alive":"close"); } void eoh() { - printf("\r\n"); + printf("\r\n"); } /* @@ -160,30 +160,30 @@ eoh() static void badrequest(long code, const char *httpcomment, const char *message) { - size_t msglen = 0; + size_t msglen = 0; - keepalive = 0; - header(code, httpcomment); - if (message) { - msglen = (strlen(message) * 2) + 15; + keepalive = 0; + header(code, httpcomment); + if (message) { + msglen = (strlen(message) * 2) + 15; - printf("Content-Length: %lu\r\nContent-Type: text/html\r\n", - (unsigned long) msglen); - printf("%s%s", message, message); - } - printf("\r\n"); - fflush(stdout); - dolog(code, msglen); + printf("Content-Length: %lu\r\nContent-Type: text/html\r\n", + (unsigned long) msglen); + printf("%s%s", message, message); + } + printf("\r\n"); + fflush(stdout); + dolog(code, msglen); - exit(0); + exit(0); } void env(const char *k, const char *v) { - if (v) { - setenv(k, v, 1); - } + if (v) { + setenv(k, v, 1); + } } #include "cgi.c" @@ -191,591 +191,592 @@ env(const char *k, const char *v) void not_found() { - char msg[] = "The requested URL does not exist here."; + char msg[] = "The requested URL does not exist here."; - header(404, "Not Found"); - printf("Content-Type: text/html\r\n"); - printf("Content-Length: %lu\r\n", (unsigned long) sizeof msg); - printf("\r\n"); - printf("%s\n", msg); /* sizeof msg includes the NULL */ - dolog(404, sizeof msg); - fflush(stdout); + header(404, "Not Found"); + printf("Content-Type: text/html\r\n"); + printf("Content-Length: %lu\r\n", (unsigned long) sizeof msg); + printf("\r\n"); + printf("%s\n", msg); /* sizeof msg includes the NULL */ + dolog(404, sizeof msg); + fflush(stdout); } char * proto_getenv(char *proto, char *name) { - char buf[80]; + char buf[80]; - snprintf(buf, sizeof buf, "%s%s", proto, name); - return getenv(buf); + snprintf(buf, sizeof buf, "%s%s", proto, name); + return getenv(buf); } void get_ucspi_env() { - char *ucspi = getenv("PROTO"); + char *ucspi = getenv("PROTO"); - if (ucspi) { - char *p; + if (ucspi) { + char *p; - /* Busybox, as usual, has the right idea */ - if ((p = proto_getenv(ucspi, "REMOTEADDR"))) { - remote_addr = strdup(p); - } else { - char *ip = proto_getenv(ucspi, "REMOTEIP"); - char *port = proto_getenv(ucspi, "REMOTEPORT"); + /* Busybox, as usual, has the right idea */ + if ((p = proto_getenv(ucspi, "REMOTEADDR"))) { + remote_addr = strdup(p); + } else { + char *ip = proto_getenv(ucspi, "REMOTEIP"); + char *port = proto_getenv(ucspi, "REMOTEPORT"); - if (ip) { - char buf[80]; + if (ip) { + char buf[80]; - snprintf(buf, sizeof buf, "%s:%s", ip, port); - remote_addr = strdup(buf); - } - } + snprintf(buf, sizeof buf, "%s:%s", ip, port); + remote_addr = strdup(buf); + } + } - if ((p = proto_getenv(ucspi, "REMOTEINFO"))) { - remote_ident = strdup(p); - } - } + if ((p = proto_getenv(ucspi, "REMOTEINFO"))) { + remote_ident = strdup(p); + } + } } void parse_options(int argc, char *argv[]) { - int opt; + int opt; - while (-1 != (opt = getopt(argc, argv, "acdhkprv."))) { - switch (opt) { - case 'a': - doauth = 1; - break; - case 'c': - docgi = 1; - break; - case 'd': - doidx = 1; - break; - case '.': - nochdir = 1; - break; - case 'p': - portappend = 1; - break; - case 'r': - redirect = 1; - break; - case 'v': - printf(FNORD "\n"); - exit(0); - case 'h': - default: - fprintf(stderr, "Usage: %s [OPTIONS]\n", - argv[0]); - fprintf(stderr, "\n"); - fprintf(stderr, "-a Enable authentication\n"); - fprintf(stderr, "-c Enable CGI\n"); - fprintf(stderr, "-d Enable directory listing\n"); - fprintf(stderr, "-. Serve out of ./ (no vhosting)\n"); - fprintf(stderr, "-p Append port to hostname directory\n"); - fprintf(stderr, "-r Enable symlink redirection\n"); - fprintf(stderr, "-v Print version and exit\n"); - exit(69); - } - } + while (-1 != (opt = getopt(argc, argv, "acdhkprv."))) { + switch (opt) { + case 'a': + doauth = 1; + break; + case 'c': + docgi = 1; + break; + case 'd': + doidx = 1; + break; + case '.': + nochdir = 1; + break; + case 'p': + portappend = 1; + break; + case 'r': + redirect = 1; + break; + case 'v': + printf(FNORD "\n"); + exit(0); + case 'h': + default: + fprintf(stderr, "Usage: %s [OPTIONS]\n", + argv[0]); + fprintf(stderr, "\n"); + fprintf(stderr, "-a Enable authentication\n"); + fprintf(stderr, "-c Enable CGI\n"); + fprintf(stderr, "-d Enable directory listing\n"); + fprintf(stderr, "-. Serve out of ./ (no vhosting)\n"); + fprintf(stderr, "-p Append port to hostname directory\n"); + fprintf(stderr, "-r Enable symlink redirection\n"); + fprintf(stderr, "-v Print version and exit\n"); + exit(69); + } + } } void fake_sendfile(int out_fd, int in_fd, off_t *offset, size_t count) { - char buf[BUFFER_SIZE]; - ssize_t l, m; + char buf[BUFFER_SIZE]; + ssize_t l, m; - /* is mmap quicker? does it matter? */ - if (-1 == lseek(in_fd, *offset, SEEK_SET)) { - /* We're screwed. The most helpful thing we can do now is die. */ - fprintf(stderr, "Unable to seek. Dying.\n"); - exit(0); - } - l = read(in_fd, buf, min(count, sizeof buf)); - if (-1 == l) { - /* Also screwed. */ - fprintf(stderr, "Unable to read an open file. Dying.\n"); - exit(0); - } - *offset += l; + /* is mmap quicker? does it matter? */ + if (-1 == lseek(in_fd, *offset, SEEK_SET)) { + /* We're screwed. The most helpful thing we can do now is die. */ + fprintf(stderr, "Unable to seek. Dying.\n"); + exit(0); + } + l = read(in_fd, buf, min(count, sizeof buf)); + if (-1 == l) { + /* Also screwed. */ + fprintf(stderr, "Unable to read an open file. Dying.\n"); + exit(0); + } + *offset += l; - while (l) { - m = write(out_fd, buf, l); - if (-1 == m) { - /* ALSO screwed. */ - fprintf(stderr, "Unable to write to client: %m (req %s). Dying.\n", path); - exit(0); - } - l -= m; - } + while (l) { + m = write(out_fd, buf, l); + if (-1 == m) { + /* ALSO screwed. */ + fprintf(stderr, "Unable to write to client: %m (req %s). Dying.\n", path); + exit(0); + } + l -= m; + } } void serve_file(int fd, char *filename, struct stat *st) { - off_t len, remain; + off_t len, remain; - if (method == POST) { - badrequest(405, "Method Not Supported", "POST is not supported by this URL"); - } + if (method == POST) { + badrequest(405, "Method Not Supported", "POST is not supported by this URL"); + } - if (st->st_mtime <= ims) { - header(304, "Not Changed"); - eoh(); - return; - } + if (st->st_mtime <= ims) { + header(304, "Not Changed"); + eoh(); + return; + } - header(200, "OK"); - printf("Content-Type: %s\r\n", getmimetype(filename)); + header(200, "OK"); + printf("Content-Type: %s\r\n", getmimetype(filename)); - if ((range_end == 0) || (range_end > st->st_size)) { - range_end = st->st_size; - } - len = range_end - range_start; - printf("Content-Length: %llu\r\n", (unsigned long long) len); + if ((range_end == 0) || (range_end > st->st_size)) { + range_end = st->st_size; + } + len = range_end - range_start; + printf("Content-Length: %llu\r\n", (unsigned long long) len); - { - struct tm *tp; - char buf[40]; + { + struct tm *tp; + char buf[40]; - tp = gmtime(&(st->st_mtime)); + tp = gmtime(&(st->st_mtime)); - strftime(buf, sizeof buf, "%a, %d %b %Y %H:%M:%S GMT", tp); - printf("Last-Modified: %s\r\n", buf); - } + strftime(buf, sizeof buf, "%a, %d %b %Y %H:%M:%S GMT", tp); + printf("Last-Modified: %s\r\n", buf); + } - eoh(); - fflush(stdout); + eoh(); + fflush(stdout); - if (method == HEAD) { - return; - } + if (method == HEAD) { + return; + } - for (remain = len; remain; ) { - size_t count = min(remain, SIZE_MAX); - ssize_t sent; + for (remain = len; remain; ) { + size_t count = min(remain, SIZE_MAX); + ssize_t sent; - alarm(SENDFILE_TIMEOUT); - sent = sendfile(1, fd, &range_start, count); - if (-1 == sent) { - fake_sendfile(1, fd, &range_start, count); - } - remain -= sent; - } + alarm(SENDFILE_TIMEOUT); + sent = sendfile(1, fd, &range_start, count); + if (-1 == sent) { + fake_sendfile(1, fd, &range_start, count); + } + remain -= sent; + } - dolog(200, len); + dolog(200, len); } void serve_idx(int fd, char *path) { - DIR *d = fdopendir(fd); - struct dirent *de; + DIR *d = fdopendir(fd); + struct dirent *de; - if (method == POST) { - badrequest(405, "Method Not Supported", "POST is not supported by this URL"); - } + if (method == POST) { + badrequest(405, "Method Not Supported", "POST is not supported by this URL"); + } - keepalive = 0; - header(200, "OK"); - printf("Content-Type: text/html\r\n"); - eoh(); + keepalive = 0; + header(200, "OK"); + printf("Content-Type: text/html\r\n"); + eoh(); - printf("\r"); - html_esc(stdout, path); - printf("

Directory Listing: "); - html_esc(stdout, path); - printf("

");
-    if (path[1]) {
-        printf("Parent Directory\n");
-    }
+	printf("\r");
+	html_esc(stdout, path);
+	printf("

Directory Listing: "); + html_esc(stdout, path); + printf("

");
+	if (path[1]) {
+		printf("Parent Directory\n");
+	}
 
-    while ((de = readdir(d))) {
-        char           *name = de->d_name;
-        char            symlink[PATH_MAX];
-        struct stat     st;
+	while ((de = readdir(d))) {
+		char		   *name = de->d_name;
+		char			symlink[PATH_MAX];
+		struct stat	 st;
 
-        if (name[0] == '.') {
-            continue;   /* hidden files -> skip */
-        }
-        if (lstat(name, &st)) {
-            continue;   /* can't stat -> skip */
-        }
+		if (name[0] == '.') {
+			continue;   /* hidden files -> skip */
+		}
+		if (lstat(name, &st)) {
+			continue;   /* can't stat -> skip */
+		}
 
-        if (S_ISDIR(st.st_mode)) {
-            printf("[DIR]     ");
-        } else if (S_ISLNK(st.st_mode)) {
-            ssize_t         len = readlink(de->d_name, symlink, (sizeof symlink) - 1);
+		if (S_ISDIR(st.st_mode)) {
+			printf("[DIR]	 ");
+		} else if (S_ISLNK(st.st_mode)) {
+			ssize_t		 len = readlink(de->d_name, symlink, (sizeof symlink) - 1);
 
-            if (len < 1) {
-                continue;
-            }
-            name = symlink;
-            printf("[LNK]     ");        /* symlink */
-        } else if (S_ISREG(st.st_mode)) {
-            printf("%10llu", (unsigned long long)st.st_size);
-        } else {
-            continue;   /* not a file we can provide -> skip */
-        }
+			if (len < 1) {
+				continue;
+			}
+			name = symlink;
+			printf("[LNK]	 ");		/* symlink */
+		} else if (S_ISREG(st.st_mode)) {
+			printf("%10llu", (unsigned long long)st.st_size);
+		} else {
+			continue;   /* not a file we can provide -> skip */
+		}
 
-        /*
-         * write a href 
-         */
-        printf("  ");
-        url_esc(stdout, name);
-        printf("\n");
-    }
-    printf("
"); + /* + * write a href + */ + printf(" "); + url_esc(stdout, name); + printf("\n"); + } + printf("
"); } void find_serve_file(char *relpath) { - int fd; - struct stat st; + int fd; + struct stat st; - /* Open fspath. If that worked, */ - if ((fd = open(relpath, O_RDONLY)) > -1) { - fstat(fd, &st); - /* If it is a directory, */ - if (S_ISDIR(st.st_mode)) { - char path2[PATH_MAX]; - int fd2; + /* Open fspath. If that worked, */ + if ((fd = open(relpath, O_RDONLY)) > -1) { + fstat(fd, &st); + /* If it is a directory, */ + if (S_ISDIR(st.st_mode)) { + char path2[PATH_MAX]; + int fd2; - /* Redirect if it doesn't end with / */ - if (! endswith(path, "/")) { - header(301, "Redirect"); - printf("Location: %s/\r\n", path); - eoh(); - return; - } - - /* Open relpath + "index.html". If that worked,*/ - snprintf(path2, sizeof path2, "%sindex.html", relpath); - if ((fd2 = open(path2, O_RDONLY)) > -1) { - /* serve that file and return. */ - fstat(fd2, &st); - serve_file(fd2, path2, &st); - close(fd2); - close(fd); - return; - } else { - if (docgi) { - snprintf(path2, sizeof path2, "%sindex.cgi", relpath); - if (! stat(path2, &st)) { - return serve_cgi(path2); - } - } - if (doidx) { - serve_idx(fd, relpath + 1); - close(fd); - return; - } - return not_found(); - } - } else { - if (docgi && endswith(relpath, ".cgi")) { - close(fd); - return serve_cgi(relpath); - } - serve_file(fd, relpath, &st); - } - } else { - if (docgi && (errno == ENOTDIR)) { - char *p; + /* Redirect if it doesn't end with / */ + if (! endswith(path, "/")) { + header(301, "Redirect"); + printf("Location: %s/\r\n", path); + eoh(); + return; + } + + /* Open relpath + "index.html". If that worked,*/ + snprintf(path2, sizeof path2, "%sindex.html", relpath); + if ((fd2 = open(path2, O_RDONLY)) > -1) { + /* serve that file and return. */ + fstat(fd2, &st); + serve_file(fd2, path2, &st); + close(fd2); + close(fd); + return; + } else { + if (docgi) { + snprintf(path2, sizeof path2, "%sindex.cgi", relpath); + if (! stat(path2, &st)) { + return serve_cgi(path2); + } + } + if (doidx) { + serve_idx(fd, relpath + 1); + close(fd); + return; + } + return not_found(); + } + } else { + if (docgi && endswith(relpath, ".cgi")) { + close(fd); + return serve_cgi(relpath); + } + serve_file(fd, relpath, &st); + } + } else { + if (docgi && (errno == ENOTDIR)) { + char *p; - if ((p = strstr(relpath, ".cgi"))) { - p += 4; - env("PATH_INFO", p); - *p = 0; - if (! stat(relpath, &st)) { - close(fd); - return serve_cgi(relpath); - } - } - } - return not_found(); - } + if ((p = strstr(relpath, ".cgi"))) { + p += 4; + env("PATH_INFO", p); + *p = 0; + if (! stat(relpath, &st)) { + close(fd); + return serve_cgi(relpath); + } + } + } + return not_found(); + } } void handle_request() { - char request[MAXREQUESTLEN]; - char fspath[PATH_MAX]; - char buf[MAXHEADERLEN]; - char *p; + char request[MAXREQUESTLEN]; + char fspath[PATH_MAX]; + char buf[MAXHEADERLEN]; + char *p; - /* Initialize globals */ - host = NULL; - user_agent = NULL; - refer = NULL; - path = NULL; - range_start = 0; - range_end = 0; - content_type = NULL; - content_length = 0; - ims = 0; + /* Initialize globals */ + host = NULL; + user_agent = NULL; + refer = NULL; + path = NULL; + range_start = 0; + range_end = 0; + content_type = NULL; + content_length = 0; + ims = 0; - alarm(READTIMEOUT); + alarm(READTIMEOUT); - /* Read request line first */ - request[0] = 0; - if (NULL == fgets(request, sizeof request, stdin)) { - /* They must have hung up! */ - exit(0); - } - if (!strncmp(request, "GET /", 5)) { - method = GET; - p = request + 5; - } else if (!strncmp(request, "POST /", 6)) { - method = POST; - p = request + 6; - } else if (!strncmp(request, "HEAD /", 6)) { - method = HEAD; - p = request + 6; - } else { - /* This also handles the case where fgets does nothing */ - badrequest(405, "Method Not Allowed", "Unsupported HTTP method."); - } + /* Read request line first */ + request[0] = 0; + if (NULL == fgets(request, sizeof request, stdin)) { + /* They must have hung up! */ + exit(0); + } + if (!strncmp(request, "GET /", 5)) { + method = GET; + p = request + 5; + } else if (!strncmp(request, "POST /", 6)) { + method = POST; + p = request + 6; + } else if (!strncmp(request, "HEAD /", 6)) { + method = HEAD; + p = request + 6; + } else { + /* This also handles the case where fgets does nothing */ + badrequest(405, "Method Not Allowed", "Unsupported HTTP method."); + } - if (docgi) { - p[-2] = 0; - env("REQUEST_METHOD", request); - } + if (docgi) { + p[-2] = 0; + env("REQUEST_METHOD", request); + } - /* Interpret path into fspath. */ - path = p - 1; - { - char *fsp = fspath; - char *query_string = NULL; - - *(fsp++) = '.'; - *(fsp++) = '/'; - for (; *p != ' '; p += 1) { - if (! query_string) { - char c = *p; + /* Interpret path into fspath. */ + path = p - 1; + { + char *fsp = fspath; + char *query_string = NULL; + + *(fsp++) = '.'; + *(fsp++) = '/'; + for (; *p != ' '; p += 1) { + if (! query_string) { + char c = *p; - switch (c) { - case 0: - badrequest(413, "Request Entity Too Large", "The HTTP request was too long"); - case '\n': - badrequest(505, "Version Not Supported", "HTTP/0.9 not supported"); - case '?': - query_string = p + 1; - continue; - case '.': - if (p[-1] == '/') { - c = ':'; - } - break; - case '%': - if (p[1] && p[2]) { - int a = fromhex(p[1]); - int b = fromhex(p[2]); + switch (c) { + case 0: + badrequest(413, "Request Entity Too Large", "The HTTP request was too long"); + case '\n': + badrequest(505, "Version Not Supported", "HTTP/0.9 not supported"); + case '?': + query_string = p + 1; + continue; + case '%': + if (p[1] && p[2]) { + int a = fromhex(p[1]); + int b = fromhex(p[2]); - if ((a >= 0) && (b >= 0)) { - c = (a << 4) | b; - p += 2; - } - } - break; - } + if ((a >= 0) && (b >= 0)) { + c = (a << 4) | b; + p += 2; + } + } + break; + } - if (fsp - fspath + 1 < sizeof fspath) { - *(fsp++) = c; - } - } - } - *fsp = 0; + if (fsp - fspath + 1 < sizeof fspath) { + *(fsp++) = c; + } + } + } + *fsp = 0; - *(p++) = 0; /* NULL-terminate path */ + /* Change "/." to "/:" to keep "hidden" files such and prevent directory traversal */ + while ((fsp = strstr(fspath, "/."))) { + *(fsp+1) = ':'; + } - if (docgi && query_string) { - env("QUERY_STRING", query_string); - } - } - http_version = -1; - if (! strncmp(p, "HTTP/1.", 7) && p[8] && ((p[8] == '\r') || (p[8] == '\n'))) { - http_version = p[7] - '0'; - } - if (! ((http_version == 0) || (http_version == 1))) { - http_version = 0; - badrequest(505, "Version Not Supported", "HTTP version not supported"); - } - if (http_version == 1) { - keepalive = 1; - } else { - keepalive = 0; - } - if (docgi) { - p[8] = 0; - env("SERVER_PROTOCOL", p); - } + *(p++) = 0; /* NULL-terminate path */ - /* Read header fields */ - { - char *base = buf; - char *lastchar = base + (sizeof buf) - 2; - int nheaders = 0; + if (docgi && query_string) { + env("QUERY_STRING", query_string); + } + } - *lastchar = 0; - while (1) { - char *cgi_name = base; - char *p; - int plen = (sizeof buf) - (base - buf); - char *name, *val; - size_t len; + http_version = -1; + if (! strncmp(p, "HTTP/1.", 7) && p[8] && ((p[8] == '\r') || (p[8] == '\n'))) { + http_version = p[7] - '0'; + } + if (! ((http_version == 0) || (http_version == 1))) { + http_version = 0; + badrequest(505, "Version Not Supported", "HTTP version not supported"); + } + if (http_version == 1) { + keepalive = 1; + } else { + keepalive = 0; + } + if (docgi) { + p[8] = 0; + env("SERVER_PROTOCOL", p); + } - /* 40 is totally arbitrary here. */ - if (plen < 40) { - badrequest(431, "Request Header Too Large", "The HTTP header block was too large"); - } - if (nheaders++ >= MAXHEADERFIELDS) { - badrequest(431, "Request Header Too Large", "Too many HTTP Headers"); - } - strcpy(cgi_name, "HTTP_"); - plen -= 5; - p = cgi_name + 5; + /* Read header fields */ + { + char *base = buf; + char *lastchar = base + (sizeof buf) - 2; + int nheaders = 0; - if (NULL == fgets(p, plen, stdin)) { - badrequest(500, "OS Error", "OS error reading headers"); - } - if (*lastchar) { - badrequest(431, "Request Header Too Large", "An HTTP header field was too large"); - } + *lastchar = 0; + while (1) { + char *cgi_name = base; + char *p; + int plen = (sizeof buf) - (base - buf); + char *name, *val; + size_t len; - len = extract_header_field(p, &val, 1); - if (! len) { - /* blank line */ - break; - } - if (! val) { - badrequest(400, "Invalid header", "Unable to parse header block"); - } + /* 40 is totally arbitrary here. */ + if (plen < 40) { + badrequest(431, "Request Header Too Large", "The HTTP header block was too large"); + } + if (nheaders++ >= MAXHEADERFIELDS) { + badrequest(431, "Request Header Too Large", "Too many HTTP Headers"); + } + strcpy(cgi_name, "HTTP_"); + plen -= 5; + p = cgi_name + 5; - name = p; + if (NULL == fgets(p, plen, stdin)) { + badrequest(500, "OS Error", "OS error reading headers"); + } + if (*lastchar) { + badrequest(431, "Request Header Too Large", "An HTTP header field was too large"); + } - /* Set up CGI environment variables */ - if (docgi) { - env(cgi_name, val); - } + len = extract_header_field(p, &val, 1); + if (! len) { + /* blank line */ + break; + } + if (! val) { + badrequest(400, "Invalid header", "Unable to parse header block"); + } - /* By default, re-use buffer space */ - base = cgi_name; + name = p; - /* Handle special header fields */ - if (! strcmp(name, "HOST")) { - host = val; - base = name + len + 1; - } else if (! strcmp(name, "USER_AGENT")) { - user_agent = val; - base = name + len + 1; - } else if (! strcmp(name, "REFERER")) { - refer = val; - base = name + len + 1; - } else if (! strcmp(name, "CONTENT_TYPE")) { - content_type = val; - base = name + len + 1; - } else if (! strcmp(name, "CONTENT_LENGTH")) { - content_length = (size_t) strtoull(val, NULL, 10); - } else if (! strcmp(name, "CONNECTION")) { - if (! strcasecmp(val, "keep-alive")) { - keepalive = 1; - } else { - keepalive = 0; - } - } else if (! strcmp(name, "IF_MODIFIED_SINCE")) { - ims = timerfc(val); - } else if (! strcmp(name, "RANGE")) { - /* Range: bytes=17-23 */ - /* Range: bytes=23- */ - if (! strncmp(val, "bytes=", 6)) { - p = val + 6; - range_start = (off_t) strtoull(p, &p, 10); - if (*p == '-') { - range_end = (off_t) strtoull(p+1, NULL, 10); - } else { - range_end = 0; - } - } - } - } - } + /* Set up CGI environment variables */ + if (docgi) { + env(cgi_name, val); + } - /* Try to change into the appropriate directory */ - if (! nochdir) { - char fn[PATH_MAX]; + /* By default, re-use buffer space */ + base = cgi_name; - if (host) { - strncpy(fn, host, sizeof fn); - } else { - fn[0] = 0; - } - if (fn[0] == '.') { - fn[0] = ':'; - } - for (p = fn; *p; p += 1) { - switch (*p) { - case '/': - *p = ':'; - break; - case ':': - *p = 0; - break; - case 'A'...'Z': - *p ^= ' '; - break; - } - } + /* Handle special header fields */ + if (! strcmp(name, "HOST")) { + host = val; + base = name + len + 1; + } else if (! strcmp(name, "USER_AGENT")) { + user_agent = val; + base = name + len + 1; + } else if (! strcmp(name, "REFERER")) { + refer = val; + base = name + len + 1; + } else if (! strcmp(name, "CONTENT_TYPE")) { + content_type = val; + base = name + len + 1; + } else if (! strcmp(name, "CONTENT_LENGTH")) { + content_length = (size_t) strtoull(val, NULL, 10); + } else if (! strcmp(name, "CONNECTION")) { + if (! strcasecmp(val, "keep-alive")) { + keepalive = 1; + } else { + keepalive = 0; + } + } else if (! strcmp(name, "IF_MODIFIED_SINCE")) { + ims = timerfc(val); + } else if (! strcmp(name, "RANGE")) { + /* Range: bytes=17-23 */ + /* Range: bytes=23- */ + if (! strncmp(val, "bytes=", 6)) { + p = val + 6; + range_start = (off_t) strtoull(p, &p, 10); + if (*p == '-') { + range_end = (off_t) strtoull(p+1, NULL, 10); + } else { + range_end = 0; + } + } + } + } + } - if ((-1 == chdir(fn)) && (-1 == chdir("default"))) { - badrequest(404, "Not Found", "This host is not served here"); - } - } + /* Try to change into the appropriate directory */ + if (! nochdir) { + char fn[PATH_MAX]; - /* Serve the file */ - alarm(WRITETIMEOUT); - cork(1); - find_serve_file(fspath); - fflush(stdout); - cork(0); + if (host) { + strncpy(fn, host, sizeof fn); + } else { + fn[0] = 0; + } + if (fn[0] == '.') { + fn[0] = ':'; + } + for (p = fn; *p; p += 1) { + switch (*p) { + case '/': + *p = ':'; + break; + case ':': + *p = 0; + break; + case 'A'...'Z': + *p ^= ' '; + break; + } + } - return; + if ((-1 == chdir(fn)) && (-1 == chdir("default"))) { + badrequest(404, "Not Found", "This host is not served here"); + } + } + + /* Serve the file */ + alarm(WRITETIMEOUT); + cork(1); + find_serve_file(fspath); + fflush(stdout); + cork(0); + + return; } int main(int argc, char *argv[], const char *const *envp) { - parse_options(argc, argv); + parse_options(argc, argv); - cwd = open(".", O_RDONLY); + cwd = open(".", O_RDONLY); - setbuffer(stdout, stdout_buf, sizeof stdout_buf); + setbuffer(stdout, stdout_buf, sizeof stdout_buf); - signal(SIGPIPE, SIG_IGN); - get_ucspi_env(); + signal(SIGPIPE, SIG_IGN); + get_ucspi_env(); - while (1) { - handle_request(); - if (! keepalive) { - break; - } - fchdir(cwd); - } + while (1) { + handle_request(); + if (! keepalive) { + break; + } + fchdir(cwd); + } - return 0; + return 0; } diff --git a/test.sh b/test.sh index e7ca56d..d983799 100755 --- a/test.sh +++ b/test.sh @@ -149,6 +149,12 @@ title "Too many headers" printf 'Header: val\r\n' done printf '\r\n') | $HTTPD 2>/dev/null | grep -q 'HTTP/1.. 431 ' && pass || fail + + title "Directory traversal" + printf 'GET /../default/index.html HTTP/1.0\r\n\r\n' | $HTTPD 2>/dev/null | grep -q 'HTTP/1.. 404' && pass || fail + + title "Escaped directory traversal" + printf 'GET /%%2e%%2e/default/index.html HTTP/1.0\r\n\r\n' | $HTTPD 2>/dev/null | grep -q 'HTTP/1.. 404' && pass || fail H "If-Modified-Since"