diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/core.c | 31 | ||||
-rw-r--r-- | server/protocol.c | 117 | ||||
-rw-r--r-- | server/util.c | 10 | ||||
-rw-r--r-- | server/vhost.c | 168 |
4 files changed, 279 insertions, 47 deletions
diff --git a/server/core.c b/server/core.c index 58b95ccf2b..c277af3bb7 100644 --- a/server/core.c +++ b/server/core.c @@ -522,6 +522,9 @@ static void *merge_core_server_configs(apr_pool_t *p, void *basev, void *virtv) if (virt->http09_enable != AP_HTTP09_UNSET) conf->http09_enable = virt->http09_enable; + if (virt->http_conformance != AP_HTTP_CONFORMANCE_UNSET) + conf->http_conformance = virt->http_conformance; + /* no action for virt->accf_map, not allowed per-vhost */ if (virt->protocol) @@ -3911,10 +3914,27 @@ static const char *set_http_protocol(cmd_parms *cmd, void *dummy, else if (strcmp(arg, "1.0") == 0) conf->http09_enable = AP_HTTP09_DISABLE; else - return "HttpProtocol min must be one of '0.9' and '1.0'"; - return NULL; + return "HttpProtocol 'min' must be one of '0.9' and '1.0'"; + return NULL; + } + + if (strcmp(arg, "strict") == 0) + conf->http_conformance = AP_HTTP_CONFORMANCE_STRICT; + else if (strcmp(arg, "strict,log-only") == 0) + conf->http_conformance = AP_HTTP_CONFORMANCE_STRICT| + AP_HTTP_CONFORMANCE_LOGONLY; + else if (strcmp(arg, "liberal") == 0) + conf->http_conformance = AP_HTTP_CONFORMANCE_LIBERAL; + else + return "HttpProtocol accepts 'min=0.9', 'min=1.0', 'liberal', " + "'strict', 'strict,log-only'"; + + if ((conf->http_conformance & AP_HTTP_CONFORMANCE_STRICT) && + (conf->http_conformance & AP_HTTP_CONFORMANCE_LIBERAL)) { + return "HttpProtocol 'strict' and 'liberal' are mutually exclusive"; } - return "HttpProtocol must be min=0.9|1.0"; + + return NULL; } static const char *set_http_method(cmd_parms *cmd, void *conf, const char *arg) @@ -4450,8 +4470,9 @@ AP_INIT_ITERATE("Protocols", set_protocols, NULL, RSRC_CONF, AP_INIT_TAKE1("ProtocolsHonorOrder", set_protocols_honor_order, NULL, RSRC_CONF, "'off' (default) or 'on' to respect given order of protocols, " "by default the client specified order determines selection"), -AP_INIT_TAKE1("HttpProtocol", set_http_protocol, NULL, RSRC_CONF, - "'min=0.9' (default) or 'min=1.0' to allow/deny HTTP/0.9"), +AP_INIT_ITERATE("HttpProtocol", set_http_protocol, NULL, RSRC_CONF, + "'min=0.9' (default) or 'min=1.0' to allow/deny HTTP/0.9; " + "'liberal', 'strict', 'strict,log-only'"), AP_INIT_ITERATE("RegisterHttpMethod", set_http_method, NULL, RSRC_CONF, "Registers non-standard HTTP methods"), { NULL } diff --git a/server/protocol.c b/server/protocol.c index 558d70f32b..a1a3845a7e 100644 --- a/server/protocol.c +++ b/server/protocol.c @@ -562,6 +562,9 @@ static int read_request_line(request_rec *r, apr_bucket_brigade *bb) char http[5]; apr_size_t len; int num_blank_lines = DEFAULT_LIMIT_BLANK_LINES; + core_server_config *conf = ap_get_core_module_config(r->server->module_config); + int strict = conf->http_conformance & AP_HTTP_CONFORMANCE_STRICT; + int enforce_strict = !(conf->http_conformance & AP_HTTP_CONFORMANCE_LOGONLY); /* Read past empty lines until we get a real request line, * a read error, the connection closes (EOF), or we timeout. @@ -648,8 +651,6 @@ static int read_request_line(request_rec *r, apr_bucket_brigade *bb) pro = ll; len = strlen(ll); } else { - core_server_config *conf; - conf = ap_get_core_module_config(r->server->module_config); r->assbackwards = 1; pro = "HTTP/0.9"; len = 8; @@ -674,12 +675,59 @@ static int read_request_line(request_rec *r, apr_bucket_brigade *bb) && apr_isdigit(pro[7])) { r->proto_num = HTTP_VERSION(pro[5] - '0', pro[7] - '0'); } - else if (3 == sscanf(r->protocol, "%4s/%u.%u", http, &major, &minor) - && (strcasecmp("http", http) == 0) - && (minor < HTTP_VERSION(1, 0)) ) /* don't allow HTTP/0.1000 */ - r->proto_num = HTTP_VERSION(major, minor); - else - r->proto_num = HTTP_VERSION(1, 0); + else { + if (strict) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02418) + "Invalid protocol '%s'", r->protocol); + if (enforce_strict) { + r->status = HTTP_BAD_REQUEST; + return 0; + } + } + if (3 == sscanf(r->protocol, "%4s/%u.%u", http, &major, &minor) + && (strcasecmp("http", http) == 0) + && (minor < HTTP_VERSION(1, 0)) ) { /* don't allow HTTP/0.1000 */ + r->proto_num = HTTP_VERSION(major, minor); + } + else { + r->proto_num = HTTP_VERSION(1, 0); + } + } + + if (strict) { + int err = 0; + if (ap_has_cntrl(r->the_request)) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02420) + "Request line must not contain control characters"); + err = HTTP_BAD_REQUEST; + } + if (r->parsed_uri.fragment) { + /* RFC3986 3.5: no fragment */ + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02421) + "URI must not contain a fragment"); + err = HTTP_BAD_REQUEST; + } + else if (r->parsed_uri.user || r->parsed_uri.password) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02422) + "URI must not contain a username/password"); + err = HTTP_BAD_REQUEST; + } + else if (r->method_number == M_INVALID) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02423) + "Invalid HTTP method string: %s", r->method); + err = HTTP_NOT_IMPLEMENTED; + } + else if (r->assbackwards == 0 && r->proto_num < HTTP_VERSION(1, 0)) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02424) + "HTTP/0.x does not take a protocol"); + err = HTTP_BAD_REQUEST; + } + + if (err && enforce_strict) { + r->status = err; + return 0; + } + } return 1; } @@ -723,6 +771,7 @@ AP_DECLARE(void) ap_get_mime_headers_core(request_rec *r, apr_bucket_brigade *bb apr_size_t len; int fields_read = 0; char *tmp_field; + core_server_config *conf = ap_get_core_module_config(r->server->module_config); /* * Read header lines until we get the empty separator line, a read error, @@ -876,6 +925,33 @@ AP_DECLARE(void) ap_get_mime_headers_core(request_rec *r, apr_bucket_brigade *bb *tmp_field-- = '\0'; } + if (conf->http_conformance & AP_HTTP_CONFORMANCE_STRICT) { + int err = 0; + + if (*last_field == '\0') { + err = HTTP_BAD_REQUEST; + ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(02425) + "Empty request header field name not allowed"); + } + else if (ap_has_cntrl(last_field)) { + err = HTTP_BAD_REQUEST; + ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(02426) + "[HTTP strict] Request header field name contains " + "control character: %.*s", + (int)LOG_NAME_MAX_LEN, last_field); + } + else if (ap_has_cntrl(value)) { + err = HTTP_BAD_REQUEST; + ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(02427) + "Request header field '%.*s' contains" + "control character", (int)LOG_NAME_MAX_LEN, + last_field); + } + if (err && !(conf->http_conformance & AP_HTTP_CONFORMANCE_LOGONLY)) { + r->status = err; + return; + } + } apr_table_addn(r->headers_in, last_field, value); /* reset the alloc_len so that we'll allocate a new @@ -925,7 +1001,7 @@ request_rec *ap_read_request(conn_rec *conn) request_rec *r; apr_pool_t *p; const char *expect; - int access_status = HTTP_OK; + int access_status; apr_bucket_brigade *tmp_bb; apr_socket_t *csd; apr_interval_time_t cur_timeout; @@ -984,9 +1060,11 @@ request_rec *ap_read_request(conn_rec *conn) /* Get the request... */ if (!read_request_line(r, tmp_bb)) { - if (r->status == HTTP_REQUEST_URI_TOO_LARGE - || r->status == HTTP_BAD_REQUEST - || r->status == HTTP_VERSION_NOT_SUPPORTED) { + switch (r->status) { + case HTTP_REQUEST_URI_TOO_LARGE: + case HTTP_BAD_REQUEST: + case HTTP_VERSION_NOT_SUPPORTED: + case HTTP_NOT_IMPLEMENTED: if (r->status == HTTP_REQUEST_URI_TOO_LARGE) { ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(00565) "request failed: client's request-line exceeds LimitRequestLine (longer than %d)", @@ -1004,19 +1082,17 @@ request_rec *ap_read_request(conn_rec *conn) r = NULL; apr_brigade_destroy(tmp_bb); goto traceout; - } - else if (r->status == HTTP_REQUEST_TIME_OUT) { + case HTTP_REQUEST_TIME_OUT: ap_update_child_status(conn->sbh, SERVER_BUSY_LOG, NULL); - if (!r->connection->keepalives) { + if (!r->connection->keepalives) ap_run_log_transaction(r); - } apr_brigade_destroy(tmp_bb); goto traceout; + default: + apr_brigade_destroy(tmp_bb); + r = NULL; + goto traceout; } - - apr_brigade_destroy(tmp_bb); - r = NULL; - goto traceout; } /* We may have been in keep_alive_timeout mode, so toggle back @@ -1101,6 +1177,7 @@ request_rec *ap_read_request(conn_rec *conn) * now read. may update status. */ ap_update_vhost_from_headers(r); + access_status = r->status; /* Toggle to the Host:-based vhost's timeout mode to fetch the * request body and send the response body, if needed. diff --git a/server/util.c b/server/util.c index 06da789718..2fe71a13c5 100644 --- a/server/util.c +++ b/server/util.c @@ -2189,6 +2189,16 @@ AP_DECLARE(void) ap_bin2hex(const void *src, apr_size_t srclen, char *dest) *dest = '\0'; } +AP_DECLARE(int) ap_has_cntrl(const char *str) +{ + while (*str) { + if (apr_iscntrl(*str)) + return 1; + str++; + } + return 0; +} + AP_DECLARE(int) ap_is_directory(apr_pool_t *p, const char *path) { apr_finfo_t finfo; diff --git a/server/vhost.c b/server/vhost.c index 90ed1398e0..227081c473 100644 --- a/server/vhost.c +++ b/server/vhost.c @@ -747,6 +747,59 @@ static apr_status_t fix_hostname_non_v6(request_rec *r, char *host) return APR_SUCCESS; } +/* + * If strict mode ever becomes the default, this should be folded into + * fix_hostname_non_v6() + */ +static apr_status_t strict_hostname_check(request_rec *r, char *host, + int logonly) +{ + char *ch; + int is_dotted_decimal = 1, leading_zeroes = 0, dots = 0; + + for (ch = host; *ch; ch++) { + if (!apr_isascii(*ch)) { + goto bad; + } + else if (apr_isalpha(*ch) || *ch == '-') { + is_dotted_decimal = 0; + } + else if (ch[0] == '.') { + dots++; + if (ch[1] == '0' && apr_isdigit(ch[2])) + leading_zeroes = 1; + } + else if (!apr_isdigit(*ch)) { + /* also takes care of multiple Host headers by denying commas */ + goto bad; + } + } + if (is_dotted_decimal) { + if (host[0] == '.' || (host[0] == '0' && apr_isdigit(host[1]))) + leading_zeroes = 1; + if (leading_zeroes || dots != 3) { + /* RFC 3986 7.4 */ + goto bad; + } + } + else { + /* The top-level domain must start with a letter (RFC 1123 2.1) */ + while (ch > host && *ch != '.') + ch--; + if (ch[0] == '.' && ch[1] != '\0' && !apr_isalpha(ch[1])) + goto bad; + } + return APR_SUCCESS; + +bad: + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02415) + "[strict] Invalid host name '%s'%s%.6s", + host, *ch ? ", problem near: " : "", ch); + if (logonly) + return APR_SUCCESS; + return APR_EINVAL; +} + /* Lowercase and remove any trailing dot and/or :port from the hostname, * and check that it is sane. * @@ -760,22 +813,23 @@ static apr_status_t fix_hostname_non_v6(request_rec *r, char *host) * Instead we just check for filesystem metacharacters: directory * separators / and \ and sequences of more than one dot. */ -static void fix_hostname(request_rec *r, const char *host_header) +static int fix_hostname(request_rec *r, const char *host_header, + unsigned http_conformance) { const char *src; char *host, *scope_id; apr_port_t port; apr_status_t rv; const char *c; + int is_v6literal = 0; + int strict = http_conformance & AP_HTTP_CONFORMANCE_STRICT; + int strict_logonly = http_conformance & AP_HTTP_CONFORMANCE_LOGONLY; src = host_header ? host_header : r->hostname; - /* According to RFC 2616, Host header field CAN be blank. - * XXX But only 'if the requested URI does not include an Internet host - * XXX name'. Can this happen? - */ + /* According to RFC 2616, Host header field CAN be blank */ if (!*src) { - return; + return is_v6literal; } /* apr_parse_addr_port will interpret a bare integer as a port @@ -784,8 +838,16 @@ static void fix_hostname(request_rec *r, const char *host_header) for (c = src; apr_isdigit(*c); ++c); if (!*c) { /* pure integer */ + if (strict) { + /* RFC 3986 7.4 */ + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02416) + "[strict] purely numeric host names not allowed: %s", + src); + if (!strict_logonly) + goto bad_nolog; + } r->hostname = src; - return; + return is_v6literal; } if (host_header) { @@ -802,9 +864,7 @@ static void fix_hostname(request_rec *r, const char *host_header) r->parsed_uri.port_str = apr_itoa(r->pool, (int)port); } if (host_header[0] == '[') - rv = fix_hostname_v6_literal(r, host); - else - rv = fix_hostname_non_v6(r, host); + is_v6literal = 1; } else { /* @@ -813,24 +873,32 @@ static void fix_hostname(request_rec *r, const char *host_header) */ host = apr_pstrdup(r->pool, r->hostname); if (ap_strchr(host, ':') != NULL) - rv = fix_hostname_v6_literal(r, host); - else - rv = fix_hostname_non_v6(r, host); + is_v6literal = 1; + } + + if (is_v6literal) { + rv = fix_hostname_v6_literal(r, host); + } + else { + rv = fix_hostname_non_v6(r, host); + if (strict && rv == APR_SUCCESS) + rv = strict_hostname_check(r, host, strict_logonly); } if (rv != APR_SUCCESS) goto bad; + r->hostname = host; - return; + return is_v6literal; bad: - r->status = HTTP_BAD_REQUEST; ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(00550) "Client sent malformed Host header: %s", - r->hostname); - return; + src); +bad_nolog: + r->status = HTTP_BAD_REQUEST; + return is_v6literal; } - /* return 1 if host matches ServerName or ServerAliases */ static int matches_aliases(server_rec *s, const char *host) { @@ -1040,23 +1108,79 @@ static void check_serverpath(request_rec *r) } } +static APR_INLINE const char *construct_host_header(request_rec *r, + int is_v6literal) +{ + struct iovec iov[5]; + apr_size_t nvec = 0; + /* + * We cannot use ap_get_server_name/port here, because we must + * ignore UseCanonicalName/Port. + */ + if (is_v6literal) { + iov[nvec].iov_base = "["; + iov[nvec].iov_len = 1; + nvec++; + } + iov[nvec].iov_base = (void *)r->hostname; + iov[nvec].iov_len = strlen(r->hostname); + nvec++; + if (is_v6literal) { + iov[nvec].iov_base = "]"; + iov[nvec].iov_len = 1; + nvec++; + } + if (r->parsed_uri.port_str) { + iov[nvec].iov_base = ":"; + iov[nvec].iov_len = 1; + nvec++; + iov[nvec].iov_base = r->parsed_uri.port_str; + iov[nvec].iov_len = strlen(r->parsed_uri.port_str); + nvec++; + } + return apr_pstrcatv(r->pool, iov, nvec, NULL); +} AP_DECLARE(void) ap_update_vhost_from_headers(request_rec *r) { - const char *host_header; + core_server_config *conf = ap_get_core_module_config(r->server->module_config); + const char *host_header = apr_table_get(r->headers_in, "Host"); + int is_v6literal, have_hostname_from_url = 0; if (r->hostname) { /* * If there was a host part in the Request-URI, ignore the 'Host' * header. */ - fix_hostname(r, NULL); + have_hostname_from_url = 1; + is_v6literal = fix_hostname(r, NULL, conf->http_conformance); } - else if ((host_header = apr_table_get(r->headers_in, "Host")) != NULL ) { - fix_hostname(r, host_header); + else if (host_header != NULL) { + is_v6literal = fix_hostname(r, host_header, conf->http_conformance); } if (r->status != HTTP_OK) return; + + if (conf->http_conformance & AP_HTTP_CONFORMANCE_STRICT) { + /* + * If we have both hostname from an absoluteURI and a Host header, + * we must ignore the Host header (RFC 2616 5.2). + * To enforce this, we reset the Host header to the value from the + * request line. + */ + if (have_hostname_from_url && host_header != NULL) { + const char *info = "Would replace"; + const char *new = construct_host_header(r, is_v6literal); + if (!(conf->http_conformance & AP_HTTP_CONFORMANCE_LOGONLY)) { + apr_table_set(r->headers_in, "Host", r->hostname); + info = "Replacing"; + } + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02417) + "%s Host header '%s' with host from request uri: " + "'%s'", info, host_header, new); + } + } + /* check if we tucked away a name_chain */ if (r->connection->vhost_lookup_data) { if (r->hostname) |