]> git.kaiwu.me - haproxy.git/commitdiff
REGTESTS: http-messaging: test http-hardening profiles and their CLI
authorWilly Tarreau <w@1wt.eu>
Wed, 4 Nov 2026 17:11:10 +0000 (18:11 +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 hardening config and CLI.

This covers the declaration and selection of http-hardening profiles, using
a few checks that are easy to observe on HTTP/1 requests:
  - the implicit "trusted" profile is the one used over the loopback when
    no profile or "auto" is set on the bind line, and both implicit
    profiles may be redefined;
  - "from" inherits the settings of another profile, and keywords are
    applied in order ("all on" followed by an exception);
  - "all-reject" and "all-delete" only cover their own family;
  - a server inherits its profile from default-server, and "auto" resets
    it to "trusted";
  - "show http-hardening" lists the profiles (implicit ones being marked
    as such) and dumps one profile's options, and "set http-hardening"
    changes an option by name or number, with immediate effect on both
    bind and server lines, and its error messages;
  - the configuration errors: undeclared or forward-referenced profile on
    bind and server lines, reserved name "auto", duplicate profile, unknown
    or self parent, unknown keyword with a suggestion, invalid value.

Since the "untrusted" profile now enables a few rejections by default, a
redefined "untrusted" profile starts with reject-lowercase-method, so the
test must expect a lower case method to be rejected there. This is now
checked explicitly, as well as the default contents of the implicit
"untrusted" and "trusted" profiles as reported by "show http-hardening",
and their inheritance by "from".

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

diff --git a/reg-tests/http-messaging/http_hardening_profiles.vtc b/reg-tests/http-messaging/http_hardening_profiles.vtc
new file mode 100644 (file)
index 0000000..6298af9
--- /dev/null
@@ -0,0 +1,345 @@
+varnishtest "HTTP hardening profiles: declaration, inheritance, selection and CLI"
+
+# This checks how http-hardening profiles are built and selected, using a few
+# checks which are easy to observe on HTTP/1 requests:
+#   - "get" as a method          (reject-lowercase-method)
+#   - a duplicate Host header     (reject-duplicate-host-header)
+#   - a header named "x_a"        (reject/delete-underscore-in-header-name)
+#   - a header named "X-Foo"      (delete-unregistered-header-fields)
+# The frontend replies itself and reports the header names it saw. Then the
+# "show http-hardening" and "set http-hardening" CLI commands are tested, as
+# well as the configuration errors.
+
+feature cmd "$HAPROXY_PROGRAM -cc 'version_atleast(3.5-dev10)'"
+feature ignore_unknown_macro
+
+# response with an underscore in a header name, for the server side
+server s1 -repeat 3 {
+       rxreq
+       txresp -hdr "x_a: 1"
+} -start
+
+haproxy h1 -conf {
+    global
+    .if feature(THREAD)
+       thread-groups 1
+    .endif
+
+    # the implicit profiles may be redefined, in which case they start from
+    # their defaults: none for "trusted", and a few rejections including
+    # reject-lowercase-method for "untrusted". The test runs over the
+    # loopback, so "trusted" is the one used by default.
+    http-hardening trusted
+       reject-lowercase-method on
+
+    http-hardening untrusted
+       reject-duplicate-host-header on
+
+    # everything but one check
+    http-hardening base
+       all on
+       reject-lowercase-method off
+
+    # inherits everything from "base", and turns the underscore rejection
+    # into a deletion since the latter is still enabled.
+    http-hardening child from base
+       reject-underscore-in-header-name off
+
+    # the aliases only cover their own family
+    http-hardening norej
+       all on
+       all-reject off
+
+    http-hardening nodel
+       all on
+       all-delete off
+
+    # starts empty, modified from the CLI
+    http-hardening cli
+       all off
+
+    http-hardening srvp
+       reject-underscore-in-header-name 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}"
+
+    frontend fe
+       bind "fd@${auto}"
+       bind "fd@${autokw}"    http-hardening auto
+       bind "fd@${untrusted}" http-hardening untrusted
+       bind "fd@${base}"      http-hardening base
+       bind "fd@${child}"     http-hardening child
+       bind "fd@${norej}"     http-hardening norej
+       bind "fd@${nodel}"     http-hardening nodel
+       bind "fd@${clip}"      http-hardening cli
+       http-request return status 200 hdr x-hdrs "%[req.hdr_names]"
+
+    # the server profile is inherited from default-server, and may be reset
+    # to "auto", which is "trusted" for servers.
+    listen srv_inherit
+       bind "fd@${srv_inherit}" http-hardening cli
+       default-server http-hardening srvp
+       server s1 ${s1_addr}:${s1_port}
+
+    listen srv_auto
+       bind "fd@${srv_auto}" http-hardening cli
+       default-server http-hardening srvp
+       server s1 ${s1_addr}:${s1_port} http-hardening auto
+} -start
+
+# no profile or "auto" on a local connection: "trusted"
+client c_auto -connect ${h1_auto_sock} {
+       txreq -nouseragent -nohost -hdr "host: a" -hdr "host: a"
+       rxresp
+       expect resp.status == 200
+
+       txreq -req get
+       rxresp
+       expect resp.status == 400
+} -run
+
+client c_autokw -connect ${h1_autokw_sock} {
+       txreq -req get
+       rxresp
+       expect resp.status == 400
+} -run
+
+# the redefined "untrusted" keeps its defaults and adds its own rules
+client c_untrusted -connect ${h1_untrusted_sock} {
+       txreq
+       rxresp
+       expect resp.status == 200
+
+       txreq -nouseragent -nohost -hdr "host: a" -hdr "host: a"
+       rxresp
+       expect resp.status == 400
+} -run
+
+client c_untrusted2 -connect ${h1_untrusted_sock} {
+       txreq -req get
+       rxresp
+       expect resp.status == 400
+} -run
+
+client c_base -connect ${h1_base_sock} {
+       txreq -req get
+       rxresp
+       expect resp.status == 200
+
+       txreq -nouseragent -hdr "x_a: 1"
+       rxresp
+       expect resp.status == 400
+} -run
+
+client c_child -connect ${h1_child_sock} {
+       txreq -req get -nouseragent -nohost -hdr "x_a: 1" -hdr "X-Foo: 2" -hdr "b: 3"
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-hdrs == "b"
+} -run
+
+client c_norej -connect ${h1_norej_sock} {
+       txreq -req get -nouseragent -nohost -hdr "x_a: 1" -hdr "X-Foo: 2" -hdr "b: 3" -hdr "host: a" -hdr "host: a"
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-hdrs == "b,host"
+} -run
+
+client c_nodel -connect ${h1_nodel_sock} {
+       txreq -nouseragent -nohost -hdr "X-Foo: 2"
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-hdrs == "x-foo"
+
+       txreq -nouseragent -hdr "x_a: 1"
+       rxresp
+       expect resp.status == 400
+} -run
+
+client c_srv_inherit -connect ${h1_srv_inherit_sock} {
+       txreq
+       rxresp
+       expect resp.status == 502
+} -run
+
+client c_srv_auto -connect ${h1_srv_auto_sock} {
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x_a == "1"
+} -run
+
+#
+# CLI
+#
+
+haproxy h1 -cli {
+       # both implicit profiles were declared, none is reported as implicit
+       send "show http-hardening"
+       expect ~ "^base\nchild\ncli\nnodel\nnorej\nsrvp\ntrusted\nuntrusted\n"
+
+       send "show http-hardening child"
+       expect ~ " 1 reject-duplicate-host-header +req +on\n 2 reject-lowercase-method +req +off\n 3 reject-underscore-in-header-name +- +off\n 4 reject-underscore-in-trailer-name +- +on\n 5 delete-underscore-in-header-name +- +on\n"
+
+       send "show http-hardening cli"
+       expect !~ " on\n"
+
+       send "show http-hardening nope"
+       expect ~ "Unknown http-hardening profile"
+
+       send "set http-hardening nope all on"
+       expect ~ "Unknown http-hardening profile"
+
+       send "set http-hardening cli nope on"
+       expect ~ "Unknown http-hardening option"
+
+       send "set http-hardening cli reject-lowercase-meth on"
+       expect ~ "Maybe you meant 'reject-lowercase-method'"
+
+       send "set http-hardening cli all maybe"
+       expect ~ "Expects 'on' or 'off'"
+
+       send "set http-hardening cli all"
+       expect ~ "Usage: set http-hardening"
+
+       send "set http-hardening cli reject-lowercase-method on"
+       expect ~ "^$"
+
+       send "show http-hardening cli"
+       expect ~ " 2 reject-lowercase-method +req +on\n 3 reject-underscore-in-header-name +- +off\n"
+}
+
+client c_cli1 -connect ${h1_clip_sock} {
+       txreq -req get
+       rxresp
+       expect resp.status == 400
+} -run
+
+haproxy h1 -cli {
+       # options may be designated by their number
+       send "set http-hardening cli 2 off"
+       expect ~ "^$"
+
+       send "set http-hardening cli 3 on"
+       expect ~ "^$"
+
+       send "show http-hardening cli"
+       expect ~ " 2 reject-lowercase-method +req +off\n 3 reject-underscore-in-header-name +- +on\n"
+}
+
+client c_cli2 -connect ${h1_clip_sock} {
+       txreq -req get
+       rxresp
+       expect resp.status == 200
+
+       txreq -nouseragent -hdr "x_a: 1"
+       rxresp
+       expect resp.status == 400
+} -run
+
+# the change applies to the servers using the profile as well
+haproxy h1 -cli {
+       send "set http-hardening srvp all off"
+       expect ~ "^$"
+}
+
+client c_srv_inherit2 -connect ${h1_srv_inherit_sock} {
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x_a == "1"
+} -run
+
+# without declaration, the two implicit profiles are reported as such, with
+# their default settings, which are inherited by "from".
+haproxy h2 -conf {
+    global
+    .if feature(THREAD)
+       thread-groups 1
+    .endif
+
+    http-hardening other from untrusted
+       reject-lowercase-method off
+
+    defaults
+       mode http
+       timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
+       timeout client  "${HAPROXY_TEST_TIMEOUT-5s}"
+       timeout server  "${HAPROXY_TEST_TIMEOUT-5s}"
+
+    frontend fe
+       bind "fd@${fe}" http-hardening untrusted
+       http-request return status 200
+} -start
+
+haproxy h2 -cli {
+       send "show http-hardening"
+       expect ~ "^untrusted \\(implicit\\)\ntrusted \\(implicit\\)\nother\n"
+
+       send "show http-hardening trusted"
+       expect !~ " on\n"
+
+       send "show http-hardening untrusted"
+       expect ~ " 1 reject-duplicate-host-header +req +off\n 2 reject-lowercase-method +req +on\n 3 reject-underscore-in-header-name +- +off\n 4 reject-underscore-in-trailer-name +- +off\n 5 delete-underscore-in-header-name +- +off\n 6 delete-underscore-in-trailer-name +- +off\n 7 delete-unregistered-header-fields +- +off\n 8 delete-unregistered-trailer-fields +- +off\n 9 delete-all-trailer-fields +- +off\n10 reject-header-trailer-folding +- +on\n11 reject-ctl-in-header-trailer +- +on\n12 reject-content-length-with-te +- +on\n13 reject-content-length-leading-zero +- +on\n"
+
+       send "show http-hardening other"
+       expect ~ " 2 reject-lowercase-method +req +off\n(.*\n)*10 reject-header-trailer-folding +- +on\n"
+}
+
+#
+# Configuration errors
+#
+
+# profiles must be declared before being referenced
+haproxy h3 -conf-BAD {unknown http-hardening profile 'later'} {
+    defaults
+       mode http
+    frontend fe
+       bind "fd@${fe}" http-hardening later
+    http-hardening later
+       all on
+}
+
+haproxy h4 -conf-BAD {unknown http-hardening profile 'nope'} {
+    defaults
+       mode http
+    backend be
+       server s1 127.0.0.1:1 http-hardening nope
+}
+
+haproxy h5 -conf-BAD {'http-hardening' name 'auto' is reserved} {
+    http-hardening auto
+       all on
+}
+
+haproxy h6 -conf-BAD {has same name as another one} {
+    http-hardening dup
+       all on
+    http-hardening dup
+       all off
+}
+
+haproxy h7 -conf-BAD {cannot inherit from 'nope' (not found)} {
+    http-hardening p1 from nope
+       all on
+}
+
+haproxy h8 -conf-BAD {cannot inherit from itself} {
+    http-hardening trusted from trusted
+       all on
+}
+
+haproxy h9 -conf-BAD {Maybe you meant 'reject-lowercase-method'} {
+    http-hardening p1
+       reject-lowercase-meth on
+}
+
+haproxy h10 -conf-BAD {expects 'on' or 'off'} {
+    http-hardening p1
+       all yes
+}