CVE-2017-5850 (C, published 2017-03-27T15:59Z)
Description: value: 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 v3 base score: 7.5)
Weakness: CWE-770 \u2014 Allocation of Resources Without Limits or Throttling
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.
Repository: https://github.com/openbsd/src
Files changed:
usr.sbin/httpd/httpd.h
usr.sbin/httpd/server_file.c
usr.sbin/httpd/server_http.c
Commit message: 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@


Vulnerable code:
/*	$OpenBSD: httpd.h,v 1.126 2017/01/31 12:21:27 reyk Exp $	*/
	TOREAD_HTTP_NONE		= -5
/*	$OpenBSD: server_file.c,v 1.63 2017/01/30 09:54:41 reyk Exp $	*/
 * Copyright (c) 2006 - 2015 Reyk Floeter <reyk@openbsd.org>
#define MAX_RANGES	4

struct range {
	off_t	start;
	off_t	end;
};
struct range	*parse_range(char *, size_t, int *);
int		 buffer_add_range(int, struct evbuffer *, struct range *);
	struct evbuffer		*evb = NULL;
	size_t			 content_length;
	uint32_t		 boundary;
	if ((range = parse_range(range_str, st->st_size, &nranges)) == NULL) {
	if ((evb = evbuffer_new()) == NULL) {
		errstr = "failed to allocate file buffer";
		goto abort;
	}
		content_length = range->end - range->start + 1;
		if (buffer_add_range(fd, evb, range) == 0)
			goto abort;

		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;
			content_length += i;
			if ((i = evbuffer_add_printf(evb,
			    "Content-Type: %s/%s\r\n",
			    media->media_type, media->media_subtype)) == -1)
				goto abort;
			content_length += i;
			if ((i = evbuffer_add_printf(evb,
			    range->start, range->end, st->st_size)) == -1)
			content_length += i;
			if (buffer_add_range(fd, evb, range) == 0)
				goto abort;
			content_length += range->end - range->start + 1;
			range++;

		if ((i = evbuffer_add_printf(evb, "\r\n--%ud--\r\n",
		    boundary)) == -1)

		content_length += i;
		    "byteranges; boundary=%ud", boundary);
	close(fd);
	fd = -1;
	if (server_bufferevent_write_buffer(clt, evb) == -1)
	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;
	evbuffer_free(evb);
	if (evb != NULL)
		evbuffer_free(evb);
struct range *
parse_range(char *str, size_t file_sz, int *nranges)
	static struct range	 ranges[MAX_RANGES];
		return (NULL);
		return (NULL);
		if (parse_range_spec(p, file_sz, &ranges[i]) == 0)
		if (i == MAX_RANGES)
			return (NULL);
	if (parse_range_spec(p, file_sz, &ranges[i]) != 0)
	*nranges = i;
	return (i ? ranges : NULL);

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);
}
/*	$OpenBSD: server_http.c,v 1.111 2017/01/31 12:21:27 reyk Exp $	*/
 * Copyright (c) 2006 - 2015 Reyk Floeter <reyk@openbsd.org>

Fixed code:
/*	$OpenBSD: httpd.h,v 1.127 2017/01/31 14:39:47 reyk Exp $	*/
#define SERVER_MAX_RANGES	4
	TOREAD_HTTP_NONE		= -5,
	TOREAD_HTTP_RANGE		= TOREAD_HTTP_CHUNK_LENGTH
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;
};

	uint64_t		 clt_boundary;
	struct range_data	 clt_ranges;
void	 server_read_httprange(struct bufferevent *, void *);
/*	$OpenBSD: server_file.c,v 1.64 2017/01/31 14:39:47 reyk Exp $	*/
 * Copyright (c) 2006 - 2017 Reyk Floeter <reyk@openbsd.org>
int		 parse_ranges(struct client *, char *, size_t);
	struct range_data	*r = &clt->clt_ranges;
	size_t			 content_length = 0;
	if ((nranges = parse_ranges(clt, range_str, st->st_size)) < 1) {
	r->range_media = media;
		range = &r->range[0];
		range = &r->range[0];
		content_length += range->end - range->start + 1;
		/* Add boundary, all parts will be handled by the callback */
		arc4random_buf(&clt->clt_boundary, sizeof(clt->clt_boundary));
		/* Calculate Content-Length of the complete multipart body */
		for (i = 0; i < nranges; i++) {
			range = &r->range[i];
			/* calculate Content-Length of the complete body */
			if ((ret = snprintf(NULL, 0,
			    "\r\n--%llu\r\n"
			    "Content-Type: %s/%s\r\n"
			    clt->clt_boundary,
			    media->media_type, media->media_subtype,
			    range->start, range->end, st->st_size)) < 0)
			/* Add data length */
			content_length += ret + range->end - range->start + 1;
		if ((ret = snprintf(NULL, 0, "\r\n--%llu--\r\n",
		    clt->clt_boundary)) < 0)
		content_length += ret;
		    "byteranges; boundary=%llu", clt->clt_boundary);
	/* Start with first range */
	r->range_toread = TOREAD_HTTP_RANGE;
		close(fd);
	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";
	}
	/* 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);
int
parse_ranges(struct client *clt, char *str, size_t file_sz)
	struct range_data	*r = &clt->clt_ranges;

	memset(r, 0, sizeof(*r));
		return (-1);
		return (-1);
		if (parse_range_spec(p, file_sz, &r->range[i]) == 0)
		if (i == SERVER_MAX_RANGES)
			return (-1);
	if (parse_range_spec(p, file_sz, &r->range[i]) != 0)
	r->range_total = file_sz;
	r->range_count = i;
	return (i);
/*	$OpenBSD: server_http.c,v 1.112 2017/01/31 14:39:47 reyk Exp $	*/
 * Copyright (c) 2006 - 2017 Reyk Floeter <reyk@openbsd.org>
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));
}