From: Willy Tarreau Date: Wed, 4 Nov 2026 17:08:51 +0000 (+0100) Subject: REGTESTS: http-messaging: test the http-hardening checks on HTTP/2 headers X-Git-Tag: v3.5-dev11~3 X-Git-Url: https://git.kaiwu.me/$%7BGITURL%7D$%7Bcid%7D?a=commitdiff_plain;h=0312f6dcde210a03f9bbf52d38a2c129147cb705;p=haproxy.git REGTESTS: http-messaging: test the http-hardening checks on HTTP/2 headers This is an LLM-generated reg-test covering HTTP/2 hardening checks. This is the HTTP/2 counterpart of http_hardening_h1.vtc for the checks that apply to HTTP/2 (folding and Transfer-Encoding cannot appear there). A rejected request must cause a stream error, except for an invalid method which results in a 400 like other method errors, and a rejected response a 502. It also checks that a Host header accompanying an identical :authority is not considered as a duplicate Host header. --- diff --git a/reg-tests/http-messaging/http_hardening_h2.vtc b/reg-tests/http-messaging/http_hardening_h2.vtc new file mode 100644 index 000000000..b7a72f4f4 --- /dev/null +++ b/reg-tests/http-messaging/http_hardening_h2.vtc @@ -0,0 +1,869 @@ +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 == + 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 == + 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