8000 If the HTTP/2 `:authority` contains `/`, HAProxy treats the part after `/` as part of the path · Issue #2941 · haproxy/haproxy · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

If the HTTP/2 :authority contains /, HAProxy treats the part after / as part of the path #2941

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
DemiMarie opened this issue Apr 18, 2025 · 11 comments
Labels
3.0 This issue affects the HAProxy 3.0 stable branch. 3.1 This issue affects the HAProxy 3.1 stable branch. status: fixed This issue is a now-fixed bug. type: bug This issue describes a bug.

Comments

@DemiMarie
Copy link
DemiMarie commented Apr 18, 2025

Detailed Description of the Problem

If I send an HTTP/2 request to HAProxy with a :authority pseudo-header that contains /, HAProxy prepends the part including and after the first / to the :path pseudo-header. If an upstream proxy performs path-based access control but does not validate :authority, these checks could be bypassed.

Expected Behavior

HAProxy should reject the request because / is not allowed in :authority pseudo-headers.

Steps to Reproduce the Behavior

  1. Send an HTTP/2 request to HAProxy with :authority: a/b.
  2. Log what the upstream server receives.

Do you have any idea what may have caused this?

HAProxy forms a request URI by string concatenation, then re-parses it to determine the path to send upstream for HTTP/1.x requests. This is only safe if parsing the URI will result in the same path that was used to form the URI, but in this case it does not.

Do you have an idea how to solve the issue?

HAProxy should strictly validate the :authority and Host headers. In particular, it should reject:

These rules come from Envoy (https://envoyproxy.io).

What is your configuration?

global
   log stderr local0 debug
   chroot /run/haproxy
   maxconn 4000
   limited-quic
   user haproxy
   group haproxy
   stats socket /run/haproxy/haproxy-stats
   ssl-default-bind-curves X25519:P-256:P-384
   ssl-default-bind-ciphersuites TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256
   ssl-default-bind-options prefer-client-ciphers ssl-min-ver TLSv1.3
   unix-bind prefix /run/haproxy user haproxy mode 0660 group qubes
   setcap cap_net_bind_service
defaults
   log global
   timeout client 3000ms
   timeout connect 3000ms
   timeout server 3000ms
crt-store
   load key '/usr/local/etc/haproxy.key' crt /usr/local/etc/haproxy.pem alias 'main'
frontend main
   mode http
   default_backend app
   bind quic4@*:443 ssl crt "@/main" name main_quic_v4
   bind quic6@*:443 ssl crt "@/main" name main_quic_v6
   bind       *:443 ssl crt "@/main" name main_tls
   option httplog
   http-request deny status 400 unless { path_beg '/test/' }
frontend local_socket
   bind /reroute
   mode http
   default_backend trivial
   option httplog
backend app
   mode http
   server reroute /reroute proto h2
backend trivial
   mode http
   server simple /launch proto h1
   option http-server-close

Output of haproxy -vv

HAProxy version 3.1.7 2025/04/17 - https://haproxy.org/
Status: stable branch - will stop receiving fixes around Q1 2026.
Known bugs: http://www.haproxy.org/bugs/bugs-3.1.7.html
Running on: Linux 6.12.21-1.qubes.fc37.x86_64 #1 SMP PREEMPT_DYNAMIC Mon Mar 31 11:38:40 GMT 2025 x86_64
Build options :
  TARGET  = linux-glibc
  CC      = cc
  CFLAGS  = -O2 -g -fwrapv
  OPTIONS = USE_PTHREAD_EMULATION=0 USE_OPENSSL=1 USE_LUA=1 USE_QUIC=1 USE_PCRE2=1 USE_PCRE2_JIT=1 USE_QUIC_OPENSSL_COMPAT=1
  DEBUG   = 

Feature list : -51DEGREES +ACCEPT4 +BACKTRACE -CLOSEFROM +CPU_AFFINITY +CRYPT_H -DEVICEATLAS +DL -ENGINE +EPOLL -EVPORTS +GETADDRINFO -KQUEUE -LIBATOMIC +LIBCRYPT +LINUX_CAP +LINUX_SPLICE +LINUX_TPROXY +LUA +MATH -MEMORY_PROFILING +NETFILTER +NS -OBSOLETE_LINKER +OPENSSL -OPENSSL_AWSLC -OPENSSL_WOLFSSL -OT -PCRE +PCRE2 +PCRE2_JIT -PCRE_JIT +POLL +PRCTL -PROCCTL -PROMEX -PTHREAD_EMULATION +QUIC +QUIC_OPENSSL_COMPAT +RT +SHM_OPEN +SLZ +SSL -STATIC_PCRE -STATIC_PCRE2 +TFO +THREAD +THREAD_DUMP +TPROXY -WURFL -ZLIB

Default settings :
  bufsize = 16384, maxrewrite = 1024, maxpollevents = 200

Built with multi-threading support (MAX_TGROUPS=16, MAX_THREADS=256, default=2).
Built with OpenSSL version : OpenSSL 3.2.4 11 Feb 2025
Running on OpenSSL version : OpenSSL 3.2.4 11 Feb 2025
OpenSSL library supports TLS extensions : yes
OpenSSL library supports SNI : yes
OpenSSL library supports : TLSv1.0 TLSv1.1 TLSv1.2 TLSv1.3
OpenSSL providers loaded : default
Built with Lua version : Lua 5.4.7
Built with network namespace support.
Built with libslz for stateless compression.
Compression algorithms supported : identity("identity"), deflate("deflate"), raw-deflate("deflate"), gzip("gzip")
Built with transparent proxy support using: IP_TRANSPARENT IPV6_TRANSPARENT IP_FREEBIND
Built with PCRE2 version : 10.44 2024-06-07
PCRE2 library supports JIT : yes
Encrypted password support via crypt(3): yes
Built with gcc compiler version 14.2.1 20250110 (Red Hat 14.2.1-7)

Available polling systems :
      epoll : pref=300,  test result OK
       poll : pref=200,  test result OK
     select : pref=150,  test result OK
Total: 3 (3 usable), will use epoll.

Available multiplexer protocols :
(protocols marked as <default> cannot be specified using 'proto' keyword)
       quic : mode=HTTP  side=FE     mux=QUIC  flags=HTX|NO_UPG|FRAMED
         h2 : mode=HTTP  side=FE|BE  mux=H2    flags=HTX|HOL_RISK|NO_UPG
  <default> : mode=HTTP  side=FE|BE  mux=H1    flags=HTX
         h1 : mode=HTTP  side=FE|BE  mux=H1    flags=HTX|NO_UPG
       fcgi : mode=HTTP  side=BE     mux=FCGI  flags=HTX|HOL_RISK|NO_UPG
  <default> : mode=SPOP  side=BE     mux=SPOP  flags=HOL_RISK|NO_UPG
       spop : mode=SPOP  side=BE     mux=SPOP  flags=HOL_RISK|NO_UPG
  <default> : mode=TCP   side=FE|BE  mux=PASS  flags=
       none : mode=TCP   side=FE|BE  mux=PASS  flags=NO_UPG

Available services : none

Available filters :
	[BWLIM] bwlim-in
	[BWLIM] bwlim-out
	[CACHE] cache
	[COMP] compression
	[FCGI] fcgi-app
	[SPOE] spoe
	[TRACE] trace

Last Outputs and Backtraces


Additional Information

This should also reproduce with HTTP/3 requests in the development branch. Envoy Proxy has a readable implementation of the above-mentioned validation logic in https://github.com/envoyproxy/envoy/blob/72fd99b8c3681e413eb40ecaebfb1c3454810763/source/extensions/http/header_validators/envoy_default/header_validator.cc, but this is licensed under Apache 2.0 and is in C++.

@DemiMarie DemiMarie 8000 added status: needs-triage This issue needs to be triaged. type: bug This issue describes a bug. labels Apr 18, 2025
@wtarreau
Copy link
Member

Interesting, thanks for reporting this. Today we're busy with releases but I'll have a look at this next week.
At first glance, given that we're recomposing the URI, I guess that filtering will happen on the recombined path, so normally haproxy and the origin server will see the same thing. The only issue would be if a client-side proxy would perform filtering on the path but would also let this :authority pass through. Regardless it deserves being cleaned up. FWIW we've started the work of merging the h2/h3 header processing, but it will take quite a bit of time (for 3.3 probably). For 3.2 we're at least making sure to align them.

@DemiMarie
Copy link
Author

That indeed matches my observations. IPv6 address validation is tricky but I recommend doing it, if for no other reason than to match the Envoy proxy. A backend proxy not being consistent with what the frontend proxy does can cause problems, such as unexpected Bad Gateway errors being returned for invalid requests.

@wtarreau
Copy link
Member

Please note, we take great efforts to interact the least possible with the request or response because there is a wide gap between what is documented as "deprecated" in recent standards and what is found in the field. Every single time we strenghthen a protocol element, we get reports about breakage of legacy applications. The HTTP ecosystem is not just the web with browsers, there are many devices out there, from payment terminals to backup systems, passing by arrival displays in train stations. When such applications respect more or less RFC2616 or at least 2068, you're happy. For example I remember that the userinfo part was still used for FTP transactions encapsulated over HTTP in certain finance and documentation systems.

The difficulty with a gateway (and particularly a load balancer) is that people insert it in places where old applications are having difficulties scaling or interoperating with the rest of the world, and purposely breaking them because we decide that their exchanges are not legitimate is not well received. So we try hard to focus only on what we use and not as much on what we see. If users want to use haproxy to increase security at the expense of interoperability, that's perfectly feasible and done by many, but via the http-request rules.

So in the case above, the real problem is the '/' in :authority (just as the space once had been), because it affects the way the URI is recomposed when the elements are re-assembled. For the IPv6 address, whether an invalid one is blocked by haproxy or the server results in the same for the client, and if the feat is that the server poorly validates it, then it's possible to write rules to address various servers' limitations.

@DemiMarie
Copy link
Author

That makes sense! I suspect that Envoy is generally not used with such legacy applications, which means that it can get away with stricter validation.

@wtarreau
Copy link
Member

Yes, also being used as a side car generally implies modern clients at least. Also it has more to defend against developer bugs and has a bigger benefit in immediately rejecting the slightest non-compliance to force developers to fix their code before deployment. In our case, the clients are everywhere all over the net and we can't fix them.

@DemiMarie
Copy link
Author

In the case of :authority, should only ?, /, #, and anything not in (0x21, 0x7E) be rejected? Or is there anything else that makes sense to reject?

@wtarreau
Copy link
Member

Yeah I agree that it should be OK.

@DemiMarie
Copy link
Author

What about misplaced [, ], and other URI delimiters? Should those be rejected or allowed?

@wtarreau
Copy link
Member

Still thinking about it. I'd hate to spend cpu cycles on these ones for most requests, but we could pass via a slow parser if we process [] as an exception as well.

@DemiMarie
Copy link
Author

I would add \ to the list of rejected characters. The WHATWG URL spec considers the host to end with any of ?, /, #, and \. There are other characters that might be valid if they come before an @, but the four I mentioned are definitely invalid.

haproxy-mirror pushed a commit that referenced this issue May 12, 2025
As discussed here:
  httpwg/http2-spec#936
  #2941

It's important to take care of some special characters in the :authority
pseudo header before reassembling a complete URI, because after assembly
it's too late (e.g. the '/').

This patch adds a specific function which was checks all such characters
and their ranges on an ist, and benefits from modern compilers
optimizations that arrange the comparisons into an evaluation tree for
faster match. That's the version that gave the most consistent performance
across various compilers, though some hand-crafted versions using bitmaps
stored in register could be slightly faster but super sensitive to code
ordering, suggesting that the results might vary with future compilers.
This one takes on average 1.2ns per character at 3 GHz (3.6 cycles per
char on avg). The resulting impact on H2 request processing time (small
requests) was measured around 0.3%, from 6.60 to 6.618us per request,
which is a bit high but remains acceptable given that the test only
focused on req rate.

The code was made usable both for H2 and H3.
haproxy-mirror pushed a commit that referenced this issue May 12, 2025
…eassembly

As discussed here:
   httpwg/http2-spec#936
   #2941

It's important to take care of some special characters in the :authority
pseudo header before reassembling a complete URI, because after assembly
it's too late (e.g. the '/'). This patch does this, both for h2 and h3.

The impact on H2 was measured in the worst case at 0.3% of the request
rate, while the impact on H3 is around 1%, but H3 was about 1% faster
than H2 before and is now on par.

It may be backported after a period of observation, and in this case it
relies on this previous commit:

   MINOR: http: add a function to validate characters of :authority

Thanks to @DemiMarie for reviving this topic in issue #2941 and bringing
new potential interesting cases.
@wtarreau
Copy link
Member

Hi @DemiMarie
I've now committed this. So we're rejecting <0x21, >0x7e, '@', '?', '/', '#'. We're also checking that '[' if present is exactly the first char, that ']' if present is exactly last or preceeded by a colon, and that we have either none of them or one of each. The perf impact is measurable (1.2ns per char, ~0.3% H2, ~1% H3 for short requests), that's never pleasant, but I think that we're on a sweet spot here where we make sure that the reassembled block is non-ambiguous, which was the point. We'll let it cook for a while in 3.2 before progressively backporting it. Thanks again for your insights on this one!

@wtarreau wtarreau added status: fixed This issue is a now-fixed bug. 3.0 This issue affects the HAProxy 3.0 stable branch. 3.1 This issue affects the HAProxy 3.1 stable branch. and removed status: needs-triage This issue needs to be triaged. labels May 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.0 This issue affects the HAProxy 3.0 stable branch. 3.1 This issue affects the HAProxy 3.1 stable branch. status: fixed This issue is a now-fixed bug. type: bug This issue describes a bug.
Projects
None yet
Development

No branches or pull requests

2 participants
0