From: Willy Tarreau Date: Tue, 20 Oct 2026 13:21:51 +0000 (+0200) Subject: MINOR: http: add one control per existing HTTP violation X-Git-Tag: v3.5-dev10~86 X-Git-Url: https://git.kaiwu.me/$%7BISSUES%7D/static/gitweb.js?a=commitdiff_plain;h=5364eccc58486afaf11c596ae68c256e8959258a;p=haproxy.git MINOR: http: add one control per existing HTTP violation Following the previous patch, this one extends every place where a check for relaxing protocol violations was permitted with a specific tunable that can individually be enabled or disabled, so that it is now possible to selectively enable a single (or a few) exceptions without granting other ones. The following ones are covered here, per direction: "fragment-in-url", HTTP_VIO_URL_FRAG, HTTP_DIR_REQ "non-ascii-in-url", HTTP_VIO_URL_NON_ASCII, HTTP_DIR_REQ "long-status-code", HTTP_VIO_STATUS_LONG, HTTP_DIR_RES "non-token-in-header-name", HTTP_VIO_HDR_NAME_NON_TOK, HTTP_DIR_BOTH "nul-in-header-value", HTTP_VIO_HDR_VALUE_NUL, HTTP_DIR_BOTH "inconsistent-transfer-encoding", HTTP_VIO_TE_BAD, HTTP_DIR_RES "unparsable-authority", HTTP_VIO_AUTH_UNPARSABLE, HTTP_DIR_REQ "invalid-connect-authority", HTTP_VIO_AUTH_CONNECT_INV, HTTP_DIR_REQ "empty-authority-host", HTTP_VIO_AUTH_EMPTY_HOST, HTTP_DIR_REQ "mismatch-authority-host", HTTP_VIO_AUTH_HOST_DIFF, HTTP_DIR_REQ "invalid-version-string", HTTP_VIO_VERSION_STRING, HTTP_DIR_BOTH "invalid-websocket-key", HTTP_VIO_WEBSOCKET_KEY, HTTP_DIR_BOTH Most of the time the check consisted in appending this after err_pos < -1: || !(global.acc_hreq_vio & (1U << HTTP_VIO_...)) For the long status code, the code was slightly reordered because the check for the lenient case was performed first and now we must do the strict check first (but the logic is unchanged, both were exclusive already). The logic remains: if strict mode (default) or relaxed without acceptance for long headers, we reject beyond 3 digits. And in any case we continue to reject values that don't fit in 16 bits. The doc was updated with descriptions of what each keyword corresponds to, associated risks, and examples. This is not much intrusive and might possibly be backported to latest LTS if users want it. --- diff --git a/doc/configuration.txt b/doc/configuration.txt index 65e77d5cb..e57ad9098 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -1793,6 +1793,8 @@ The following keywords are supported in the "global" section : - 51degrees-property-separator - 51degrees-use-performance-graph - 51degrees-use-predictive-graph + - acceptable-http-request-violations + - acceptable-http-response-violations - acme.scheduler - ca-base - chroot @@ -2141,6 +2143,166 @@ The following keywords are supported in the "global" section : Please note that this option is only available when HAProxy has been compiled with USE_51DEGREES and 51DEGREES_VER=4. +acceptable-http-request-violations { [-+][,...] } +acceptable-http-response-violations { [-+][,...] } + Finely tune which HTTP violations are acceptable when explicitly requested + + Proxy options "accept-unsafe-violations-in-http-request" and + "accept-unsafe-violations-in-http-response" allow to relax some controles in + the HTTP parsers to explicitly accept certain violations of the protocol + specification that may be needed to integrate into some environments where a + non-compliant (and possibly vulnerable) component resides, for the time it + takes to fix it. However, by default these options relax all controls, which + can become particularly dangerous when only a specific one is needed. + + These two global directives permit to refine exactly which of the violations + are permitted when the aformentioned options are used. Note that these rules + apply to all frontends and backends that use the options above, because these + options are meant to only be used under exceptional circumstances and not to + be widely deployed all over a configuration. When a frontend rejects a + request based on such a violation, a "400 Bad Request" response is sent to + the client. When a backend rejects such a response, a "502 Bad Gateway" + status is returned. + + Both directives adopt the same syntax: the argument is a comma-delimited list + of names, each designating a specific violation to act on. There are 3 ways + to use these directives: + - set the list of acceptable violations: the existing list is replaced with + the new one. This is the recommended way to use the options, guaranteeing + that the minimal set of required exceptions is allowed. + + - remove some violations from the existing list: after a '-' sign, all + designated violations will be removed from the currently permitted set. + This may be used to work around a non-recurring problem in emergency, by + accepting all violations except the most dangerous ones. + + - add some violations to the existing list: after a '+' sign, all + designated violations will be added to the currently permitted set. This + may be used when cumulating multiple statements that need to be commented + out once in a while for example. + + Special violation names "none" and "all" designate and empty and a full set, + respectively, and are usable both in the request list and in the response + list. + + The currently defined list of configurable violations is listed below, with + the direction they each apply to (request, response, both). Unless otherwise + noted, they apply to HTTP/1 only. + + - "fragment-in-url" (request) : this controls the acceptance of character + '#' in the path component of a request, which designates a fragment + identifier and is not permitted in HTTP URIs transiting over the network. + The risk is moderate (improper suffix match leading to bad routing). This + exception applies to HTTP/1, HTTP/2 and HTTP/3. + + - "non-ascii-in-url" (request): this controls the acceptance of non-ASCII + characters in the URI (characters of code 128 to 255 included). These may + stem from accentuated letters or emojis leaked directly into the request + without the mandatory URL-encoding. The risk is somewhat low. + + - "long-status-code" (response): this controls acceptance of status codes + longer than 3 characters (e.g. "0200" or "9999"). In any case, status + codes greater than 65535 will always be blocked, but below this, they are + controled by this setting. A lazy parser in a client may get confused and + even confuse "2040" with "204" for example and get desynchronized, so the + risk is moderate. Only ever enable this with known clients if absolutely + needed. + + - "non-token-in-header-name" (both): this controls the acceptance of + forbidden characters in header field names. It is often used to adapt to + internally developed non-standard applications which use forbidden + characters in header names (e.g. '/'). The risk is low on responses but + can be moderate on requests. + + - "nul-in-header-value" (both): this controls the acceptance of the NUL + character (0x00) in header field values. The risk is high in both + directions and this option must never be used outside of two fully + trusted endpoints. + + - "inconsistent-transfer-encoding" (response): transfer codings must + respect a strict ordering and always be finished by "chunked" which may + only appear once. This setting allows to relax strict controls on the + response only, offering tolerance for empty values and duplicate + "chunked", which may occasionally appear in responses from very old + poorly implemented servers or caches where compression is enabled. These + are never allowed on the request and it is never allowed to have anything + other token after "chunked" though. The risk is low for trusted servers + but moderate with untrusted ones. + + - "unparsable-authority" (request): this controls the acceptance of + characters outside the list of permitted ones in the authority component + of the URI, or uneven brackets in IPv6 addresses. The risk is high as + certain characters may be used to confuse a URI parser on a backend + server. + + - "invalid-connect-authority" (request): this controls the acceptance of a + URL that isn't strictly in the "host[:port]" format in a CONNECT request. + The risk is moderate, and this should never be needed. + + - "empty-authority-host" (request): this controls the acceptance of a + request having a scheme but no authority. The risk is moderate and this + should never be needed. + + - "mismatch-authority-host" (request): this controls the acceptance of a + request whose host and authority component are both present and differ. + This is common in environments playing URL rewriting games, and may be + used to work around failures to do it properly, but only if the request + is also fixed in the configuration via "http-request set-header host" or + "http-request set-uri". The risk is high, particularly for requests + originating from untrusted clients. + + - "invalid-version-string" (both): this controls the acceptance of an HTTP + version that is not strictly made of one digit, a dot, and another digit. + It should not be needed but might help to work around scripts emittng a + version based on an improperly rounded floating point number, such as + "HTTP/1.099999875" or "HTTP/0.9999999". This should never be needed, but + the risk is low (the string is rebuilt on output). + + - "invalid-websocket-key" (both): this controls the acceptance of WebSocket + protocol Upgrade requests lacking a valid "sec-websocket-key" header, or + upgrade responses lacking a valid "sec-websocket-accept" header. This + should only be used between trusted WebSocket endpoints, and the risk is + moderate. + + By default, for backward compatibility, all violations above are flagged as + acceptable, meaning that a proxy having "option accept-unsafe-http-*" will + accept them all. It may be a good practice to explicitly reset them to "none" + before redefining them so that commenting their definition clears all of + them. It is also important to systematically indicate in a comment why it is + being used and for how long. + + Examples: + + # Below we add one exception per bogus application + global + acceptable-http-request-violations none + acceptable-http-response-violations none + + # application "marketplace" has a problem with redirects that some + # browsers follow without encoding. Fix planned for 2026Q4, remove + # option accept-unsafe-... from their frontend. + acceptable-http-request-violations +non-ascii-in-url + + # the SMS gateway client is unable to set the port in the host header, + # this requires "option accept-unsafe-..." in their frontend + acceptable-http-request-violations +mismatch-authority-host + + # Below we allow all request exceptions except the most dangerous ones. + # Anything after a "-" is removed from the set. + global + # customer complains the site is broken, relax most controls + acceptable-http-request-violations all,-nul-in-header-value,unparsable-authority + # also try to without this one + acceptable-http-request-violations -unparsable-authority + + # Below we preset the exact list of what is acceptable in the response so + # that hosted customers can provide their own options in the backend to + # enable or disable these. + global + # acceptable violations that may be enabled per frontend/backend + acceptable-http-request-violations non-ascii-in-url + acceptable-http-response-violations inconsistent-transfer-encoding + acme.scheduler { auto | off } Enable or disable the ACME scheduler. diff --git a/include/haproxy/http-t.h b/include/haproxy/http-t.h index 4f392d72f..385088099 100644 --- a/include/haproxy/http-t.h +++ b/include/haproxy/http-t.h @@ -208,6 +208,18 @@ enum http_dir { /* known configurable HTTP violations */ enum http_violation_idx { + HTTP_VIO_URL_FRAG = 0, // fragment in url ('#') + HTTP_VIO_URL_NON_ASCII, // non-ASCII (>=128) in URL + HTTP_VIO_STATUS_LONG, // status longer than 3 characters + HTTP_VIO_HDR_NAME_NON_TOK, // non-token in header name + HTTP_VIO_HDR_VALUE_NUL, // NUL byte in header value + HTTP_VIO_TE_BAD, // inconsistent transfer encoding + HTTP_VIO_AUTH_UNPARSABLE, // unparsable authority + HTTP_VIO_AUTH_CONNECT_INV, // invalid authority with CONNECT + HTTP_VIO_AUTH_EMPTY_HOST, // empty host component in authority + HTTP_VIO_AUTH_HOST_DIFF, // authority and host field mismatch + HTTP_VIO_VERSION_STRING, // unsupported version string + HTTP_VIO_WEBSOCKET_KEY, // invalid or missing websocket-key header /* insert new violations above this line */ HTTP_VIO_TOTAL_COUNT = 32, // end marker HTTP_VIO_NONE, // special code for none diff --git a/src/h1.c b/src/h1.c index 39a298790..8608c99c1 100644 --- a/src/h1.c +++ b/src/h1.c @@ -562,7 +562,8 @@ int h1_headers_to_hdr_list(char *start, const char *stop, http_msg_rquri2: if (likely((unsigned char)(*ptr - 33) <= 93)) { /* 33 to 126 included */ if (*ptr == '#') { - if (h1m->err_pos < -1) /* PR_O2_REQBUG_OK not set */ + if (h1m->err_pos < -1 || /* PR_O2_REQBUG_OK not set */ + !(global.acc_hreq_vio & (1U << HTTP_VIO_URL_FRAG))) /* !fragment-in-url */ goto invalid_char; if (h1m->err_pos == -1) /* PR_O2_REQBUG_OK set: just log */ h1m->err_pos = ptr - start + skip; @@ -579,7 +580,7 @@ int h1_headers_to_hdr_list(char *start, const char *stop, * accept-unsafe-violations-in-http-request is enabled in the frontend. * In any case, we capture the faulty char. */ - if (h1m->err_pos < -1) + if (h1m->err_pos < -1 || !(global.acc_hreq_vio & (1U << HTTP_VIO_URL_NON_ASCII))) goto invalid_char; if (h1m->err_pos == -1) h1m->err_pos = ptr - start + skip; @@ -743,12 +744,14 @@ int h1_headers_to_hdr_list(char *start, const char *stop, if (likely(HTTP_IS_DIGIT(*ptr))) { if (!restarting && ptr - sl.st.c.ptr >= 3) { /* more than 3 digits (checked again after a restart) */ - if (h1m->err_pos == -1) /* only capture the error pointer */ - h1m->err_pos = ptr - start + skip; - else if (h1m->err_pos < -1 || sl.st.status >= ((uint16_t)~0 - 9) / 10) { + if (sl.st.status >= ((uint16_t)~0 - 9) / 10 || h1m->err_pos < -1 || + !(global.acc_hres_vio & (1U << HTTP_VIO_STATUS_LONG))) { /* strict checks or risk of overflow */ state = H1_MSG_RPCODE; goto http_msg_invalid; + } else if (h1m->err_pos == -1) { + /* only capture the error pointer */ + h1m->err_pos = ptr - start + skip; } } sl.st.status = sl.st.status * 10 + *ptr - '0'; @@ -849,7 +852,9 @@ int h1_headers_to_hdr_list(char *start, const char *stop, EAT_AND_JUMP_OR_RETURN(ptr, end, http_msg_hdr_l1_sp, http_msg_ood, state, H1_MSG_HDR_L1_SP); } - if (likely(h1m->err_pos < -1) || *ptr == '\n') { + if (likely(h1m->err_pos < -1) || *ptr == '\n' || + (!(h1m->flags & H1_MF_RESP) && !(global.acc_hreq_vio & (1U << HTTP_VIO_HDR_NAME_NON_TOK))) || + ((h1m->flags & H1_MF_RESP) && !(global.acc_hres_vio & (1U << HTTP_VIO_HDR_NAME_NON_TOK)))) { state = H1_MSG_HDR_NAME; goto http_msg_invalid; } @@ -930,7 +935,9 @@ int h1_headers_to_hdr_list(char *start, const char *stop, /* RFC9110 clarified that NUL is explicitly forbidden in header values * (like CR and LF). */ - if (h1m->err_pos < -1) { /* PR_O2_REQBUG_OK not set */ + if (h1m->err_pos < -1 || /* PR_O2_R*BUG_OK not set */ + (!(h1m->flags & H1_MF_RESP) && !(global.acc_hreq_vio & HTTP_VIO_HDR_VALUE_NUL)) || + ((h1m->flags & H1_MF_RESP) && !(global.acc_hres_vio & HTTP_VIO_HDR_VALUE_NUL))) { state = H1_MSG_HDR_VAL; goto http_msg_invalid; } @@ -1010,9 +1017,11 @@ int h1_headers_to_hdr_list(char *start, const char *stop, if (isteqi(n, ist("transfer-encoding"))) { ret = h1_parse_xfer_enc_header(h1m, v); if (ret < 0) { - /* For the response only, don't report error if PR_O2_RSPBUG_OK is set - * and the error can be hidden */ - if (ret == -2 || !(h1m->flags & H1_MF_RESP) || (h1m->err_pos < -1)) { + /* Unparsable transfer-encoding. For the response only, don't report + * error if PR_O2_RSPBUG_OK is set and the error can be hidden. + */ + if (ret == -2 || !(h1m->flags & H1_MF_RESP) || (h1m->err_pos < -1) || + !(global.acc_hres_vio & (1U << HTTP_VIO_TE_BAD))) { state = H1_MSG_HDR_L2_LWS; ptr = v.ptr; /* Set ptr on the error */ goto http_msg_invalid; @@ -1147,7 +1156,7 @@ int h1_headers_to_hdr_list(char *start, const char *stop, authority = http_parse_authority(&parser, 1); bad_authority = http_authority_has_forbidden_char(authority); if (bad_authority) { - if (h1m->err_pos < -1) { + if (h1m->err_pos < -1 || !(global.acc_hreq_vio & (1U << HTTP_VIO_AUTH_UNPARSABLE))) { state = H1_MSG_LAST_LF; /* WT: gcc seems to see a path where sl.rq.u.ptr was used * uninitialized, but it doesn't know that the function is @@ -1183,7 +1192,7 @@ int h1_headers_to_hdr_list(char *start, const char *stop, ret = -1; if (ret < 0) { - if (h1m->err_pos < -1) { + if (h1m->err_pos < -1 || !(global.acc_hreq_vio & (1U << HTTP_VIO_AUTH_CONNECT_INV))) { state = H1_MSG_LAST_LF; /* WT: gcc seems to see a path where sl.rq.u.ptr was used * uninitialized, but it doesn't know that the function is @@ -1216,7 +1225,7 @@ int h1_headers_to_hdr_list(char *start, const char *stop, uri_host.len -= istlen(uri_port) + 1; if (!istlen(uri_host) && (isteqi(scheme, ist("http://")) || isteqi(scheme, ist("https://")))) { - if (h1m->err_pos < -1) { + if (h1m->err_pos < -1 || !(global.acc_hreq_vio & (1U << HTTP_VIO_AUTH_EMPTY_HOST))) { state = H1_MSG_LAST_LF; ptr = sl.rq.u.ptr; /* Set ptr on the error */ goto http_msg_invalid; @@ -1231,7 +1240,7 @@ int h1_headers_to_hdr_list(char *start, const char *stop, if (isttest(host) && !isteqi(authority, host)) { ret = h1_validate_mismatch_authority(scheme, authority, host); if (ret < 0) { - if (h1m->err_pos < -1) { + if (h1m->err_pos < -1 || !(global.acc_hreq_vio & (1U << HTTP_VIO_AUTH_HOST_DIFF))) { state = H1_MSG_LAST_LF; ptr = host.ptr; /* Set ptr on the error */ goto http_msg_invalid; diff --git a/src/h1_htx.c b/src/h1_htx.c index 590a74bbd..11ad7b2e6 100644 --- a/src/h1_htx.c +++ b/src/h1_htx.c @@ -54,7 +54,7 @@ static int h1_process_req_vsn(struct h1m *h1m, union h1_sl *sl) * exactly one digit "." one digit. This check may be disabled using * option accept-unsafe-violations-in-http-request. */ - if (h1m->err_pos == -2) { /* PR_O2_REQBUG_OK not set */ + if (h1m->err_pos < -1 || !(global.acc_hreq_vio & (1U << HTTP_VIO_VERSION_STRING))) { /* PR_O2_REQBUG_OK not set */ if (sl->rq.v.len != 8) return 0; @@ -106,7 +106,7 @@ static int h1_process_res_vsn(struct h1m *h1m, union h1_sl *sl) * exactly one digit "." one digit. This check may be disabled using * option accept-unsafe-violations-in-http-response. */ - if (h1m->err_pos == -2) { /* PR_O2_RSPBUG_OK not set */ + if (h1m->err_pos < -1 || !(global.acc_hreq_vio & (1U << HTTP_VIO_VERSION_STRING))) { /* PR_O2_RSPBUG_OK not set */ if (sl->st.v.len != 8) return 0; diff --git a/src/h2.c b/src/h2.c index 1d21f5e95..2456998e8 100644 --- a/src/h2.c +++ b/src/h2.c @@ -361,7 +361,8 @@ int h2_make_htx_request(struct http_hdr *list, struct htx *htx, unsigned int *ms * rejecting NUL, CR and LF characters. For :path we reject all CTL * chars, spaces, and '#'. */ - if (phdr == H2_PHDR_IDX_PATH && !relaxed) { + if (phdr == H2_PHDR_IDX_PATH && + !(relaxed && (global.acc_hreq_vio & (1U << HTTP_VIO_URL_FRAG)))) { /* !fragment-in-url */ ctl = ist_find_range(list[idx].v, 0, '#'); if (unlikely(ctl) && http_path_has_forbidden_char(list[idx].v, ctl)) goto fail; diff --git a/src/h3.c b/src/h3.c index a64ad4bd4..ccbb28bbf 100644 --- a/src/h3.c +++ b/src/h3.c @@ -847,7 +847,7 @@ static ssize_t h3_req_headers_to_htx(struct qcs *qcs, const struct buffer *buf, goto out; } - if (!relaxed) { + if (!(relaxed && (global.acc_hreq_vio & (1U << HTTP_VIO_URL_FRAG)))) { /* !fragment-in-url */ /* we need to reject any control chars or '#' from the path, * unless option accept-unsafe-violations-in-http-request is set. */ diff --git a/src/http.c b/src/http.c index d67e4015f..3c99bd354 100644 --- a/src/http.c +++ b/src/http.c @@ -1631,6 +1631,18 @@ struct ist http_trim_trailing_spht(struct ist value) const struct http_violation http_violations[] = { { "none", HTTP_VIO_NONE, HTTP_DIR_BOTH }, /* insert new violations below this line */ + { "fragment-in-url", HTTP_VIO_URL_FRAG, HTTP_DIR_REQ }, + { "non-ascii-in-url", HTTP_VIO_URL_NON_ASCII, HTTP_DIR_REQ }, + { "long-status-code", HTTP_VIO_STATUS_LONG, HTTP_DIR_RES }, + { "non-token-in-header-name", HTTP_VIO_HDR_NAME_NON_TOK, HTTP_DIR_BOTH }, + { "nul-in-header-value", HTTP_VIO_HDR_VALUE_NUL, HTTP_DIR_BOTH }, + { "inconsistent-transfer-encoding", HTTP_VIO_TE_BAD, HTTP_DIR_RES }, + { "unparsable-authority", HTTP_VIO_AUTH_UNPARSABLE, HTTP_DIR_REQ }, + { "invalid-connect-authority", HTTP_VIO_AUTH_CONNECT_INV, HTTP_DIR_REQ }, + { "empty-authority-host", HTTP_VIO_AUTH_EMPTY_HOST, HTTP_DIR_REQ }, + { "mismatch-authority-host", HTTP_VIO_AUTH_HOST_DIFF, HTTP_DIR_REQ }, + { "invalid-version-string", HTTP_VIO_VERSION_STRING, HTTP_DIR_BOTH }, + { "invalid-websocket-key", HTTP_VIO_WEBSOCKET_KEY, HTTP_DIR_BOTH }, /* insert new violations above this line */ { "all", HTTP_VIO_ALL, HTTP_DIR_BOTH }, { NULL, 0 } diff --git a/src/mux_h1.c b/src/mux_h1.c index 108ba8bcc..d9c68514b 100644 --- a/src/mux_h1.c +++ b/src/mux_h1.c @@ -2086,8 +2086,12 @@ static size_t h1_handle_headers(struct h1s *h1s, struct h1m *h1m, struct htx *ht if (!ws_ret) { h1_capture_bad_message(h1s->h1c, h1s, h1m, buf); - if ((!(h1m->flags & H1_MF_RESP) && !(h1s->h1c->px->options2 & PR_O2_REQBUG_OK)) || - ((h1m->flags & H1_MF_RESP) && !(h1s->h1c->px->options2 & PR_O2_RSPBUG_OK))) { + if ((!(h1m->flags & H1_MF_RESP) && + (!(h1s->h1c->px->options2 & PR_O2_REQBUG_OK) || + !(global.acc_hreq_vio & (1U << HTTP_VIO_WEBSOCKET_KEY)))) || + ((h1m->flags & H1_MF_RESP) && + (!(h1s->h1c->px->options2 & PR_O2_RSPBUG_OK) || + !(global.acc_hres_vio & (1U << HTTP_VIO_WEBSOCKET_KEY))))) { htx->flags |= HTX_FL_PARSING_ERROR; h1s->flags |= H1S_F_PARSING_ERROR; TRACE_ERROR("missing/invalid websocket key, reject H1 message",