]> git.kaiwu.me - haproxy.git/commitdiff
REGTESTS: http-messaging: test the http-hardening checks on HTTP/1 headers
authorWilly Tarreau <w@1wt.eu>
Wed, 4 Nov 2026 17:07:55 +0000 (18:07 +0100)
committerWilly Tarreau <w@1wt.eu>
Wed, 4 Nov 2026 21:45:33 +0000 (22:45 +0100)
This is an LLM-generated reg-test covering HTTP/1 hardening checks.

Each header-related check gets its own profile, applied to a bind line for
requests and to a server line for responses, and a profile with everything
disabled shows that each offending message is accepted by default. The
frontend replies itself with the method, the header names and the value
of a header it saw, so that deletions are observed after the H1 parser.

Covered: duplicate Host, lower case method, underscore in header name (reject,
delete, and reject having precedence over delete), unregistered header
fields, folding (after the colon, and after a value with SP or HT), CTL and
DEL in values (with HT, SP and obs-text still accepted, and CTL found past
the first words), Content-Length with Transfer-Encoding, and Content-Length
with leading zeroes. Rejected requests must return a 400 and rejected
responses a 502.

reg-tests/http-messaging/http_hardening_h1.vtc [new file with mode: 0644]

diff --git a/reg-tests/http-messaging/http_hardening_h1.vtc b/reg-tests/http-messaging/http_hardening_h1.vtc
new file mode 100644 (file)
index 0000000..41a3c4c
--- /dev/null
@@ -0,0 +1,540 @@
+varnishtest "HTTP hardening checks on HTTP/1 headers"
+
+# Each http-hardening check gets its own profile containing only that check,
+# 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 showing that each offending message is accepted by default.
+#
+# On the request side there is no server: the frontend replies itself and
+# reports the method, the header names it saw and the hex-encoded value of
+# header "foo", which reflect what remains after the checks ran in the H1
+# parser.
+#
+# On the response side, each profile's server first sends a valid response,
+# then the offending ones, which must turn into 502 when the check is enabled.
+# Each response is on its own server connection ("http-reuse never") and each
+# client request on its own client connection.
+
+feature cmd "$HAPROXY_PROGRAM -cc 'version_atleast(3.5-dev10)'"
+feature ignore_unknown_macro
+
+# reference server: all offending responses, all expected to pass
+server s_off {
+       rxreq
+       txresp -hdr "x_a: 1" -hdr "x-b: 2"
+       accept
+       rxreq
+       txresp -hdr "X-Foo: 1" -hdr "foo: 2"
+       accept
+       rxreq
+       txresp -hdr "foo:\r\n b"
+       accept
+       rxreq
+       txresp -hdr "foo: a\r\n b"
+       accept
+       rxreq
+       txresp -hdr "foo: a\x01b"
+       accept
+       rxreq
+       txresp -nolen -hdr "content-length: 2" -hdr "transfer-encoding: chunked" -body "2\r\nok\r\n0\r\n\r\n"
+       accept
+       rxreq
+       txresp -nolen -hdr "content-length: 02" -body "ok"
+} -start
+
+server s_us_rej {
+       rxreq
+       txresp -hdr "x-b: 2"
+       accept
+       rxreq
+       txresp -hdr "x_a: 1"
+} -start
+
+server s_us_del {
+       rxreq
+       txresp -hdr "x_a: 1" -hdr "x-b: 2"
+} -start
+
+server s_us_both {
+       rxreq
+       txresp -hdr "x_a: 1"
+} -start
+
+server s_unreg {
+       rxreq
+       txresp -hdr "X-Foo: 1" -hdr "x-bar: 2" -hdr "foo: 3" -hdr "xa: 4"
+} -start
+
+server s_fold {
+       rxreq
+       txresp -hdr "foo: a b"
+       accept
+       rxreq
+       txresp -hdr "foo:\r\n b"
+       accept
+       rxreq
+       txresp -hdr "foo: a\r\n b"
+} -start
+
+server s_ctl {
+       rxreq
+       txresp -hdr "foo: a\tb\xe9c"
+       accept
+       rxreq
+       txresp -hdr "foo: a\x01b"
+       accept
+       rxreq
+       txresp -hdr "foo: a\x7fb"
+       accept
+       rxreq
+       txresp -hdr "foo: abcdefghijklmnopq\x1fb"
+} -start
+
+server s_clte {
+       rxreq
+       txresp -nolen -hdr "transfer-encoding: chunked" -body "2\r\nok\r\n0\r\n\r\n"
+       accept
+       rxreq
+       txresp -nolen -hdr "content-length: 2" -hdr "transfer-encoding: chunked" -body "2\r\nok\r\n0\r\n\r\n"
+} -start
+
+server s_cl0 {
+       rxreq
+       txresp -nolen -hdr "content-length: 0"
+       accept
+       rxreq
+       txresp -nolen -hdr "content-length: 20" -body "01234567890123456789"
+       accept
+       rxreq
+       txresp -nolen -hdr "content-length: 02" -body "ok"
+} -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
+
+    # reject must have precedence over delete
+    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 fold
+       reject-header-trailer-folding on
+
+    http-hardening ctl
+       reject-ctl-in-header-trailer on
+
+    http-hardening clte
+       reject-content-length-with-te 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}"      http-hardening off
+       bind "fd@${req_dup_host}" http-hardening dup-host
+       bind "fd@${req_lc_meth}"  http-hardening lc-meth
+       bind "fd@${req_us_rej}"   http-hardening us-rej
+       bind "fd@${req_us_del}"   http-hardening us-del
+       bind "fd@${req_us_both}"  http-hardening us-both
+       bind "fd@${req_unreg}"    http-hardening unreg
+       bind "fd@${req_fold}"     http-hardening fold
+       bind "fd@${req_ctl}"      http-hardening ctl
+       bind "fd@${req_clte}"     http-hardening clte
+       bind "fd@${req_cl0}"      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 profile
+    listen res_off
+       bind "fd@${res_off}" http-hardening off
+       server s ${s_off_addr}:${s_off_port} http-hardening off
+
+    listen res_us_rej
+       bind "fd@${res_us_rej}" http-hardening off
+       server s ${s_us_rej_addr}:${s_us_rej_port} http-hardening us-rej
+
+    listen res_us_del
+       bind "fd@${res_us_del}" http-hardening off
+       server s ${s_us_del_addr}:${s_us_del_port} http-hardening us-del
+
+    listen res_us_both
+       bind "fd@${res_us_both}" http-hardening off
+       server s ${s_us_both_addr}:${s_us_both_port} http-hardening us-both
+
+    listen res_unreg
+       bind "fd@${res_unreg}" http-hardening off
+       server s ${s_unreg_addr}:${s_unreg_port} http-hardening unreg
+
+    listen res_fold
+       bind "fd@${res_fold}" http-hardening off
+       server s ${s_fold_addr}:${s_fold_port} http-hardening fold
+
+    listen res_ctl
+       bind "fd@${res_ctl}" http-hardening off
+       server s ${s_ctl_addr}:${s_ctl_port} http-hardening ctl
+
+    listen res_clte
+       bind "fd@${res_clte}" http-hardening off
+       server s ${s_clte_addr}:${s_clte_port} http-hardening clte
+
+    listen res_cl0
+       bind "fd@${res_cl0}" http-hardening off
+       server s ${s_cl0_addr}:${s_cl0_port} http-hardening cl0
+} -start
+
+#
+# Requests
+#
+
+# reference: everything is accepted with all checks off
+client c_req_off -connect ${h1_req_off_sock} {
+       txreq -nouseragent -nohost -hdr "host: a" -hdr "host: a"
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-hdrs == "host"
+
+       txreq -nouseragent -req get
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-meth == "get"
+
+       txreq -nouseragent -nohost -hdr "x_a: 1" -hdr "X-Foo: 2"
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-hdrs == "x_a,x-foo"
+
+       txreq -nouseragent -hdr "foo:\r\n b"
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-foo == "62"
+
+       txreq -nouseragent -hdr "foo: a\r\n b"
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-foo == "6120202062"
+
+       txreq -nouseragent -hdr "foo: a\x01b"
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-foo == "610162"
+
+       txreq -nouseragent -req POST -nolen -hdr "content-length: 02" -body "ok"
+       rxresp
+       expect resp.status == 200
+
+       # T-E + C-L forces a close
+       txreq -nouseragent -req POST -nolen -hdr "content-length: 2" -hdr "transfer-encoding: chunked" -body "2\r\nok\r\n0\r\n\r\n"
+       rxresp
+       expect resp.status == 200
+} -run
+
+client c_req_dup_host -connect ${h1_req_dup_host_sock} {
+       txreq -nouseragent -nohost -hdr "host: a"
+       rxresp
+       expect resp.status == 200
+
+       txreq -nouseragent -nohost -hdr "host: a" -hdr "host: a"
+       rxresp
+       expect resp.status == 400
+       expect_close
+} -run
+
+client c_req_lc_meth -connect ${h1_req_lc_meth_sock} {
+       txreq -nouseragent -req GET
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-meth == "GET"
+
+       # non-standard methods are fine as long as they're upper case
+       txreq -nouseragent -req MY-METH_1
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-meth == "MY-METH_1"
+
+       txreq -nouseragent -req gET
+       rxresp
+       expect resp.status == 400
+       expect_close
+} -run
+
+client c_req_lc_meth2 -connect ${h1_req_lc_meth_sock} {
+       txreq -nouseragent -req GEt
+       rxresp
+       expect resp.status == 400
+       expect_close
+} -run
+
+client c_req_us_rej -connect ${h1_req_us_rej_sock} {
+       txreq -nouseragent -nohost -hdr "x-a: 1"
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-hdrs == "x-a"
+
+       # underscores in values are not concerned
+       txreq -nouseragent -nohost -hdr "x-a: _"
+       rxresp
+       expect resp.status == 200
+
+       txreq -nouseragent -nohost -hdr "x-a: 1" -hdr "x_b: 2"
+       rxresp
+       expect resp.status == 400
+       expect_close
+} -run
+
+client c_req_us_rej2 -connect ${h1_req_us_rej_sock} {
+       txreq -nouseragent -nohost -hdr "xb_: 2"
+       rxresp
+       expect resp.status == 400
+       expect_close
+} -run
+
+client c_req_us_del -connect ${h1_req_us_del_sock} {
+       txreq -nouseragent -nohost -hdr "x_a: 1" -hdr "x-b: 2" -hdr "_c: 3" -hdr "d: 4"
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-hdrs == "x-b,d"
+} -run
+
+client c_req_us_both -connect ${h1_req_us_both_sock} {
+       txreq -nouseragent -nohost -hdr "x_a: 1"
+       rxresp
+       expect resp.status == 400
+       expect_close
+} -run
+
+client c_req_unreg -connect ${h1_req_unreg_sock} {
+       txreq -nouseragent -nohost -hdr "X-Foo: 1" -hdr "x-bar: 2" -hdr "foo: 3" -hdr "xa: 4" -hdr "x: 5" -hdr "a-x-b: 6"
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-hdrs == "foo,xa,x,a-x-b"
+} -run
+
+client c_req_fold -connect ${h1_req_fold_sock} {
+       txreq -nouseragent -hdr "foo: a b"
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-foo == "612062"
+
+       txreq -nouseragent -hdr "foo:\r\n b"
+       rxresp
+       expect resp.status == 400
+       expect_close
+} -run
+
+client c_req_fold2 -connect ${h1_req_fold_sock} {
+       txreq -nouseragent -hdr "foo: a\r\n b"
+       rxresp
+       expect resp.status == 400
+       expect_close
+} -run
+
+client c_req_fold3 -connect ${h1_req_fold_sock} {
+       txreq -nouseragent -hdr "foo: a\r\n\tb"
+       rxresp
+       expect resp.status == 400
+       expect_close
+} -run
+
+# HT, SP and obs-text remain valid, other CTLs and DEL are rejected wherever
+# they are in the value, including past the first 8 bytes.
+client c_req_ctl -connect ${h1_req_ctl_sock} {
+       txreq -nouseragent -hdr "foo: a\tb c\xe9d"
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-foo == "6109622063E964"
+
+       txreq -nouseragent -hdr "foo: a\x01b"
+       rxresp
+       expect resp.status == 400
+       expect_close
+} -run
+
+client c_req_ctl2 -connect ${h1_req_ctl_sock} {
+       txreq -nouseragent -hdr "foo: a\x7fb"
+       rxresp
+       expect resp.status == 400
+       expect_close
+} -run
+
+client c_req_ctl3 -connect ${h1_req_ctl_sock} {
+       txreq -nouseragent -hdr "foo: abcdefghijklmnopq\x1fb"
+       rxresp
+       expect resp.status == 400
+       expect_close
+} -run
+
+client c_req_ctl4 -connect ${h1_req_ctl_sock} {
+       txreq -nouseragent -hdr "foo: abcdefghijklmnopqrstuvw\x08"
+       rxresp
+       expect resp.status == 400
+       expect_close
+} -run
+
+client c_req_clte -connect ${h1_req_clte_sock} {
+       txreq -nouseragent -req POST -nolen -hdr "content-length: 2" -body "ok"
+       rxresp
+       expect resp.status == 200
+
+       txreq -nouseragent -req POST -nolen -hdr "transfer-encoding: chunked" -body "2\r\nok\r\n0\r\n\r\n"
+       rxresp
+       expect resp.status == 200
+
+       txreq -nouseragent -req POST -nolen -hdr "content-length: 2" -hdr "transfer-encoding: chunked" -body "2\r\nok\r\n0\r\n\r\n"
+       rxresp
+       expect resp.status == 400
+       expect_close
+} -run
+
+client c_req_cl0 -connect ${h1_req_cl0_sock} {
+       txreq -nouseragent -req POST -nolen -hdr "content-length: 0"
+       rxresp
+       expect resp.status == 200
+
+       txreq -nouseragent -req POST -nolen -hdr "content-length: 20" -body "01234567890123456789"
+       rxresp
+       expect resp.status == 200
+
+       txreq -nouseragent -req POST -nolen -hdr "content-length: 02" -body "ok"
+       rxresp
+       expect resp.status == 400
+       expect_close
+} -run
+
+client c_req_cl02 -connect ${h1_req_cl0_sock} {
+       txreq -nouseragent -req POST -nolen -hdr "content-length: 00"
+       rxresp
+       expect resp.status == 400
+       expect_close
+} -run
+
+#
+# Responses
+#
+
+client c_res_off -connect ${h1_res_off_sock} -repeat 7 {
+       txreq
+       rxresp
+       expect resp.status == 200
+} -run
+
+client c_res_us_rej1 -connect ${h1_res_us_rej_sock} {
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-b == "2"
+} -run
+
+client c_res_us_rej2 -connect ${h1_res_us_rej_sock} {
+       txreq
+       rxresp
+       expect resp.status == 502
+} -run
+
+client c_res_us_del -connect ${h1_res_us_del_sock} {
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x_a == <undef>
+       expect resp.http.x-b == "2"
+} -run
+
+client c_res_us_both -connect ${h1_res_us_both_sock} {
+       txreq
+       rxresp
+       expect resp.status == 502
+} -run
+
+client c_res_unreg -connect ${h1_res_unreg_sock} {
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-foo == <undef>
+       expect resp.http.x-bar == <undef>
+       expect resp.http.foo == "3"
+       expect resp.http.xa == "4"
+} -run
+
+client c_res_fold1 -connect ${h1_res_fold_sock} {
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.foo == "a b"
+} -run
+
+client c_res_fold2 -connect ${h1_res_fold_sock} {
+       txreq
+       rxresp
+       expect resp.status == 502
+} -run
+
+client c_res_fold3 -connect ${h1_res_fold_sock} {
+       txreq
+       rxresp
+       expect resp.status == 502
+} -run
+
+client c_res_ctl1 -connect ${h1_res_ctl_sock} {
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.foo == "a\tb\xe9c"
+} -run
+
+client c_res_ctl2 -connect ${h1_res_ctl_sock} -repeat 3 {
+       txreq
+       rxresp
+       expect resp.status == 502
+} -run
+
+client c_res_clte1 -connect ${h1_res_clte_sock} {
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.body == "ok"
+} -run
+
+client c_res_clte2 -connect ${h1_res_clte_sock} {
+       txreq
+       rxresp
+       expect resp.status == 502
+} -run
+
+client c_res_cl0_1 -connect ${h1_res_cl0_sock} -repeat 2 {
+       txreq
+       rxresp
+       expect resp.status == 200
+} -run
+
+client c_res_cl0_2 -connect ${h1_res_cl0_sock} {
+       txreq
+       rxresp
+       expect resp.status == 502
+} -run