Hi Willy,

Sorry about that, attached as a file this time.

Thanks,
Mani

On Thu, 13 Aug 2026 at 11:19, Willy Tarreau <[email protected]> wrote:
>
> Hi Mani,
>
> On Thu, Aug 13, 2026 at 10:52:37AM +0530, Mani Goyal wrote:
> > Thanks Bob and Willy for the review, good catch on '#'.
> >
> > I'm not currently receiving list mail (my subscription confirmation
> > never arrived, even checked spam), so please keep me in Cc on replies
> > for now.
> >
> > v2 adds '#' as a third authority terminator alongside '/' and '?', plus
> > a regression test for it (it needed its own frontend with "option
> > accept-unsafe-violations-in-http-request" since a raw '#' is rejected
> > by the request-line parser otherwise).
>
> Thanks. However, as you can see below, your mailer corrupted the patch
> by replacing tabs with spaces. Please just resend it attached and we'll
> take care of it.
>
> thank you!
> willy
>
> >
> >
> >
> > >From a18590dd49ec4cea2329db36fdf342e2f9e352d9 Mon Sep 17 00:00:00 2001
> > From: Mani Goyal <[email protected]>
> > Date: Wed, 12 Aug 2026 12:26:22 +0530
> > Subject: [PATCH] BUG/MEDIUM: http: fix authority parsing for absolute-form
> >  URI with empty path
> >
> > http_parse_authority() only stopped scanning at '/', not '?' or '#'.
> > For an absolute-form request-target with no path but a query string
> > (e.g. "http://host?token=...";), the authority scan ran to the end of
> > the URI and swallowed the query string into the authority. This
> > caused http_scheme_based_normalize() to see an empty path and append
> > '/' after the query string instead of between the host and the
> > query, corrupting the request on the wire. The same corruption
> > happens with a literal '#' in the request-target when HTTP
> > violations are tolerated (option accept-unsafe-violations-in-http-
> > request), since it is not rejected by the request-line parser in
> > that mode either.
> >
> > Per RFC 3986 section 3.2, authority terminates at '/', '?', or '#',
> > or at the end of the URI, so also stop at these two delimiters.
> >
> > Add cases to h1_host_normalization.vtc covering an empty path with a
> > query string, with and without a port needing normalization. Add a
> > new h1_authority_fragment_char.vtc covering the '#' terminator
> > specifically, since it requires accept-unsafe-violations-in-http-
> > request to reach the parser at all and doesn't belong in the shared
> > frontend used by the other host-normalization cases.
> >
> > This should be backported to all stable versions.
> >
> > Should fix issue #3460.
> > ---
> >  .../h1_authority_fragment_char.vtc            | 72 +++++++++++++++++++
> >  .../http-messaging/h1_host_normalization.vtc  | 38 ++++++++++
> >  src/http.c                                    |  4 +-
> >  3 files changed, 112 insertions(+), 2 deletions(-)
> >  create mode 100644 reg-tests/http-messaging/h1_authority_fragment_char.vtc
> >
> > diff --git a/reg-tests/http-messaging/h1_authority_fragment_char.vtc
> > b/reg-tests/http-messaging/h1_authority_fragment_char.vtc
> > new file mode 100644
> > index 000000000..d43c7a38e
> > --- /dev/null
> > +++ b/reg-tests/http-messaging/h1_authority_fragment_char.vtc
> > @@ -0,0 +1,72 @@
> > +varnishtest "H1 absolute-form URI whose authority is terminated by
> > '#' (rfc3986 3.2)"
> > +
> > +feature cmd "$HAPROXY_PROGRAM -cc 'version_atleast(2.6-dev0)'"
> > +feature ignore_unknown_macro
> > +
> > +barrier b1 cond 2 -cyclic
> > +
> > +syslog S1 -level info {
> > + # C1
> > + recv
> > + expect ~ "^.* uri: GET http://hostname#23frag HTTP/1.1; host: {hostname}$"
> > + barrier b1 sync
> > +
> > + # C2
> > + recv
> > + expect ~ "^.* uri: GET http://hostname/abc#23frag HTTP/1.1; host: 
> > {hostname}$"
> > +} -start
> > +
> > +haproxy h1 -conf {
> > +    global
> > +    .if feature(THREAD)
> > +        thread-groups 1
> > +    .endif
> > +
> > + 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}"
> > +
> > + # a raw '#' in the request-target is rejected by default; relax this
> > + # so the case reaches authority parsing.
> > + option accept-unsafe-violations-in-http-request
> > +
> > + http-request capture req.hdr(host) len 512
> > + log-format "uri: %r; host: %hr"
> > + log ${S1_addr}:${S1_port} len 2048 local0 debug err
> > +
> > + http-request return status 200
> > +} -start
> > +
> > +# no path, authority terminated by '#' rather than '/' or '?' => must not 
> > be
> > +# absorbed into the authority, and the uri must be passed through untouched
> > +client c1 -connect ${h1_fe_sock} {
> > + txreq \
> > +  -req "GET" \
> > +  -url "http://hostname#frag"; \
> > +  -hdr "host: hostname"
> > +
> > + rxresp
> > + expect resp.status == 200
> > +} -run
> > +
> > +# Wait matching on log message
> > +barrier b1 sync
> > +
> > +# a path is present before the '#', so authority is terminated by '/' as 
> > usual;
> > +# the trailing fragment must be preserved as part of the path
> > +client c2 -connect ${h1_fe_sock} {
> > + txreq \
> > +  -req "GET" \
> > +  -url "http://hostname/abc#frag"; \
> > +  -hdr "host: hostname"
> > +
> > + rxresp
> > + expect resp.status == 200
> > +} -run
> > +
> > +syslog S1 -wait
> > diff --git a/reg-tests/http-messaging/h1_host_normalization.vtc
> > b/reg-tests/http-messaging/h1_host_normalization.vtc
> > index 0d6112705..048ad95bd 100644
> > --- a/reg-tests/http-messaging/h1_host_normalization.vtc
> > +++ b/reg-tests/http-messaging/h1_host_normalization.vtc
> > @@ -231,6 +231,16 @@ syslog S1 -level info {
> >      # C43
> >      recv
> >      expect ~ "^.* uri: GET https://hostname:444/ HTTP/1.1; host:
> > {hostname:444}$"
> > +    barrier b1 sync
> > +
> > +    # C44
> > +    recv
> > +    expect ~ "^.* uri: GET http://hostname\\?a=b HTTP/1.1; host: 
> > {hostname}$"
> > +    barrier b1 sync
> > +
> > +    # C45
> > +    recv
> > +    expect ~ "^.* uri: GET http://hostname\\?c=d HTTP/1.1; host: 
> > {hostname}$"
> >
> >  } -start
> >
> > @@ -908,4 +918,32 @@ client c43 -connect ${h1_fe_sock} {
> >      expect resp.status == 200
> >  } -run
> >
> > +# Wait matching on log message
> > +barrier b1 sync
> > +
> > +# empty path with a query string, no port => query must be preserved
> > +client c44 -connect ${h1_fe_sock} {
> > +    txreq \
> > +      -req "GET" \
> > +      -url "http://hostname?a=b"; \
> > +      -hdr "host: hostname"
> > +
> > +    rxresp
> > +    expect resp.status == 200
> > +} -run
> > +
> > +# Wait matching on log message
> > +barrier b1 sync
> > +
> > +# empty path with a query string and a default port => no stray '/' after 
> > query
> > +client c45 -connect ${h1_fe_sock} {
> > +    txreq \
> > +      -req "GET" \
> > +      -url "http://hostname:80?c=d"; \
> > +      -hdr "host: hostname:80"
> > +
> > +    rxresp
> > +    expect resp.status == 200
> > +} -run
> > +
> >  syslog S1 -wait
> > diff --git a/src/http.c b/src/http.c
> > index e7e78300d..c29f77572 100644
> > --- a/src/http.c
> > +++ b/src/http.c
> > @@ -682,12 +682,12 @@ struct ist http_parse_authority(struct
> > http_uri_parser *parser, int no_userinfo)
> >   ptr = start = istptr(parser->uri);
> >   end = istend(parser->uri);
> >
> > - while (ptr < end && *ptr != '/') {
> > + while (ptr < end && *ptr != '/' && *ptr != '?' && *ptr != '#') {
> >   if (*ptr++ == '@' && no_userinfo)
> >   start = ptr;
> >   }
> >
> > - /* OK, ptr point on the '/' or the end */
> > + /* OK, ptr point on the '/', the '?', the '#' or the end */
> >
> >    authority:
> >   parser->uri = ist2(ptr, end - ptr);
> > --
> > 2.34.1
> >

Attachment: 0001-BUG-MEDIUM-http-fix-authority-parsing-for-absolute-f.patch
Description: Binary data

Reply via email to