An absolute-form request target with the http or https scheme and an empty
host was accepted and forwarded verbatim to H1 servers, for instance:
GET http:///admin.internal/x HTTP/1.1\r\n\r\n
GET http://:8080/x HTTP/1.1\r\n\r\n
GET http://@/x HTTP/1.1\r\n\r\n
RFC9110#4.2.1 says that a recipient must reject such URIs as invalid. Here
the rules see an empty host (or ":8080") and the path "/admin.internal/x",
while servers relying on WHATWG-style URL parsers (e.g. NodeJS) take
"admin.internal" as the host and "/x" as the path. It's a parser
differential that may be used to bypass host or path based rules, depending
on the server.
Let's reject these requests, unless unsafe violations are accepted (then the
error is only captured). A reg-test was added for this.
Note that the "strange cases" C32 to C35 of the h1_host_normalization
reg-test explicitly verified that "GET http://:" with "host: :80" (or
"https://:" and "host: :443" / "host: :") were accepted and normalized to
"http:///" with an empty host. These cases are now rejected with a 400, so
the test was updated accordingly. Developers may want to double-check that
there was no other reason to accept them.
The validation of the request target was introduced by commit
25bcdb1d9
("BUG/MAJOR: h1: Be stricter on request target validation during message
parsing") in 3.0-dev12, which didn't cover this case. This should be
backported as far as 3.0.
requests;
* In H1 only, no check will be performed against the authority and the Host
- header value.
+ header value;
+
+ * In H1 only, when a request has no Host header, a Host header is built
+ from the authority of its target (the request-target itself for CONNECT
+ requests). But it is not added when the authority contains characters
+ forbidden in the Host header or, for CONNECT requests, when the
+ authority is invalid.
* In H1 only, tests on the HTTP version will be relaxed. It will allow
HTTP/0.9 GET requests to pass through (no version specified), as well as
--- /dev/null
+varnishtest "H1 absolute-form http(s) URIs with an empty host"
+
+# http and https URIs with an empty host are invalid (RFC9110#4.2.1) and
+# must be rejected, otherwise rules see no host while some servers would
+# find one in the forwarded URI.
+
+feature ignore_unknown_macro
+
+haproxy h1 -conf {
+ defaults
+ mode http
+ timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
+ timeout client "${HAPROXY_TEST_TIMEOUT-5s}"
+ timeout server "${HAPROXY_TEST_TIMEOUT-5s}"
+
+ frontend fe1
+ bind "fd@${fe1}"
+ http-request return status 200
+} -start
+
+client c1 -connect ${h1_fe1_sock} {
+ txreq -url "http:///admin.internal/x" -nohost
+ rxresp
+ expect resp.status == 400
+} -run
+
+client c2 -connect ${h1_fe1_sock} {
+ txreq -url "http://:8080/x" -nohost
+ rxresp
+ expect resp.status == 400
+} -run
+
+client c3 -connect ${h1_fe1_sock} {
+ txreq -url "https://@/x" -nohost
+ rxresp
+ expect resp.status == 400
+} -run
+
+client c4 -connect ${h1_fe1_sock} {
+ txreq -url "http://www.example.com:8080/x" -nohost
+ rxresp
+ expect resp.status == 200
+} -run
# C32
recv
- expect ~ "^.* uri: GET http:/// HTTP/1.1; host: {}$"
+ expect ~ "^.* uri: <BADREQ>; host: $"
barrier b1 sync
# C33
recv
- expect ~ "^.* uri: GET https:/// HTTP/1.1; host: {}$"
+ expect ~ "^.* uri: <BADREQ>; host: $"
barrier b1 sync
# C34
recv
- expect ~ "^.* uri: GET http:/// HTTP/1.1; host: {}$"
+ expect ~ "^.* uri: <BADREQ>; host: $"
barrier b1 sync
# C35
recv
- expect ~ "^.* uri: GET https:/// HTTP/1.1; host: {}$"
+ expect ~ "^.* uri: <BADREQ>; host: $"
barrier b1 sync
# C36
-hdr "host: :80"
rxresp
- expect resp.status == 200
+ expect resp.status == 400
} -run
# Wait matching on log message
-hdr "host: :443"
rxresp
- expect resp.status == 200
+ expect resp.status == 400
} -run
# Wait matching on log message
-hdr "host: :"
rxresp
- expect resp.status == 200
+ expect resp.status == 400
} -run
# Wait matching on log message
-hdr "host: :"
rxresp
- expect resp.status == 200
+ expect resp.status == 400
} -run
# Wait matching on log message
}
else { /* Scheme found: MUST be an absolute-URI */
struct ist host = IST_NULL;
+ struct ist uri_host = authority;
+ struct ist uri_port = http_get_host_port(authority);
if (sl.rq.meth == HTTP_METH_CONNECT) {
ptr = sl.rq.u.ptr; /* Set ptr on the error */
goto http_msg_invalid;
}
+ /* RFC9110#4.2.1/4.2.2: an http(s) URI with an
+ * empty host is invalid. Otherwise the target
+ * seen by the rules (no host) would differ from
+ * the one some servers would find in the URI.
+ */
+ if (isttest(uri_port))
+ uri_host.len -= istlen(uri_port) + 1;
+ if (!istlen(uri_host) &&
+ (isteqi(scheme, ist("http://")) || isteqi(scheme, ist("https://")))) {
+ if (h1m->err_pos < -1) {
+ state = H1_MSG_LAST_LF;
+ ptr = sl.rq.u.ptr; /* Set ptr on the error */
+ goto http_msg_invalid;
+ }
+ if (h1m->err_pos == -1) /* capture the error pointer */
+ h1m->err_pos = sl.rq.u.ptr - start + skip; /* >= 0 now */
+ }
+
if (host_idx != -1)
host = hdr[host_idx].v;
/* For non-CONNECT method, the authority must match the host header value */