Commit message: Don't allow unhandled POSTs to write to the filesystem by default

If there's no registered handler for a POST request, the default behaviour
is to write it to the filesystem. Several million deployed devices appear
to have this behaviour, making it possible to (at least) store arbitrary
data on them. Add a configure option that enables this behaviour, and change
the default to just drop POSTs that aren't directly handled.
CVE: CVE-2016-6255
CWE: CWE-284
Vulnerability description: Portable UPnP SDK (aka libupnp) before 1.6.21 allows remote attackers to write to arbitrary files in the webroot via a POST request without a registered handler.

Project: pupnp-code
File: webserver.c

static int CheckOtherHTTPHeaders(
	/*! [in] HTTP Request message. */
	http_message_t *Req,
	/*! [out] Send Instruction object to data for the response. */
	struct SendInstruction *RespInstr,
	/*! Size of the file containing the request document. */
	off_t FileSize)
{
	http_header_t *header;
	ListNode *node;
	/*NNS: dlist_node* node; */
	int index, RetCode = HTTP_OK;
	char *TmpBuf;
	size_t TmpBufSize = LINE_SIZE;

	TmpBuf = (char *)malloc(TmpBufSize);
	if (!TmpBuf)
		return HTTP_INTERNAL_SERVER_ERROR;
	node = ListHead(&Req->headers);
	while (node != NULL) {
		header = (http_header_t *) node->item;
		/* find header type. */
		index = map_str_to_int((const char *)header->name.buf,
				header->name.length, Http_Header_Names,
				NUM_HTTP_HEADER_NAMES, FALSE);
		if (header->value.length >= TmpBufSize) {
			free(TmpBuf);
			TmpBufSize = header->value.length + 1;
			TmpBuf = (char *)malloc(TmpBufSize);
			if (!TmpBuf)
				return HTTP_INTERNAL_SERVER_ERROR;
		}
		memcpy(TmpBuf, header->value.buf, header->value.length);
		TmpBuf[header->value.length] = '\0';
		if (index >= 0) {
			switch (Http_Header_Names[index].id) {
			case HDR_TE: {
				/* Request */
				RespInstr->IsChunkActive = 1;

				if (strlen(TmpBuf) > strlen("gzip")) {
					/* means client will accept trailer. */
					if (StrStr(TmpBuf, "trailers") != NULL) {
						RespInstr->IsTrailers = 1;
					}
				}
				break;
			}
			case HDR_CONTENT_LENGTH:
				RespInstr->RecvWriteSize = atoi(TmpBuf);
				break;
			case HDR_RANGE:
				RetCode = CreateHTTPRangeResponseHeader(TmpBuf,
					FileSize, RespInstr);
				if (RetCode != HTTP_OK) {
					free(TmpBuf);
					return RetCode;
				}
				break;
			case HDR_ACCEPT_LANGUAGE:
				if (header->value.length + 1 > sizeof(RespInstr->AcceptLanguageHeader)) {
					size_t length = sizeof(RespInstr->AcceptLanguageHeader) - 1;
					memcpy(RespInstr->AcceptLanguageHeader, TmpBuf, length);
					RespInstr->AcceptLanguageHeader[length] = '\0';
				} else {
					memcpy(RespInstr->AcceptLanguageHeader, TmpBuf,
						header->value.length + 1);
				}
				break;
			default:
				/*
				   TODO
				 */
				/*
				   header.value is the value.
				 */
				/*
				   case HDR_CONTENT_TYPE: return 1;
				   case HDR_CONTENT_LANGUAGE:return 1;
				   case HDR_LOCATION: return 1;
				   case HDR_CONTENT_LOCATION:return 1;
				   case HDR_ACCEPT: return 1;
				   case HDR_ACCEPT_CHARSET: return 1;
				   case HDR_USER_AGENT: return 1;
				 */

				/*Header check for encoding */
				/*
				   case HDR_ACCEPT_RANGE:
				   case HDR_CONTENT_RANGE:
				   case HDR_IF_RANGE:
				 */

				/*Header check for encoding */
				/*
				   case HDR_ACCEPT_ENCODING:
				   if(StrStr(TmpBuf, "identity"))
				   {
				   break;
				   }
				   else return -1;
				   case HDR_CONTENT_ENCODING:
				   case HDR_TRANSFER_ENCODING:
				 */
				break;
			}
		}
		node = ListNext(&Req->headers, node);
	}
	free(TmpBuf);

	return RetCode;
}

Project: pupnp-code
File: webserver.c

static int CreateHTTPRangeResponseHeader(
	/*! String containing the range. */
	char *ByteRangeSpecifier,
	/*! Length of the file. */
	off_t FileLength,
	/*! [out] SendInstruction object where the range operations will be stored. */
	struct SendInstruction *Instr)
{
	off_t FirstByte, LastByte;
	char *RangeInput;
	char *Ptr;
	int rc = 0;

	Instr->IsRangeActive = 1;
	Instr->ReadSendSize = FileLength;
	if (!ByteRangeSpecifier)
		return HTTP_BAD_REQUEST;
	RangeInput = strdup(ByteRangeSpecifier);
	if (!RangeInput)
		return HTTP_INTERNAL_SERVER_ERROR;
	/* CONTENT-RANGE: bytes 222-3333/4000  HTTP_PARTIAL_CONTENT */
	if (StrStr(RangeInput, "bytes") == NULL ||
	    (Ptr = StrStr(RangeInput, "=")) == NULL) {
		free(RangeInput);
		Instr->IsRangeActive = 0;
		return HTTP_BAD_REQUEST;
	}
	/* Jump = */
	Ptr = Ptr + 1;
	if (FileLength < 0) {
		free(RangeInput);
		return HTTP_REQUEST_RANGE_NOT_SATISFIABLE;
	}
	if (GetNextRange(&Ptr, &FirstByte, &LastByte) != -1) {
		if (FileLength < FirstByte) {
			free(RangeInput);
			return HTTP_REQUEST_RANGE_NOT_SATISFIABLE;
		}
		if (FirstByte >= 0 && LastByte >= 0 && LastByte >= FirstByte) {
			if (LastByte >= FileLength)
				LastByte = FileLength - 1;
			Instr->RangeOffset = FirstByte;
			Instr->ReadSendSize = LastByte - FirstByte + 1;
			/* Data between two range. */
			rc = snprintf(Instr->RangeHeader,
				sizeof(Instr->RangeHeader),
				"CONTENT-RANGE: bytes %" PRId64
				"-%" PRId64 "/%" PRId64 "\r\n",
				(int64_t)FirstByte,
				(int64_t)LastByte,
				(int64_t)FileLength);
			if (rc < 0 || (unsigned int) rc >= sizeof(Instr->RangeHeader)) {
				free(RangeInput);
				return HTTP_INTERNAL_SERVER_ERROR;
			}
		} else if (FirstByte >= 0 && LastByte == -1
			   && FirstByte < FileLength) {
			Instr->RangeOffset = FirstByte;
			Instr->ReadSendSize = FileLength - FirstByte;
			rc = snprintf(Instr->RangeHeader,
				sizeof(Instr->RangeHeader),
				"CONTENT-RANGE: bytes %" PRId64
				"-%" PRId64 "/%" PRId64 "\r\n",
				(int64_t)FirstByte,
				(int64_t)(FileLength - 1),
				(int64_t)FileLength);
			if (rc < 0 || (unsigned int) rc >= sizeof(Instr->RangeHeader)) {
				free(RangeInput);
				return HTTP_INTERNAL_SERVER_ERROR;
			}
		} else if (FirstByte == -1 && LastByte > 0) {
			if (LastByte >= FileLength) {
				Instr->RangeOffset = 0;
				Instr->ReadSendSize = FileLength;
				rc = snprintf(Instr->RangeHeader,
					sizeof(Instr->RangeHeader),
					"CONTENT-RANGE: bytes 0-%" PRId64
					"/%" PRId64 "\r\n",
					(int64_t)(FileLength - 1),
					(int64_t)FileLength);
			} else {
				Instr->RangeOffset = FileLength - LastByte;
				Instr->ReadSendSize = LastByte;
				rc = snprintf(Instr->RangeHeader,
					sizeof(Instr->RangeHeader),
					"CONTENT-RANGE: bytes %" PRId64
					"-%" PRId64 "/%" PRId64 "\r\n",
					(int64_t)(FileLength - LastByte),
					(int64_t)FileLength - 1,
					(int64_t)FileLength);
			}
			if (rc < 0 || (unsigned int) rc >= sizeof(Instr->RangeHeader)) {
				free(RangeInput);
				return HTTP_INTERNAL_SERVER_ERROR;
			}
		} else {
			free(RangeInput);
			return HTTP_REQUEST_RANGE_NOT_SATISFIABLE;
		}
	} else {
		free(RangeInput);
		return HTTP_REQUEST_RANGE_NOT_SATISFIABLE;
	}

	free(RangeInput);
	return HTTP_OK;
}

Project: pupnp-code
File: webserver.c

static int GetNextRange(
	/*! string containing the token / range. */
	char **SrcRangeStr,
	/*! gets the first byte of the token. */
	off_t *FirstByte,
	/*! gets the last byte of the token. */
	off_t *LastByte)
{
	char *Ptr;
	char *Tok;
	int i;
	int64_t F = -1;
	int64_t L = -1;
	int Is_Suffix_byte_Range = 1;

	if (*SrcRangeStr == NULL)
		return -1;
	Tok = StrTok(SrcRangeStr, ",");
	if ((Ptr = strstr(Tok, "-")) == NULL)
		return -1;
	*Ptr = ' ';
	sscanf(Tok, "%" SCNd64 "%" SCNd64, &F, &L);
	if (F == -1 || L == -1) {
		*Ptr = '-';
		for (i = 0; i < (int)strlen(Tok); i++) {
			if (Tok[i] == '-') {
				break;
			} else if (isdigit(Tok[i])) {
				Is_Suffix_byte_Range = 0;
				break;
			}
		}
		if (Is_Suffix_byte_Range) {
			*FirstByte = (off_t) L;
			*LastByte = (off_t) F;
			return 1;
		}
	}
	*FirstByte = (off_t) F;
	*LastByte = (off_t) L;

	return 1;
}

Project: pupnp-code
File: webserver.c

static char *StrStr(
	/*! Input string. */
	char *s1,
	/*! Input sub-string. */
	const char *s2)
{
	char *Str1;
	char *Str2;
	const char *Ptr;
	char *ret = NULL;

	Str1 = strdup(s1);
	if (!Str1)
		goto error1;
	Str2 = strdup(s2);
	if (!Str2)
		goto error2;
	ToUpperCase(Str1);
	ToUpperCase(Str2);
	Ptr = strstr(Str1, Str2);
	if (!Ptr)
		ret = NULL;
	else
		ret = s1 + (Ptr - Str1);

	free(Str2);
error2:
	free(Str1);
error1:
	return ret;
}

Project: pupnp-code
File: webserver.c

static char *StrTok(
	/*! String containing the token. */
	char **Src,
	/*! Set of delimiter characters. */
	const char *Del)
{
	char *TmpPtr;
	char *RetPtr;

	if (*Src != NULL) {
		RetPtr = *Src;
		TmpPtr = strstr(*Src, Del);
		if (TmpPtr != NULL) {
			*TmpPtr = '\0';
			*Src = TmpPtr + strlen(Del);
		} else
			*Src = NULL;

		return RetPtr;
	}

	return NULL;
}

Project: pupnp-code
File: webserver.c

static void ToUpperCase(
	/*! Input string to be converted. */
	char *s)
{
	while (*s) {
		*s = (char)toupper(*s);
		++s;
	}
}

Project: pupnp-code
File: webserver.c

static void alias_release(
	/*! [in] XML alias object. */
	struct xml_alias_t *alias)
{
	ithread_mutex_lock(&gWebMutex);
	/* ignore invalid alias */
	if (!is_valid_alias(alias)) {
		ithread_mutex_unlock(&gWebMutex);
		return;
	}
	assert(*alias->ct > 0);
	*alias->ct -= 1;
	if (*alias->ct <= 0) {
		membuffer_destroy(&alias->doc);
		membuffer_destroy(&alias->name);
		free(alias->ct);
	}
	ithread_mutex_unlock(&gWebMutex);
}

Project: pupnp-code
File: webserver.c

static UPNP_INLINE int get_alias(
	/*! [in] request file passed in to be compared with. */
	const char *request_file,
	/*! [out] xml alias object which has a file name stored. */
	struct xml_alias_t *alias,
	/*! [out] File information object which will be filled up if the file
	 * comparison succeeds. */
	UpnpFileInfo *info)
{
	int cmp = strcmp(alias->name.buf, request_file);
	if (cmp == 0) {
		UpnpFileInfo_set_FileLength(info, (off_t)alias->doc.length);
		UpnpFileInfo_set_IsDirectory(info, FALSE);
		UpnpFileInfo_set_IsReadable(info, TRUE);
		UpnpFileInfo_set_LastModified(info, alias->last_modified);
	}

	return cmp == 0;
}

Project: pupnp-code
File: webserver.c

static UPNP_INLINE int get_content_type(
	/*! [in] . */
	const char *filename,
	/*! [out] . */
	OUT UpnpFileInfo *fileInfo)
{
	const char *extension;
	const char *type;
	const char *subtype;
	int ctype_found = FALSE;
	char *temp = NULL;
	size_t length = 0;
	int rc = 0;

	UpnpFileInfo_set_ContentType(fileInfo, NULL);
	/* get ext */
	extension = strrchr(filename, '.');
	if (extension != NULL)
		if (search_extension(extension + 1, &type, &subtype) == 0)
			ctype_found = TRUE;
	if (!ctype_found) {
		/* unknown content type */
		type = gMediaTypes[APPLICATION_INDEX];
		subtype = "octet-stream";
	}
	length = strlen(type) + strlen("/") + strlen(subtype) + 1;
	temp = malloc(length);
	if (!temp)
		return UPNP_E_OUTOF_MEMORY;
	rc = snprintf(temp, length, "%s/%s", type, subtype);
	if (rc < 0 || (unsigned int) rc >= length) {
		free(temp);
		return UPNP_E_OUTOF_MEMORY;
	}
	UpnpFileInfo_set_ContentType(fileInfo, temp);
	free(temp);
	if (!UpnpFileInfo_get_ContentType(fileInfo))
		return UPNP_E_OUTOF_MEMORY;

	return 0;
}

Project: pupnp-code
File: webserver.c

static int get_file_info(
	/*! [in] Filename having the description document. */
	const char *filename,
	/*! [out] File information object having file attributes such as filelength,
	 * when was the file last modified, whether a file or a directory and
	 * whether the file or directory is readable. */
	OUT UpnpFileInfo *info)
{
	int code;
	struct stat s;
	FILE *fp;
	int rc = 0;
	time_t aux_LastModified;
	struct tm date;
	char buffer[ASCTIME_R_BUFFER_SIZE];

	UpnpFileInfo_set_ContentType(info, NULL);
	code = stat(filename, &s);
	if (code == -1)
		return -1;
	if (S_ISDIR(s.st_mode))
		UpnpFileInfo_set_IsDirectory(info, TRUE);
	else if (S_ISREG(s.st_mode))
		UpnpFileInfo_set_IsDirectory(info, FALSE);
	else
		return -1;
	/* check readable */
	fp = fopen(filename, "r");
	UpnpFileInfo_set_IsReadable(info, fp != NULL);
	if (fp)
		fclose(fp);
	UpnpFileInfo_set_FileLength(info, s.st_size);
	UpnpFileInfo_set_LastModified(info, s.st_mtime);
	rc = get_content_type(filename, info);
	aux_LastModified = UpnpFileInfo_get_LastModified(info);
	UpnpPrintf(UPNP_INFO, HTTP, __FILE__, __LINE__,
		"file info: %s, length: %lld, last_mod=%s readable=%d\n",
		filename,
		(long long)UpnpFileInfo_get_FileLength(info),
		web_server_asctime_r(http_gmtime_r(&aux_LastModified, &date), buffer),
		UpnpFileInfo_get_IsReadable(info));

	return rc;
}

Project: pupnp-code
File: webserver.c

static UPNP_INLINE void glob_alias_init(void)
{
	struct xml_alias_t *alias = &gAliasDoc;

	membuffer_init(&alias->doc);
	membuffer_init(&alias->name);
	alias->ct = NULL;
	alias->last_modified = 0;
}

Project: pupnp-code
File: webserver.c

static int isFileInVirtualDir(
	/*! [in] Directory path to be tested for virtual directory. */
	char *filePath)
{
	virtualDirList *pCurVirtualDir;
	size_t webDirLen;

	pCurVirtualDir = pVirtualDirList;
	while (pCurVirtualDir != NULL) {
		webDirLen = strlen(pCurVirtualDir->dirName);
		if (webDirLen) {
			if (pCurVirtualDir->dirName[webDirLen - 1] == '/') {
				if (strncmp(pCurVirtualDir->dirName, filePath,
						webDirLen) == 0)
					return !0;
			} else {
				if (strncmp(pCurVirtualDir->dirName, filePath,
						webDirLen) == 0 &&
				    (filePath[webDirLen] == '/' ||
				     filePath[webDirLen] == '\0' ||
				     filePath[webDirLen] == '?'))
					return !0;
			}
		}
		pCurVirtualDir = pCurVirtualDir->next;
	}

	return 0;
}

Project: pupnp-code
File: webserver.c

static UPNP_INLINE int is_valid_alias(
	/*! [in] XML alias object. */
	const struct xml_alias_t *alias)
{
	return alias->doc.buf != NULL;
}

Project: pupnp-code
File: webserver.c

static UPNP_INLINE void media_list_init(void)
{
	int i;
	const char *s = gEncodedMediaTypes;
	struct document_type_t *doc_type;

	for (i = 0; *s != '\0'; i++) {
		doc_type = &gMediaTypeList[i];
		doc_type->file_ext = s;
		/* point to type. */
		s += strlen(s) + 1;
		doc_type->content_type = gMediaTypes[(int)*s];
		/* point to subtype. */
		s++;
		doc_type->content_subtype = s;
		/* next entry. */
		s += strlen(s) + 1;
	}
	assert(i == NUM_MEDIA_TYPES);
}

Project: pupnp-code
File: webserver.c

static char *web_server_asctime_r(const struct tm *tm, char *buf)
{
	if (tm == NULL || buf == NULL)
		return NULL;

	asctime_s(buf, ASCTIME_R_BUFFER_SIZE, tm);
	return buf;
}

Project: pupnp-code
File: webserver.c

void web_server_destroy(void)
{
	if (bWebServerState == WEB_SERVER_ENABLED) {
		membuffer_destroy(&gDocumentRootDir);
		alias_release(&gAliasDoc);

		ithread_mutex_lock(&gWebMutex);
		memset(&gAliasDoc, 0, sizeof(struct xml_alias_t));
		ithread_mutex_unlock(&gWebMutex);

		ithread_mutex_destroy(&gWebMutex);
		bWebServerState = WEB_SERVER_DISABLED;
	}
}

Project: pupnp-code
File: webserver.c

int web_server_init()
{
	int ret = 0;

	if (bWebServerState == WEB_SERVER_DISABLED) {
		/* decode media list */
		media_list_init();
		membuffer_init(&gDocumentRootDir);
		glob_alias_init();
		pVirtualDirList = NULL;

		/* Initialize callbacks */
		virtualDirCallback.get_info = NULL;
		virtualDirCallback.open = NULL;
		virtualDirCallback.read = NULL;
		virtualDirCallback.write = NULL;
		virtualDirCallback.seek = NULL;
		virtualDirCallback.close = NULL;

		if (ithread_mutex_init(&gWebMutex, NULL) == -1)
			ret = UPNP_E_OUTOF_MEMORY;
		else
			bWebServerState = WEB_SERVER_ENABLED;
	}

	return ret;
}

Project: pupnp-code
File: webserver.c

int web_server_set_alias(const char *alias_name, 
	const char *alias_content, size_t alias_content_length,
	time_t last_modified)
{
	int ret_code;
	struct xml_alias_t alias;

	alias_release(&gAliasDoc);
	if (alias_name == NULL) {
		/* don't serve aliased doc anymore */
		return 0;
	}
	assert(alias_content != NULL);
	membuffer_init(&alias.doc);
	membuffer_init(&alias.name);
	alias.ct = NULL;
	do {
		/* insert leading /, if missing */
		if (*alias_name != '/')
			if (membuffer_assign_str(&alias.name, "/") != 0)
				break;	/* error; out of mem */
		ret_code = membuffer_append_str(&alias.name, alias_name);
		if (ret_code != 0)
			break;	/* error */
		if ((alias.ct = (int *)malloc(sizeof(int))) == NULL)
			break;	/* error */
		*alias.ct = 1;
		membuffer_attach(&alias.doc, (char *)alias_content,
				 alias_content_length);
		alias.last_modified = last_modified;
		/* save in module var */
		ithread_mutex_lock(&gWebMutex);
		gAliasDoc = alias;
		ithread_mutex_unlock(&gWebMutex);

		return 0;
	} while (FALSE);
	/* error handler */
	/* free temp alias */
	membuffer_destroy(&alias.name);
	membuffer_destroy(&alias.doc);
	free(alias.ct);

	return UPNP_E_OUTOF_MEMORY;
}

Project: pupnp-code
File: webserver.c

int web_server_set_root_dir(const char *root_dir)
{
	size_t index;
	int ret;

	ret = membuffer_assign_str(&gDocumentRootDir, root_dir);
	if (ret != 0)
		return ret;
	/* remove trailing '/', if any */
	if (gDocumentRootDir.length > 0) {
		index = gDocumentRootDir.length - 1;	/* last char */
		if (gDocumentRootDir.buf[index] == '/')
			membuffer_delete(&gDocumentRootDir, index, 1);
	}

	return 0;
}