--- /dev/null
+varnishtest "HTTP hardening checks on HTTP/2 headers"
+
+# Note: vtest needs the pseudo-headers to be explicit on requests carrying
+# extra headers, otherwise it appends them after the regular ones.
+#
+# Same principle as http_hardening_h1.vtc, for the checks which apply to
+# HTTP/2: each check has its own profile, applied to a dedicated bind line for
+# requests, or to a dedicated server line for responses. The "off" profile has
+# everything disabled and serves as the reference. A rejected request causes
+# a stream error (or a 400 for an invalid method, like other method errors),
+# and a rejected response causes a 502.
+#
+# On the request side the frontend replies itself and reports the method, the
+# header names it saw and the hex-encoded value of header "foo".
+
+feature cmd "$HAPROXY_PROGRAM -cc 'version_atleast(3.5-dev10)'"
+feature ignore_unknown_macro
+
+server s_off_us {
+ rxpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ } -run
+
+ stream 1 {
+ rxreq
+ txresp -status 200 -hdr "x_a" "1" -hdr "x-b" "2"
+ } -run
+} -start
+
+server s_off_unreg {
+ rxpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ } -run
+
+ stream 1 {
+ rxreq
+ txresp -status 200 -hdr "x-foo" "1" -hdr "foo" "2"
+ } -run
+} -start
+
+server s_off_ctl {
+ rxpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ } -run
+
+ stream 1 {
+ rxreq
+ txresp -status 200 -hdr "foo" "a\x01b"
+ } -run
+} -start
+
+server s_off_cl0 {
+ rxpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ } -run
+
+ stream 1 {
+ rxreq
+ txresp -status 200 -hdr "content-length" "00"
+ } -run
+} -start
+
+server s_us_rej_ok {
+ rxpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ } -run
+
+ stream 1 {
+ rxreq
+ txresp -status 200 -hdr "x-b" "2"
+ } -run
+} -start
+
+server s_us_rej {
+ rxpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ } -run
+
+ stream 1 {
+ rxreq
+ txresp -status 200 -hdr "x_a" "1"
+ } -run
+} -start
+
+server s_us_del {
+ rxpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ } -run
+
+ stream 1 {
+ rxreq
+ txresp -status 200 -hdr "x_a" "1" -hdr "x-b" "2"
+ } -run
+} -start
+
+server s_us_both {
+ rxpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ } -run
+
+ stream 1 {
+ rxreq
+ txresp -status 200 -hdr "x_a" "1"
+ } -run
+} -start
+
+server s_unreg {
+ rxpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ } -run
+
+ stream 1 {
+ rxreq
+ txresp -status 200 -hdr "x-foo" "1" -hdr "foo" "2" -hdr "xa" "3"
+ } -run
+} -start
+
+server s_ctl_ok {
+ rxpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ } -run
+
+ stream 1 {
+ rxreq
+ txresp -status 200 -hdr "foo" "a\tb\xe9c"
+ } -run
+} -start
+
+server s_ctl1 {
+ rxpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ } -run
+
+ stream 1 {
+ rxreq
+ txresp -status 200 -hdr "foo" "a\x01b"
+ } -run
+} -start
+
+server s_ctl2 {
+ rxpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ } -run
+
+ stream 1 {
+ rxreq
+ txresp -status 200 -hdr "foo" "a\x7fb"
+ } -run
+} -start
+
+server s_ctl3 {
+ rxpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ } -run
+
+ stream 1 {
+ rxreq
+ txresp -status 200 -hdr "foo" "abcdefghijklmnopq\x1fb"
+ } -run
+} -start
+
+server s_cl0_ok {
+ rxpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ } -run
+
+ stream 1 {
+ rxreq
+ txresp -status 200 -hdr "content-length" "0"
+ } -run
+} -start
+
+server s_cl0 {
+ rxpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ } -run
+
+ stream 1 {
+ rxreq
+ txresp -status 200 -hdr "content-length" "00"
+ } -run
+} -start
+
+haproxy h1 -conf {
+ global
+ .if feature(THREAD)
+ thread-groups 1
+ .endif
+
+ http-hardening off
+ all off
+
+ http-hardening dup-host
+ reject-duplicate-host-header on
+
+ http-hardening lc-meth
+ reject-lowercase-method on
+
+ http-hardening us-rej
+ reject-underscore-in-header-name on
+
+ http-hardening us-del
+ delete-underscore-in-header-name on
+
+ http-hardening us-both
+ reject-underscore-in-header-name on
+ delete-underscore-in-header-name on
+
+ http-hardening unreg
+ delete-unregistered-header-fields on
+
+ http-hardening ctl
+ reject-ctl-in-header-trailer on
+
+ http-hardening cl0
+ reject-content-length-leading-zero on
+
+ defaults
+ mode http
+ http-reuse never
+ timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
+ timeout client "${HAPROXY_TEST_TIMEOUT-5s}"
+ timeout server "${HAPROXY_TEST_TIMEOUT-5s}"
+
+ # request side: the frontend replies with what it parsed
+ frontend fe_req
+ bind "fd@${req_off}" proto h2 http-hardening off
+ bind "fd@${req_dup_host}" proto h2 http-hardening dup-host
+ bind "fd@${req_lc_meth}" proto h2 http-hardening lc-meth
+ bind "fd@${req_us_rej}" proto h2 http-hardening us-rej
+ bind "fd@${req_us_del}" proto h2 http-hardening us-del
+ bind "fd@${req_us_both}" proto h2 http-hardening us-both
+ bind "fd@${req_unreg}" proto h2 http-hardening unreg
+ bind "fd@${req_ctl}" proto h2 http-hardening ctl
+ bind "fd@${req_cl0}" proto h2 http-hardening cl0
+ http-request return status 200 hdr x-meth "%[method]" hdr x-hdrs "%[req.hdr_names]" hdr x-foo "%[req.hdr(foo),hex]"
+
+ # response side: one listener per server
+ listen s_off_us
+ bind "fd@${s_off_us}" proto h2 http-hardening off
+ server s ${s_off_us_addr}:${s_off_us_port} proto h2 http-hardening off
+
+ listen s_off_unreg
+ bind "fd@${s_off_unreg}" proto h2 http-hardening off
+ server s ${s_off_unreg_addr}:${s_off_unreg_port} proto h2 http-hardening off
+
+ listen s_off_ctl
+ bind "fd@${s_off_ctl}" proto h2 http-hardening off
+ server s ${s_off_ctl_addr}:${s_off_ctl_port} proto h2 http-hardening off
+
+ listen s_off_cl0
+ bind "fd@${s_off_cl0}" proto h2 http-hardening off
+ server s ${s_off_cl0_addr}:${s_off_cl0_port} proto h2 http-hardening off
+
+ listen s_us_rej_ok
+ bind "fd@${s_us_rej_ok}" proto h2 http-hardening off
+ server s ${s_us_rej_ok_addr}:${s_us_rej_ok_port} proto h2 http-hardening us-rej
+
+ listen s_us_rej
+ bind "fd@${s_us_rej}" proto h2 http-hardening off
+ server s ${s_us_rej_addr}:${s_us_rej_port} proto h2 http-hardening us-rej
+
+ listen s_us_del
+ bind "fd@${s_us_del}" proto h2 http-hardening off
+ server s ${s_us_del_addr}:${s_us_del_port} proto h2 http-hardening us-del
+
+ listen s_us_both
+ bind "fd@${s_us_both}" proto h2 http-hardening off
+ server s ${s_us_both_addr}:${s_us_both_port} proto h2 http-hardening us-both
+
+ listen s_unreg
+ bind "fd@${s_unreg}" proto h2 http-hardening off
+ server s ${s_unreg_addr}:${s_unreg_port} proto h2 http-hardening unreg
+
+ listen s_ctl_ok
+ bind "fd@${s_ctl_ok}" proto h2 http-hardening off
+ server s ${s_ctl_ok_addr}:${s_ctl_ok_port} proto h2 http-hardening ctl
+
+ listen s_ctl1
+ bind "fd@${s_ctl1}" proto h2 http-hardening off
+ server s ${s_ctl1_addr}:${s_ctl1_port} proto h2 http-hardening ctl
+
+ listen s_ctl2
+ bind "fd@${s_ctl2}" proto h2 http-hardening off
+ server s ${s_ctl2_addr}:${s_ctl2_port} proto h2 http-hardening ctl
+
+ listen s_ctl3
+ bind "fd@${s_ctl3}" proto h2 http-hardening off
+ server s ${s_ctl3_addr}:${s_ctl3_port} proto h2 http-hardening ctl
+
+ listen s_cl0_ok
+ bind "fd@${s_cl0_ok}" proto h2 http-hardening off
+ server s ${s_cl0_ok_addr}:${s_cl0_ok_port} proto h2 http-hardening cl0
+
+ listen s_cl0
+ bind "fd@${s_cl0}" proto h2 http-hardening off
+ server s ${s_cl0_addr}:${s_cl0_port} proto h2 http-hardening cl0
+
+} -start
+
+#
+# Requests
+#
+
+# reference: everything is accepted with all checks off
+client c_req_off -connect ${h1_req_off_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a" -hdr "host" "a"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-hdrs == "host"
+ } -run
+ stream 3 {
+ txreq -req get -url / -scheme http -hdr ":authority" "a"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-meth == "get"
+ } -run
+ stream 5 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a" -hdr "x_a" "1" -hdr "x-foo" "2"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-hdrs == "host,x_a,x-foo"
+ } -run
+ stream 7 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a" -hdr "foo" "a\x01b"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-foo == "610162"
+ } -run
+ stream 9 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a" -hdr "foo" "a\x7fb"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-foo == "617F62"
+ } -run
+ stream 11 {
+ txreq -req POST -url / -scheme http -hdr ":authority" "a" -hdr "content-length" "00"
+ rxresp
+ expect resp.status == 200
+ } -run
+} -run
+
+# without :authority, Host is the only source of the authority
+client c_req_dup_host -connect ${h1_req_dup_host_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr "host" "a"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-hdrs == "host"
+ } -run
+ stream 3 {
+ txreq -req GET -url / -scheme http -hdr "host" "a" -hdr "host" "a"
+ rxrst
+ expect rst.err == PROTOCOL_ERROR
+ } -run
+} -run
+
+# :authority followed by an identical host header is legitimate (RFC9113
+# 8.3.1), it is not a duplicate Host header field.
+client c_req_dup_host2 -connect ${h1_req_dup_host_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a" -hdr "host" "a"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-hdrs == "host"
+ } -run
+} -run
+
+client c_req_lc_meth -connect ${h1_req_lc_meth_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-meth == "GET"
+ } -run
+ stream 3 {
+ txreq -req MY-METH_1 -url / -scheme http -hdr ":authority" "a"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-meth == "MY-METH_1"
+ } -run
+ stream 5 {
+ txreq -req gET -url / -scheme http -hdr ":authority" "a"
+ rxresp
+ expect resp.status == 400
+ } -run
+} -run
+
+client c_req_us_rej -connect ${h1_req_us_rej_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a" -hdr "x-a" "_"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-hdrs == "host,x-a"
+ } -run
+ stream 3 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a" -hdr "x-a" "1" -hdr "x_b" "2"
+ rxrst
+ expect rst.err == PROTOCOL_ERROR
+ } -run
+} -run
+
+client c_req_us_del -connect ${h1_req_us_del_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a" -hdr "x_a" "1" -hdr "x-b" "2" -hdr "_c" "3" -hdr "d" "4"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-hdrs == "host,x-b,d"
+ } -run
+} -run
+
+client c_req_us_both -connect ${h1_req_us_both_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a" -hdr "x_a" "1"
+ rxrst
+ expect rst.err == PROTOCOL_ERROR
+ } -run
+} -run
+
+client c_req_unreg -connect ${h1_req_unreg_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a" -hdr "x-foo" "1" -hdr "foo" "3" -hdr "xa" "4" -hdr "x" "5" -hdr "a-x-b" "6"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-hdrs == "host,foo,xa,x,a-x-b"
+ } -run
+} -run
+
+client c_req_ctl -connect ${h1_req_ctl_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a" -hdr "foo" "a\tb c\xe9d"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-foo == "6109622063E964"
+ } -run
+ stream 3 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a" -hdr "foo" "a\x01b"
+ rxrst
+ expect rst.err == PROTOCOL_ERROR
+ } -run
+ stream 5 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a" -hdr "foo" "a\x7fb"
+ rxrst
+ expect rst.err == PROTOCOL_ERROR
+ } -run
+ stream 7 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a" -hdr "foo" "abcdefghijklmnopq\x1fb"
+ rxrst
+ expect rst.err == PROTOCOL_ERROR
+ } -run
+} -run
+
+client c_req_cl0 -connect ${h1_req_cl0_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req POST -url / -scheme http -hdr ":authority" "a" -hdr "content-length" "0"
+ rxresp
+ expect resp.status == 200
+ } -run
+ stream 3 {
+ txreq -req POST -url / -scheme http -hdr ":authority" "a" -hdr "content-length" "00"
+ rxrst
+ expect rst.err == PROTOCOL_ERROR
+ } -run
+} -run
+
+#
+# Responses
+#
+
+client c_s_off_us -connect ${h1_s_off_us_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x_a == "1"
+ } -run
+} -run
+
+client c_s_off_unreg -connect ${h1_s_off_unreg_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-foo == "1"
+ } -run
+} -run
+
+client c_s_off_ctl -connect ${h1_s_off_ctl_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a"
+ rxresp
+ expect resp.status == 200
+ } -run
+} -run
+
+client c_s_off_cl0 -connect ${h1_s_off_cl0_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a"
+ rxresp
+ expect resp.status == 200
+ } -run
+} -run
+
+client c_s_us_rej_ok -connect ${h1_s_us_rej_ok_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-b == "2"
+ } -run
+} -run
+
+client c_s_us_rej -connect ${h1_s_us_rej_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a"
+ rxresp
+ expect resp.status == 502
+ } -run
+} -run
+
+client c_s_us_del -connect ${h1_s_us_del_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x_a == <undef>
+ expect resp.http.x-b == "2"
+ } -run
+} -run
+
+client c_s_us_both -connect ${h1_s_us_both_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a"
+ rxresp
+ expect resp.status == 502
+ } -run
+} -run
+
+client c_s_unreg -connect ${h1_s_unreg_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-foo == <undef>
+ expect resp.http.foo == "2"
+ expect resp.http.xa == "3"
+ } -run
+} -run
+
+client c_s_ctl_ok -connect ${h1_s_ctl_ok_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a"
+ rxresp
+ expect resp.status == 200
+ } -run
+} -run
+
+client c_s_ctl1 -connect ${h1_s_ctl1_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a"
+ rxresp
+ expect resp.status == 502
+ } -run
+} -run
+
+client c_s_ctl2 -connect ${h1_s_ctl2_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a"
+ rxresp
+ expect resp.status == 502
+ } -run
+} -run
+
+client c_s_ctl3 -connect ${h1_s_ctl3_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a"
+ rxresp
+ expect resp.status == 502
+ } -run
+} -run
+
+client c_s_cl0_ok -connect ${h1_s_cl0_ok_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a"
+ rxresp
+ expect resp.status == 200
+ } -run
+} -run
+
+client c_s_cl0 -connect ${h1_s_cl0_sock} {
+ txpri
+ stream 0 {
+ txsettings
+ rxsettings
+ txsettings -ack
+ rxsettings
+ expect settings.ack == true
+ } -run
+
+ stream 1 {
+ txreq -req GET -url / -scheme http -hdr ":authority" "a"
+ rxresp
+ expect resp.status == 502
+ } -run
+} -run