Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/cinderproxy.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@
#define QUEUE_CAPACITY 256
#define BODY_CHUNK 16384

static int headers_complete(const char *buf, size_t len) {
for (size_t i = 0; i + 3 < len; i++) {
if (memcmp(buf + i, "\r\n\r\n", 4) == 0) return 1;
}
return 0;
}

typedef struct { char ip[64]; time_t start; int count; int used; } rate_entry_t;
typedef struct { rate_entry_t e[RATE_SLOTS]; pthread_mutex_t lock; int limit; int window; } limiter_t;
typedef struct backend_health backend_health_t;
Expand Down Expand Up @@ -284,9 +291,9 @@ static void handle_client(client_t *c) {
if (n <= 0) { free(buf); goto done; }
used += (size_t)n;
buf[used] = '\0';
if (strstr(buf, "\r\n\r\n")) break;
if (headers_complete(buf, used)) break;
}
if (!strstr(buf, "\r\n\r\n")) {
if (!headers_complete(buf, used)) {
error_resp(fd, tls, 431, "Request Header Fields Too Large");
free(buf);
goto done;
Expand Down
2 changes: 2 additions & 0 deletions src/http.c
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ http_parse_result_t http_parse_request(const char *buf,size_t len,size_t max_hea
if(len>max_header_bytes) return HTTP_PARSE_TOO_LARGE;
memset(out,0,sizeof(*out)); out->content_length=0;
const char *end=buf+len,*line=find_crlf(buf,end); if(!line) return HTTP_PARSE_BAD_REQUEST;
if(memchr(buf,'\0',(size_t)(line-buf))) return HTTP_PARSE_BAD_REQUEST;
const char *sp1=memchr(buf,' ',(size_t)(line-buf)); if(!sp1||sp1==buf) return HTTP_PARSE_BAD_REQUEST;
const char *sp2=memchr(sp1+1,' ',(size_t)(line-(sp1+1))); if(!sp2) return HTTP_PARSE_BAD_REQUEST;
if(memchr(sp2+1,' ',(size_t)(line-(sp2+1)))) return HTTP_PARSE_BAD_REQUEST;
Expand All @@ -81,6 +82,7 @@ http_parse_result_t http_parse_request(const char *buf,size_t len,size_t max_hea
int host_count=0,cl_seen=0; const char *p=line+2;
while(p<end){
const char *eol=find_crlf(p,end); if(!eol) return HTTP_PARSE_BAD_REQUEST;
if(memchr(p,'\0',(size_t)(eol-p))) return HTTP_PARSE_BAD_REQUEST;
if(eol==p){ *header_end=(size_t)((eol+2)-buf); if(!strcmp(out->version,"HTTP/1.1")&&host_count!=1) return HTTP_PARSE_BAD_REQUEST; return HTTP_PARSE_OK; }
if(out->header_count>=HTTP_MAX_HEADERS) return HTTP_PARSE_TOO_LARGE;
const char *colon=memchr(p,':',(size_t)(eol-p)); if(!colon||colon==p) return HTTP_PARSE_BAD_REQUEST;
Expand Down
21 changes: 21 additions & 0 deletions tests/test_http.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@

static http_parse_result_t parse(const char *s){ http_request_t r; size_t e=0; return http_parse_request(s,strlen(s),16384,&r,&e); }
int main(void){
{
const char *requests[]={
"GET\0X / HTTP/1.1\r\nHost: localhost\r\n\r\n",
"GET /ok\0hidden HTTP/1.1\r\nHost: localhost\r\n\r\n",
"GET / HTTP/1.1\0hidden\r\nHost: localhost\r\n\r\n",
"GET / HTTP/1.1\r\nHost\0hidden: localhost\r\n\r\n",
"GET / HTTP/1.1\r\nHost: localhost\0hidden\r\n\r\n"
};
const size_t lengths[]={
sizeof("GET\0X / HTTP/1.1\r\nHost: localhost\r\n\r\n")-1,
sizeof("GET /ok\0hidden HTTP/1.1\r\nHost: localhost\r\n\r\n")-1,
sizeof("GET / HTTP/1.1\0hidden\r\nHost: localhost\r\n\r\n")-1,
sizeof("GET / HTTP/1.1\r\nHost\0hidden: localhost\r\n\r\n")-1,
sizeof("GET / HTTP/1.1\r\nHost: localhost\0hidden\r\n\r\n")-1
};
http_request_t r; size_t e=0;
for(size_t i=0;i<sizeof(requests)/sizeof(requests[0]);i++)
assert(http_parse_request(requests[i],lengths[i],16384,&r,&e)==HTTP_PARSE_BAD_REQUEST);
const char body[]="POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 3\r\n\r\na\0b";
assert(http_parse_request(body,sizeof(body)-1,16384,&r,&e)==HTTP_PARSE_OK);
}
assert(parse("GET /ok HTTP/1.1\r\nHost: localhost\r\n\r\n")==HTTP_PARSE_OK);
assert(parse("GET /search?q=../safe HTTP/1.1\r\nHost: localhost\r\n\r\n")==HTTP_PARSE_OK);
assert(parse("GET / HTTP/1.1\r\n\r\n")==HTTP_PARSE_BAD_REQUEST);
Expand Down
53 changes: 53 additions & 0 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import socket
import subprocess
import time
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROXY_PORT = 18080
Expand Down Expand Up @@ -53,6 +55,56 @@ def oversized_header_status():
return response.split(b" ", 2)[1]


def test_nul_headers_never_reach_backend():
received = []

class RecordingHandler(BaseHTTPRequestHandler):
def do_GET(self):
received.append(self.path)
self.send_response(200)
self.send_header("Content-Length", "0")
self.end_headers()

def log_message(self, *_):
pass

backend = ThreadingHTTPServer(("127.0.0.1", 0), RecordingHandler)
thread = threading.Thread(target=backend.serve_forever, daemon=True)
thread.start()
proxy = None
try:
proxy = subprocess.Popen([
"./build/cinderproxy", "--listen", str(PROXY_PORT),
"--backend-host", "127.0.0.1", "--backend-port", str(backend.server_port),
"--timeout", "2",
], cwd=ROOT)
wait_for_port(PROXY_PORT)
assert proxy.poll() is None, "test proxy did not start"
malformed = [
b"GET\x00X /nul HTTP/1.1\r\nHost: localhost\r\n\r\n",
b"GET /nul\x00hidden HTTP/1.1\r\nHost: localhost\r\n\r\n",
b"GET /nul HTTP/1.1\x00hidden\r\nHost: localhost\r\n\r\n",
b"GET /nul HTTP/1.1\r\nHost\x00hidden: localhost\r\n\r\n",
b"GET /nul HTTP/1.1\r\nHost: localhost\x00hidden\r\n\r\n",
]
for payload in malformed:
with socket.create_connection(("127.0.0.1", PROXY_PORT), timeout=3) as sock:
sock.sendall(payload)
response = http.client.HTTPResponse(sock)
response.begin()
response.read()
assert response.status == 400, (payload, response.status)
assert received == [], received
assert request("GET", "/control")[0] == 200
assert received == ["/control"], received
print("test_nul_headers_never_reach_backend: ok")
finally:
stop_process(proxy)
backend.shutdown()
backend.server_close()
thread.join(timeout=3)


def main():
backend = subprocess.Popen([
"python3",
Expand Down Expand Up @@ -103,4 +155,5 @@ def main():


if __name__ == "__main__":
test_nul_headers_never_reach_backend()
main()
Loading