diff --git a/src/httpgets.c b/src/httpgets.c index 8ce46f3..6ef4a41 100644 --- a/src/httpgets.c +++ b/src/httpgets.c @@ -27,6 +27,8 @@ int http_gets(HTTPC *httpc, UCHAR *buf, unsigned max) memset(buf, 0, max); } + int saw_cr = 0; + for(i=0; i < max; ) { /* get one character from client socket */ c = http_getc(httpc); @@ -51,7 +53,7 @@ int http_gets(HTTPC *httpc, UCHAR *buf, unsigned max) if (httpd->client & HTTPD_CLIENT_INDUMP) { wtodumpf(httpc->buf, i, "Receive Buffer"); } - + http_dbgf(fmt, httpc, httpc->socket, seconds); } httpc->state = CSTATE_CLOSE; @@ -62,21 +64,30 @@ int http_gets(HTTPC *httpc, UCHAR *buf, unsigned max) goto quit; } - /* check for ASCII CR and discard */ - if (c==0x0D) continue; + /* check for ASCII CR */ + if (c == 0x0D) { + saw_cr = 1; + continue; + } /* check for ASCII LF */ - if (c != 0x0A) { - /* not ASCII LF, translate ASCII to EBCDIC and save character */ - buf[i++] = (UCHAR)asc2ebc[c]; - continue; + if (c == 0x0A) { + /* LF terminates line (CRLF or bare LF both accepted) */ + buf[i++] = '\n'; /* EBCDIC newline */ + buf[i] = 0; /* end of string */ + rc = i; /* length of string */ + break; + } + + /* data character after CR without LF = bare CR (RFC 7230 §3.5) */ + if (saw_cr) { + errno = EINVAL; + rc = -1; + goto quit; } - /* ASCII LF */ - buf[i++] = '\n'; /* EBCDIC newline */ - buf[i] = 0; /* end of string */ - rc = i; /* length of string */ - break; + /* translate ASCII to EBCDIC and save character */ + buf[i++] = (UCHAR)asc2ebc[c]; } quit: diff --git a/src/httpin.c b/src/httpin.c index 90fdaff..b18f5ac 100644 --- a/src/httpin.c +++ b/src/httpin.c @@ -11,12 +11,24 @@ int http_in(HTTPC *httpc) UCHAR *buf = httpc->buf; unsigned len = CBUFSIZE-1; UCHAR *p; + int host_count = 0; // wtof("%s: Enter", __func__); /* read the request string "method uri version" */ rc = http_gets(httpc, buf, len); - if (rc < 0) goto failed; + if (rc < 0) { + if (errno == EINVAL) goto badrequest; + goto failed; + } + if (rc == 0) { + /* request line exceeds buffer — URI too long */ + http_resp(httpc, 414); + http_printf(httpc, "Content-Type: text/plain\r\n\r\n"); + http_printf(httpc, "414 URI Too Long\r\n"); + httpc->state = CSTATE_DONE; + goto quit; + } /* "GET path/to/resource HTTP/1.0\n" */ @@ -39,14 +51,27 @@ int http_in(HTTPC *httpc) if (http_set_env(httpc, "REQUEST_URI", p)) goto failed; p = strtok(NULL, " "); - if (!p) goto failed; + if (!p) goto badrequest; /* missing HTTP version */ if (http_set_env(httpc, "REQUEST_VERSION", p)) goto failed; + /* validate HTTP version — RFC 7230 §2.6 */ + if (http_cmp(p, "HTTP/1.0") != 0 && http_cmp(p, "HTTP/1.1") != 0) { + http_resp(httpc, 505); + http_printf(httpc, "Content-Type: text/plain\r\n\r\n"); + http_printf(httpc, "505 HTTP Version Not Supported\r\n"); + httpc->state = CSTATE_DONE; + goto quit; + } + /* create "HTTP_..." environment variables from HTTP headers */ do { /* get HTTP header string */ rc = http_gets(httpc, buf, len); - if (rc < 0) goto failed; + if (rc < 0) { + if (errno == EINVAL) goto badrequest; + goto failed; + } + if (rc == 0) goto badrequest; /* header line too long */ /* check for end of HTTP headers */ if (buf[0]=='\n') break; @@ -55,22 +80,64 @@ int http_in(HTTPC *httpc) p = strrchr(buf, '\n'); if (p) *p = 0; - // wtof("%s: \"%s\"", __func__, buf); - /* find keyword delimiter */ p = strchr(buf, ':'); if (!p) continue; /* replace delimiter with 0 byte */ *p++ = 0; - + + /* validate header name — RFC 7230 §3.2.6 token chars only + allowlist approach avoids EBCDIC code page mismatches */ + { + UCHAR *s; + for (s = buf; *s; s++) { + if (isalnum(*s)) continue; + if (*s == '!' || *s == '#' || *s == '$' || *s == '%' || + *s == '&' || *s == '\'' || *s == '*' || *s == '+' || + *s == '-' || *s == '.' || *s == '^' || *s == '_' || + *s == '`' || *s == '|' || *s == '~') + continue; + goto badrequest; + } + } + /* skip any spaces */ while(*p==' ') p++; + /* validate header value — no control chars (RFC 7230 §3.2.6) */ + { + UCHAR *s; + for (s = p; *s; s++) { + if (*s < ' ' && *s != '\t') + goto badrequest; + } + } + + /* track Host header for duplicate detection */ + if (http_cmp(buf, "Host") == 0) + host_count++; + /* create "HTTP_..." environment variable */ if (http_set_http_env(httpc, buf, p)) goto failed; } while(rc > 0); + /* reject multiple Host headers — RFC 7230 §5.4 */ + if (host_count > 1) goto badrequest; + + /* validate Content-Length — RFC 7230 §3.3.2 (digits only) */ + { + UCHAR *cl = http_get_env(httpc, "HTTP_CONTENT-LENGTH"); + if (cl) { + UCHAR *s; + if (!*cl) goto badrequest; + for (s = cl; *s; s++) { + if (*s < '0' || *s > '9') + goto badrequest; + } + } + } + /* HTTP/1.1 requires a Host header */ { UCHAR *ver = http_get_env(httpc, "REQUEST_VERSION"); @@ -106,6 +173,13 @@ int http_in(HTTPC *httpc) httpc->state = CSTATE_PARSE; goto quit; +badrequest: + http_resp(httpc, 400); + http_printf(httpc, "Content-Type: text/plain\r\n\r\n"); + http_printf(httpc, "400 Bad Request\r\n"); + httpc->state = CSTATE_DONE; + goto quit; + failed: if (httpc->request_count > 0) { /* keep-alive: idle timeout or client disconnect — just close */ diff --git a/src/httppars.c b/src/httppars.c index b1960eb..6f44bfd 100644 --- a/src/httppars.c +++ b/src/httppars.c @@ -122,8 +122,13 @@ httppars(HTTPC *httpc) goto postdata; } - /* not implemented */ - rc = http_resp_not_implemented(httpc); + /* method not allowed — RFC 7231 §6.5.5 */ + httpc->keepalive = 0; /* unread body would poison next request */ + http_resp(httpc, 405); + http_printf(httpc, "Cache-Control: no-store\r\n"); + http_printf(httpc, "Content-Type: text/plain\r\n"); + http_printf(httpc, "\r\n"); + http_printf(httpc, "405 Method Not Allowed\n"); httpc->state = CSTATE_DONE; goto quit; @@ -198,6 +203,12 @@ httppars(HTTPC *httpc) if (http_set_env(httpc, "POST_STRING", buf)) goto failed; nodata: + /* if the request had a body we didn't read, disable keep-alive + to prevent body data from poisoning the next request */ + if (http_get_env(httpc, "HTTP_CONTENT-LENGTH") || + http_get_env(httpc, "HTTP_TRANSFER-ENCODING")) + httpc->keepalive = 0; + /* no more expected data from client */ memset(httpc->buf, 0, CBUFSIZE); httpc->len = 0; diff --git a/src/httpprm.c b/src/httpprm.c index b0ad904..0fd1394 100644 --- a/src/httpprm.c +++ b/src/httpprm.c @@ -113,7 +113,7 @@ set_defaults(HTTPD *httpd) httpd->cgilua_path = NULL; httpd->cgilua_cpath = NULL; httpd->unused_80 = NULL; - httpd->docroot[0] = '\0'; + strcpy(httpd->docroot, "/www"); httpd->codepage[0] = '\0'; httpd->dbg_enabled = 0; httpd->cfg_keepalive_timeout = 5;