From: Willy Tarreau Date: Tue, 10 Nov 2026 10:55:41 +0000 (+0100) Subject: REGTESTS: http-messaging: test the reporting of HTTP hardening results X-Git-Tag: v3.5-dev12~1 X-Git-Url: https://git.kaiwu.me/%7B@url%7D?a=commitdiff_plain;h=c5c1fa00546028a94d6202260b4df20ccd2cc060;p=haproxy.git REGTESTS: http-messaging: test the reporting of HTTP hardening results This new test checks fs.hdng.found, fs.hdng.fixed, bs.hdng.found and bs.hdng.fixed from a log-format, as well as the hdng_rej_name converter. The same anomalies (underscore in a header name, unregistered header, CTL in a trailer) are sent: - through the "trusted" profile on both sides: everything passes, and only "found" reports them; - in a request to a "strict" profile which deletes the two headers and rejects the trailer: the 400 is logged with the three rules in "fixed" and hdng_rej_name only names the reject. The trailers are sent apart so that the stream exists when they are rejected; - in a response from a server using the "strict" profile: same with a 502 on the backend side; - in an H2 request with only the deleted headers: nothing is named. --- diff --git a/reg-tests/http-messaging/http_hardening_report.vtc b/reg-tests/http-messaging/http_hardening_report.vtc new file mode 100644 index 000000000..eadfa0ac4 --- /dev/null +++ b/reg-tests/http-messaging/http_hardening_report.vtc @@ -0,0 +1,143 @@ +varnishtest "HTTP hardening reporting: fs/bs.hdng.found/fixed and hdng_rej_name" + +# This checks that the HTTP hardening conditions met and rules enforced on +# each side are reported by the {fs,bs}.hdng.{found,fixed} sample fetches, and +# that the hdng_rej_name converter turns a "fixed" mask into the name of the reject +# rule. Each message carries the same anomalies: +# - a header named "x_a" (underscore-in-header-name) +# - a header named "X-Foo" (unregistered-header-fields) +# - a trailer with a CTL in its value (reject-ctl-in-header-trailer) +# The trailer is rejected late, once the stream exists, so that the stream +# logs it (a rejection before that is handled and logged by the mux alone). +# The "trusted" profile (the default over the loopback) lets everything pass +# and only reports what was found. The "strict" profile deletes the headers +# and rejects the trailer, so only the reject is named by hdng_rej_name. + +feature cmd "$HAPROXY_PROGRAM -cc 'version_atleast(3.5-dev11)'" +feature ignore_unknown_macro + +# HTTP_HRD_* bits met here: +# 0x004 reject-underscore-in-header-name +# 0x010 delete-underscore-in-header-name +# 0x040 delete-unregistered-header-fields (only looked up when enabled) +# 0x100 delete-all-trailer-fields (any trailer) +# 0x400 reject-ctl-in-header-trailer +# The masks are logged in hex after htonl. The H2 frontend has no +# backend, so "bs" is not available there. + +# vtest cannot parse the CTL in the trailer, so only the headers are read +server s_trusted { + rxreqhdrs + expect req.http.x_a == "1" + expect req.http.x-foo == "2" + txresp -nolen -hdr "x_a: 1" -hdr "X-Foo: 2" -hdr "transfer-encoding: chunked" -body "2\r\nok\r\n0\r\nt-a: 1\x012\r\n\r\n" +} -start + +server s_strict { + rxreq + txresp -nolen -hdr "x_a: 1" -hdr "X-Foo: 2" -hdr "transfer-encoding: chunked" -body "2\r\nok\r\n0\r\nt-a: 1\x012\r\n\r\n" +} -start + +syslog Strusted -level info { + recv + expect ~ "fe_trusted .* fs=00000514/00000000/- bs=00000514/00000000/-$" +} -start + +syslog Sreq -level info { + recv + expect ~ "fe_req .* fs=00000454/00000450/reject-ctl-in-header-trailer bs=-/-/-$" +} -start + +syslog Sres -level info { + recv + expect ~ "fe_res .* fs=00000000/00000000/- bs=00000454/00000450/reject-ctl-in-header-trailer$" +} -start + +syslog Sh2 -level info { + recv + expect ~ "fe_h2 .* fs=00000054/00000050/- bs=-/-/-$" +} -start + +haproxy h1 -conf { + global + .if feature(THREAD) + thread-groups 1 + .endif + + http-hardening strict + delete-underscore-in-header-name on + delete-unregistered-header-fields on + reject-ctl-in-header-trailer on + + defaults + mode http + http-reuse never + option http-buffer-request + timeout connect "${HAPROXY_TEST_TIMEOUT-5s}" + timeout client "${HAPROXY_TEST_TIMEOUT-5s}" + timeout server "${HAPROXY_TEST_TIMEOUT-5s}" + log-format "%f %ST fs=%[fs.hdng.found,htonl,hex]/%[fs.hdng.fixed,htonl,hex]/%[fs.hdng.fixed,hdng_rej_name] bs=%[bs.hdng.found,htonl,hex]/%[bs.hdng.fixed,htonl,hex]/%[bs.hdng.fixed,hdng_rej_name]" + + # everything passes, only reported as found + listen fe_trusted + log ${Strusted_addr}:${Strusted_port} local0 + bind "fd@${fe_trusted}" http-hardening trusted + http-response wait-for-body time 5s + server s1 ${s_trusted_addr}:${s_trusted_port} + + # the request trailer is rejected after the deletion of the headers + frontend fe_req + log ${Sreq_addr}:${Sreq_port} local0 + bind "fd@${fe_req}" http-hardening strict + http-request return status 200 + + # the response trailer is rejected after the deletion of the headers + listen fe_res + log ${Sres_addr}:${Sres_port} local0 + bind "fd@${fe_res}" http-hardening strict + http-response wait-for-body time 5s + server s1 ${s_strict_addr}:${s_strict_port} http-hardening strict + + # H2 only deletes here, so nothing is named + frontend fe_h2 + log ${Sh2_addr}:${Sh2_port} local0 + bind "fd@${fe_h2}" proto h2 http-hardening strict + http-request return status 200 +} -start + +client c_trusted -connect ${h1_fe_trusted_sock} { + txreq -req POST -nolen -hdr "x_a: 1" -hdr "X-Foo: 2" -hdr "transfer-encoding: chunked" -body "2\r\nok\r\n0\r\nt-a: 1\x012\r\n\r\n" + rxresphdrs + expect resp.status == 200 + expect resp.http.x_a == "1" + expect resp.http.x-foo == "2" +} -run + +# the trailers are sent apart so that the stream exists when they're parsed, +# otherwise the error would be handled by the mux before the stream is created. +client c_req -connect ${h1_fe_req_sock} { + txreq -req POST -nolen -hdr "x_a: 1" -hdr "X-Foo: 2" -hdr "transfer-encoding: chunked" -body "2\r\nok\r\n" + delay 0.2 + send "0\r\nt-a: 1\x012\r\n\r\n" + rxresp + expect resp.status == 400 +} -run + +client c_res -connect ${h1_fe_res_sock} { + txreq + rxresp + expect resp.status == 502 +} -run + +client c_h2 -connect ${h1_fe_h2_sock} { + stream 1 { + txreq -req GET -url / -scheme http -hdr ":authority" "a" -hdr "x_a" "1" -hdr "x-foo" "2" + rxresp + expect resp.status == 200 + } -run +} -run + +syslog Strusted -wait +syslog Sreq -wait +syslog Sres -wait +syslog Sh2 -wait