CVE-2017-5850 (published 2017-03-27T15:59Z)

httpd in OpenBSD allows remote attackers to cause a denial of service (memory consumption) via a series of requests for a large file using an HTTP Range header.

Severity: HIGH
CVSS v2 base score: 7.8 (AV:N/AC:L/Au:N/C:N/I:N/A:C)
CVSS v3 base score: 7.5, HIGH (CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: CWE-770
cwe_name: Allocation of Resources Without Limits or Throttling
description: The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.
extended_description: Code frequently has to work with limited resources, so programmers must be careful to ensure that resources are not consumed too quickly, or too easily.  Without use of quotas, resource limits, or other protection mechanisms, it can be easy for an attacker to consume many resources by rapidly making many requests, or causing larger resources to be used than is needed. When too many resources are allocated, or if a single resource is too large, then it can prevent the code from working correctly, possibly leading to a denial of service.
         

Fix information:
repo_url: https://github.com/openbsd/src
commit_details: repo_url: https://github.com/openbsd/src
author: reyk
author_date: 2017-01-31 14:39:47+00:00
msg: Reimplement httpd's support for byte ranges.

The previous implementation loaded all the output into a single output
buffer and used its size to determine the Content-Length of the body.

The new implementation calculates the body length first and writes the
individual ranges in an async way using the bufferevent mechanism.

This prevents httpd from using too much memory and applies the
watermark and throttling mechanisms to range requests.

Problem reported by Pierre Kim (pierre.kim.sec at gmail.com)

OK benno@ sunil@
repository: repo_url: https://github.com/openbsd/src
repo_name: openbsd/src
description: Read-only git conversion of OpenBSD's official CVS src repository. Pull requests not accepted - send diffs to the tech@ mailing list.
repo_language: C
owner: openbsd
file_changes: filename: httpd.h
change_type: ModificationType.MODIFY
diff: @@ -1,4 +1,4 @@
-/*	$OpenBSD: httpd.h,v 1.126 2017/01/31 12:21:27 reyk Exp $	*/
+/*	$OpenBSD: httpd.h,v 1.127 2017/01/31 14:39:47 reyk Exp $	*/
 
 /*
  * Copyright (c) 2006 - 2015 Reyk Floeter <reyk@openbsd.org>
@@ -73,6 +73,7 @@
 #define SERVER_MAX_PREFETCH	256
 #define SERVER_MIN_PREFETCHED	32
 #define SERVER_HSTS_DEFAULT_AGE	31536000
+#define SERVER_MAX_RANGES	4
 
 #define MEDIATYPE_NAMEMAX	128	/* file name extension */
 #define MEDIATYPE_TYPEMAX	64	/* length of type/subtype */
@@ -93,7 +94,8 @@ enum httpchunk {
 	TOREAD_HTTP_HEADER		= -2,
 	TOREAD_HTTP_CHUNK_LENGTH	= -3,
 	TOREAD_HTTP_CHUNK_TRAILER	= -4,
-	TOREAD_HTTP_NONE		= -5
+	TOREAD_HTTP_NONE		= -5,
+	TOREAD_HTTP_RANGE		= TOREAD_HTTP_CHUNK_LENGTH
 };
 
 #if DEBUG
@@ -295,6 +297,22 @@ struct fcgi_data {
 	int			 headersdone;
 };
 
+struct range {
+	off_t	start;
+	off_t	end;
+};
+
+struct range_data {
+	struct range		 range[SERVER_MAX_RANGES];
+	int			 range_count;
+	int			 range_index;
+	off_t			 range_toread;
+
+	/* For the Content headers in each part */
+	struct media_type	*range_media;
+	size_t			 range_total;
+};
+
 struct client {
 	uint32_t		 clt_id;
 	pid_t			 clt_pid;
@@ -313,6 +331,7 @@ struct client {
 	void			*clt_descreq;
 	void			*clt_descresp;
 	int			 clt_sndbufsiz;
+	uint64_t		 clt_boundary;
 
 	int			 clt_fd;
 	struct tls		*clt_tls_ctx;
@@ -327,6 +346,7 @@ struct client {
 	int			 clt_done;
 	int			 clt_chunk;
 	int			 clt_inflight;
+	struct range_data	 clt_ranges;
 	struct fcgi_data	 clt_fcgi;
 	char			*clt_remote_user;
 	struct evbuffer		*clt_srvevb;
@@ -601,6 +621,7 @@ const char
 	*server_httperror_byid(unsigned int);
 void	 server_read_httpcontent(struct bufferevent *, void *);
 void	 server_read_httpchunks(struct bufferevent *, void *);
+void	 server_read_httprange(struct bufferevent *, void *);
 int	 server_writeheader_http(struct client *clt, struct kv *, void *);
 int	 server_headers(struct client *, void *,
 	    int (*)(struct client *, struct kv *, void *), void *);

programming_language: C
filename: server_file.c
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,7 @@
-/*	$OpenBSD: server_file.c,v 1.63 2017/01/30 09:54:41 reyk Exp $	*/
+/*	$OpenBSD: server_file.c,v 1.64 2017/01/31 14:39:47 reyk Exp $	*/
 
 /*
- * Copyright (c) 2006 - 2015 Reyk Floeter <reyk@openbsd.org>
+ * Copyright (c) 2006 - 2017 Reyk Floeter <reyk@openbsd.org>
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
@@ -36,12 +36,6 @@
 
 #define MINIMUM(a, b)	(((a) < (b)) ? (a) : (b))
 #define MAXIMUM(a, b)	(((a) > (b)) ? (a) : (b))
-#define MAX_RANGES	4
-
-struct range {
-	off_t	start;
-	off_t	end;
-};
 
 int		 server_file_access(struct httpd *, struct client *,
 		    char *, size_t);
@@ -55,8 +49,7 @@ int		 server_file_modified_since(struct http_descriptor *,
 		    struct stat *);
 int		 server_file_method(struct client *);
 int		 parse_range_spec(char *, size_t, struct range *);
-struct range	*parse_range(char *, size_t, int *);
-int		 buffer_add_range(int, struct evbuffer *, struct range *);
+int		 parse_ranges(struct client *, char *, size_t);
 
 int
 server_file_access(struct httpd *env, struct client *clt,
@@ -303,11 +296,10 @@ server_partial_file_request(struct httpd *env, struct client *clt, char *path,
 	struct http_descriptor	*resp = clt->clt_descresp;
 	struct http_descriptor	*desc = clt->clt_descreq;
 	struct media_type	*media, multipart_media;
+	struct range_data	*r = &clt->clt_ranges;
 	struct range		*range;
-	struct evbuffer		*evb = NULL;
-	size_t			 content_length;
+	size_t			 content_length = 0;
 	int			 code = 500, fd = -1, i, nranges, ret;
-	uint32_t		 boundary;
 	char			 content_range[64];
 	const char		*errstr = NULL;
 
@@ -315,7 +307,7 @@ server_partial_file_request(struct httpd *env, struct client *clt, char *path,
 	if (desc->http_method != HTTP_METHOD_GET)
 		return server_file_request(env, clt, path, st);
 
-	if ((range = parse_range(range_str, st->st_size, &nranges)) == NULL) {
+	if ((nranges = parse_ranges(clt, range_str, st->st_size)) < 1) {
 		code = 416;
 		(void)snprintf(content_range, sizeof(content_range),
 		    "bytes */%lld", st->st_size);
@@ -328,12 +320,10 @@ server_partial_file_request(struct httpd *env, struct client *clt, char *path,
 		goto abort;
 
 	media = media_find_config(env, srv_conf, path);
-	if ((evb = evbuffer_new()) == NULL) {
-		errstr = "failed to allocate file buffer";
-		goto abort;
-	}
+	r->range_media = media;
 
 	if (nranges == 1) {
+		range = &r->range[0];
 		(void)snprintf(content_range, sizeof(content_range),
 		    "bytes %lld-%lld/%lld", range->start, range->end,
 		    st->st_size);
@@ -341,56 +331,46 @@ server_partial_file_request(struct httpd *env, struct client *clt, char *path,
 		    content_range) == NULL)
 			goto abort;
 
-		content_length = range->end - range->start + 1;
-		if (buffer_add_range(fd, evb, range) == 0)
-			goto abort;
-
+		range = &r->range[0];
+		content_length += range->end - range->start + 1;
 	} else {
-		content_length = 0;
-		boundary = arc4random();
-		/* Generate a multipart payload of byteranges */
-		while (nranges--) {
-			if ((i = evbuffer_add_printf(evb, "\r\n--%ud\r\n",
-			    boundary)) == -1)
-				goto abort;
+		/* Add boundary, all parts will be handled by the callback */
+		arc4random_buf(&clt->clt_boundary, sizeof(clt->clt_boundary));
 
-			content_length += i;
-			if ((i = evbuffer_add_printf(evb,
-			    "Content-Type: %s/%s\r\n",
-			    media->media_type, media->media_subtype)) == -1)
-				goto abort;
+		/* Calculate Content-Length of the complete multipart body */
+		for (i = 0; i < nranges; i++) {
+			range = &r->range[i];
 
-			content_length += i;
-			if ((i = evbuffer_add_printf(evb,
+			/* calculate Content-Length of the complete body */
+			if ((ret = snprintf(NULL, 0,
+			    "\r\n--%llu\r\n"
+			    "Content-Type: %s/%s\r\n"
 			    "Content-Range: bytes %lld-%lld/%lld\r\n\r\n",
-			    range->start, range->end, st->st_size)) == -1)
+			    clt->clt_boundary,
+			    media->media_type, media->media_subtype,
+			    range->start, range->end, st->st_size)) < 0)
 				goto abort;
 
-			content_length += i;
-			if (buffer_add_range(fd, evb, range) == 0)
-				goto abort;
+			/* Add data length */
+			content_length += ret + range->end - range->start + 1;
 
-			content_length += range->end - range->start + 1;
-			range++;
 		}
-
-		if ((i = evbuffer_add_printf(evb, "\r\n--%ud--\r\n",
-		    boundary)) == -1)
+		if ((ret = snprintf(NULL, 0, "\r\n--%llu--\r\n",
+		    clt->clt_boundary)) < 0)
 			goto abort;
-
-		content_length += i;
+		content_length += ret;
 
 		/* prepare multipart/byteranges media type */
 		(void)strlcpy(multipart_media.media_type, "multipart",
 		    sizeof(multipart_media.media_type));
 		(void)snprintf(multipart_media.media_subtype,
 		    sizeof(multipart_media.media_subtype),
-		    "byteranges; boundary=%ud", boundary);
+		    "byteranges; boundary=%llu", clt->clt_boundary);
 		media = &multipart_media;
 	}
 
-	close(fd);
-	fd = -1;
+	/* Start with first range */
+	r->range_toread = TOREAD_HTTP_RANGE;
 
 	ret = server_response_http(clt, 206, media, content_length,
 	    MINIMUM(time(NULL), st->st_mtim.tv_sec));
@@ -399,23 +379,34 @@ server_partial_file_request(struct httpd *env, struct client *clt, char *path,
 		goto fail;
 	case 0:
 		/* Connection is already finished */
+		close(fd);
 		goto done;
 	default:
 		break;
 	}
 
-	if (server_bufferevent_write_buffer(clt, evb) == -1)
+	clt->clt_fd = fd;
+	if (clt->clt_srvbev != NULL)
+		bufferevent_free(clt->clt_srvbev);
+
+	clt->clt_srvbev_throttled = 0;
+	clt->clt_srvbev = bufferevent_new(clt->clt_fd, server_read_httprange,
+	    server_write, server_file_error, clt);
+	if (clt->clt_srvbev == NULL) {
+		errstr = "failed to allocate file buffer event";
 		goto fail;
+	}
 
-	bufferevent_enable(clt->clt_bev, EV_READ|EV_WRITE);
-	if (clt->clt_persist)
-		clt->clt_toread = TOREAD_HTTP_HEADER;
-	else
-		clt->clt_toread = TOREAD_HTTP_NONE;
-	clt->clt_done = 0;
+	/* Adjust read watermark to the socket output buffer size */
+	bufferevent_setwatermark(clt->clt_srvbev, EV_READ, 0,
+	    clt->clt_sndbufsiz);
+
+	bufferevent_settimeout(clt->clt_srvbev,
+	    srv_conf->timeout.tv_sec, srv_conf->timeout.tv_sec);
+	bufferevent_enable(clt->clt_srvbev, EV_READ);
+	bufferevent_disable(clt->clt_bev, EV_READ);
 
  done:
-	evbuffer_free(evb);
 	server_reset_http(clt);
 	return (0);
  fail:
@@ -423,8 +414,6 @@ server_partial_file_request(struct httpd *env, struct client *clt, char *path,
 	bufferevent_free(clt->clt_bev);
 	clt->clt_bev = NULL;
  abort:
-	if (evb != NULL)
-		evbuffer_free(evb);
 	if (fd != -1)
 		close(fd);
 	if (errstr == NULL)
@@ -668,41 +657,44 @@ server_file_modified_since(struct http_descriptor *desc, struct stat *st)
 	return (-1);
 }
 
-struct range *
-parse_range(char *str, size_t file_sz, int *nranges)
+int
+parse_ranges(struct client *clt, char *str, size_t file_sz)
 {
-	static struct range	 ranges[MAX_RANGES];
 	int			 i = 0;
 	char			*p, *q;
+	struct range_data	*r = &clt->clt_ranges;
+
+	memset(r, 0, sizeof(*r));
 
 	/* Extract range unit */
 	if ((p = strchr(str, '=')) == NULL)
-		return (NULL);
+		return (-1);
 
 	*p++ = '\0';
 	/* Check if it's a bytes range spec */
 	if (strcmp(str, "bytes") != 0)
-		return (NULL);
+		return (-1);
 
 	while ((q = strchr(p, ',')) != NULL) {
 		*q++ = '\0';
 
 		/* Extract start and end positions */
-		if (parse_range_spec(p, file_sz, &ranges[i]) == 0)
+		if (parse_range_spec(p, file_sz, &r->range[i]) == 0)
 			continue;
 
 		i++;
-		if (i == MAX_RANGES)
-			return (NULL);
+		if (i == SERVER_MAX_RANGES)
+			return (-1);
 
 		p = q;
 	}
 
-	if (parse_range_spec(p, file_sz, &ranges[i]) != 0)
+	if (parse_range_spec(p, file_sz, &r->range[i]) != 0)
 		i++;
 
-	*nranges = i;
-	return (i ? ranges : NULL);
+	r->range_total = file_sz;
+	r->range_count = i;
+	return (i);
 }
 
 int
@@ -752,26 +744,3 @@ parse_range_spec(char *str, size_t size, struct range *r)
 
 	return (1);
 }
-
-int
-buffer_add_range(int fd, struct evbuffer *evb, struct range *range)
-{
-	char	buf[BUFSIZ];
-	size_t	n, range_sz;
-	ssize_t	nread;
-
-	if (lseek(fd, range->start, SEEK_SET) == -1)
-		return (0);
-
-	range_sz = range->end - range->start + 1;
-	while (range_sz) {
-		n = MINIMUM(range_sz, sizeof(buf));
-		if ((nread = read(fd, buf, n)) == -1)
-			return (0);
-
-		evbuffer_add(evb, buf, nread);
-		range_sz -= nread;
-	}
-
-	return (1);
-}

programming_language: C
filename: server_http.c
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,7 @@
-/*	$OpenBSD: server_http.c,v 1.111 2017/01/31 12:21:27 reyk Exp $	*/
+/*	$OpenBSD: server_http.c,v 1.112 2017/01/31 14:39:47 reyk Exp $	*/
 
 /*
- * Copyright (c) 2006 - 2015 Reyk Floeter <reyk@openbsd.org>
+ * Copyright (c) 2006 - 2017 Reyk Floeter <reyk@openbsd.org>
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
@@ -609,6 +609,101 @@ server_read_httpchunks(struct bufferevent *bev, void *arg)
 	server_close(clt, strerror(errno));
 }
 
+void
+server_read_httprange(struct bufferevent *bev, void *arg)
+{
+	struct client		*clt = arg;
+	struct evbuffer		*src = EVBUFFER_INPUT(bev);
+	size_t			 size;
+	struct media_type	*media;
+	struct range_data	*r = &clt->clt_ranges;
+	struct range		*range;
+
+	getmonotime(&clt->clt_tv_last);
+
+	if (r->range_toread > 0) {
+		size = EVBUFFER_LENGTH(src);
+		if (!size)
+			return;
+
+		/* Read chunk data */
+		if ((off_t)size > r->range_toread) {
+			size = r->range_toread;
+			if (server_bufferevent_write_chunk(clt, src, size)
+			    == -1)
+				goto fail;
+			r->range_toread = 0;
+		} else {
+			if (server_bufferevent_write_buffer(clt, src) == -1)
+				goto fail;
+			r->range_toread -= size;
+		}
+		if (r->range_toread < 1)
+			r->range_toread = TOREAD_HTTP_RANGE;
+		DPRINTF("%s: done, size %lu, to read %lld", __func__,
+		    size, r->range_toread);
+	}
+
+	switch (r->range_toread) {
+	case TOREAD_HTTP_RANGE:
+		if (r->range_index >= r->range_count) {
+			if (r->range_count > 1) {
+				/* Add end marker */
+				if (server_bufferevent_printf(clt,
+				    "\r\n--%llu--\r\n",
+				    clt->clt_boundary) == -1)
+					goto fail;
+			}
+			r->range_toread = TOREAD_HTTP_NONE;
+			break;
+		}
+
+		range = &r->range[r->range_index];
+
+		if (r->range_count > 1) {
+			media = r->range_media;
+			if (server_bufferevent_printf(clt,
+			    "\r\n--%llu\r\n"
+			    "Content-Type: %s/%s\r\n"
+			    "Content-Range: bytes %lld-%lld/%zu\r\n\r\n",
+			    clt->clt_boundary,
+			    media->media_type, media->media_subtype,
+			    range->start, range->end, r->range_total) == -1)
+				goto fail;
+		}
+		r->range_toread = range->end - range->start + 1;
+
+		if (lseek(clt->clt_fd, range->start, SEEK_SET) == -1)
+			goto fail;
+
+		/* Throw away bytes that are already in the input buffer */
+		evbuffer_drain(src, EVBUFFER_LENGTH(src));
+
+		/* Increment for the next part */
+		r->range_index++;
+		break;
+	case TOREAD_HTTP_NONE:
+	case 0:
+		break;
+	}
+
+	if (clt->clt_done)
+		goto done;
+
+	if (EVBUFFER_LENGTH(EVBUFFER_OUTPUT(clt->clt_bev)) > (size_t)
+	    SERVER_MAX_PREFETCH * clt->clt_sndbufsiz) {
+		bufferevent_disable(clt->clt_srvbev, EV_READ);
+		clt->clt_srvbev_throttled = 1;
+	}
+
+	return;
+ done:
+	(*bev->errorcb)(bev, EVBUFFER_READ, bev->cbarg);
+	return;
+ fail:
+	server_close(clt, strerror(errno));
+}
+
 void
 server_reset_http(struct client *clt)
 {

programming_language: C