Open kenballus opened 5 months ago
@kenballus thank you!
I can't think of any immediate security implications of this behavior. If Mongoose isn't attempting to be compliant with the standards, then I won't report issues of this nature in the future.
@kenballus thank you!
Yes we are not interested in the standard compliance just for the sake of it, which do not contribute to the usability or security. Most of your reports do contribute to those, so if you continue reporting, we'd much appreciate it.
Yes we are not interested in the standard compliance just for the sake of it, which do not contribute to the usability or security. Most of your reports do contribute to those, so if you continue reporting, we'd much appreciate it.
Got it; thanks.
A final note though:
Standards compliance is, of course, meaningless in-and-of-itself. However, in protocols like HTTP, where messages are written and rewritten as they pass through chains of gateways, the effects are parsing discrepancies become difficult to predict. Standards compliance is a good way to ensure that these discrepancies are not exploitable, because the standards authors try to ensure that compliant servers are not vulnerable to this sort of thing.
For instance, Mongoose allows ` (SP) characters within versions, but many servers don't. Similarly, many servers allow
` (SP) characters within paths, but Mongoose doesn't.
Thus, when Mongoose receives the following request line,
GET / HTTP/1.1 HTTP/1.1
... it sees the path as /
, and the version as HTTP/1.1 HTTP/1.1
.
When some other servers (Lighttpd, Libevent, FastHTTP) receive the same request line, they see the path as / HTTP/1.1
and the version as HTTP/1.1
. If a cache server interpreted the message in this second way, and forwarded the message to Mongoose, that's a vector for cache poisoning.
In short, it's difficult to predict when these discrepancies matter. The best we can do is comply with the standards, so that if these discrepancies matter in the future, we can safely say that we did the best we could.
For this reason, I've changed my mind about this one; it probably does matter.
@kenballus thank you.
Please forgive me my arrogance. I am trying to wrap my head around this, and still don't understand how gateways and Mongoose play together here. Mongoose is at the end of the request chain, correct? How the fact it accepts a bad version, is an attack vector? Could you give a example please?
I can see:
Please forgive me my arrogance.
And please forgive mine too :)
I'll try to make it more concrete. Let's say we have a setup like this:
Mongoose <-> Gateway <-> Client
Suppose that the gateway has a rule specifying that requests for /.ssh/id_rsa should be blocked.
If a client sends the following request to the caching gateway:
GET /.ssh/id_rsa whatever HTTP/1.1\r\n
Host: whatever\r\n
\r\n
The gateway sees the request as having URI=/.ssh/id_rsa whatever
, version=HTTP/1.1
and forwards the request to Mongoose. This doesn't trip the rule because, from the gateway's perspective, the request is asking for /.ssh/id_rsa whatever
, not /.ssh/id_rsa
.
Mongoose sees the request as having the URI /.ssh/id_rsa
, version=whatever HTTP/1.1
, so it responds with the content of .ssh/id_rsa
.
Hopefully it's clear why this is a problem.
According to https://datatracker.ietf.org/doc/html/rfc2616#section-5.1, the valid request is this:
Method SP Request-URI SP HTTP-Version CRLF
According to that, GET /.ssh/id_rsa whatever HTTP/1.1\r\n
request is invalid, as it has 4 items, whereas only 3 are allowed.
Am I reading that correctly? If we patch Mongoose to allow only 3 space-separated items (per standard), would it solve this issue?
If we patch Mongoose to allow only 3 space-separated items (per standard), would it solve this issue?
That is one fix for the problem described above. An easier (and more correct) solution would be to enforce that received HTTP versions always match HTTP/[0-9]\.[0-9]
.
A request is no version is also correct (https://serverfault.com/questions/1130764/is-get-a-valid-http-request) , so I guess we could check the version - it could be either empty, or match HTTP/x.y
A request with no version is correct HTTP/0.9, but not correct HTTP/1.0 or 1.1. If you want to support all three protocols, then yes, empty versions should also be permitted.
To see this for yourself,
include
include "mongoose.h"
static int const s_debug_level = MG_LL_INFO; static char const s_listening_address[] = "http://0.0.0.0:80";
// Handle interrupts, like Ctrl-C static int s_signo; static void signal_handler(int signo) { s_signo = signo; }
static size_t base64_encoded_len(size_t const plaintext_len) { // Base64-encoding takes 3n bytes to 4n bytes. // Then, there's padding, so we add on a correction term. // Note that we do not care about '\0' terminators here. return 4 * (plaintext_len / 3) + (plaintext_len % 3 != 0 ? 4 : 0); }
define INITIAL_BUF_SIZE (65535)
// buf: A pointer to a heap-allocated buffer (might be
realloc
ed) // buf_size: The size of buf (gets adjusted if buf isrealloc
ed) // buf_size_remaining: The number of free bytes at the end of buf (gets adjusted if *buf isrealloc
ed) // src: A buffer that we'll copy into buf // src_size: The number of bytes to copy from src to buf. void add_to_buffer(char *const buf_ptr, size_t const buf_size_ptr, size_t const buf_size_remaining_ptr, char const const src, size_t const src_size) { char buf = buf_ptr; size_t buf_size = buf_size_ptr; size_t buf_size_remaining = buf_size_remaining_ptr; while (buf_size_remaining < src_size) { // Is there enough room? char *const new_buf = realloc(buf, buf_size + INITIAL_BUF_SIZE); // If not, make buf INITIAL_BUF_SIZE bigger. if (new_buf == NULL) { // Did it work? puts("Out of memory!"); exit(1); // If not, fail. } buf = new_buf; // Update buf and friends. buf_size += INITIAL_BUF_SIZE; buf_size_remaining += INITIAL_BUF_SIZE; } memcpy(buf + (buf_size - buf_size_remaining), src, src_size); // Copy the data into the buffer buf_size_remaining -= src_size;}
static char B64_SCRATCH_SPACE[INITIAL_BUF_SIZE]; static char STRCAT_SCRATCH_SPACE[INITIAL_BUF_SIZE]; static char const PAYLOAD_START[] = "{\"headers\":["; static char const PAYLOAD_FIRST_HEADER_START[] = "[\""; static char const PAYLOAD_HEADER_START[] = ",[\""; static char const PAYLOAD_HEADER_MID[] = "\",\""; static char const PAYLOAD_HEADER_END[] = "\"]"; static char const PAYLOAD_BODY_BEGIN[] = "],\"body\":\""; static char const PAYLOAD_URI_BEGIN[] = "\",\"uri\":\""; static char const PAYLOAD_METHOD_BEGIN[] = "\",\"method\":\""; static char const PAYLOAD_VERSION_BEGIN[] = "\",\"version\":\""; static char const PAYLOAD_END[] = "\"}"; static void cb(struct mg_connection c, int ev, void ev_data) { if (ev != MG_EV_HTTP_MSG) { return; } struct mg_http_message parsed_request = {0}; parsed_request = (struct mg_http_message )ev_data; // mg_http_parse((char *)c->recv.buf, c->recv.len, &parsed_request);
}
int main(void) { // Initialise stuff signal(SIGINT, signal_handler); signal(SIGTERM, signal_handler); mg_log_set(s_debug_level);
}
This is unexpected, because RFC 9112 specifies that HTTP versions must match the following ABNF rule:
This is equivalent to the following regex:
HTTP/[0-9]\.[0-9]
. The ideal fix here would be to reject HTTP versions that do not match this regex.Environment