Encode query semicolons

Co-authored-by: Romain <rtribotte@users.noreply.github.com>
This commit is contained in:
LandryBe 2023-06-15 18:20:06 +02:00 committed by GitHub
parent 6885e410f0
commit e62fe64ec9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 176 additions and 4 deletions

View file

@ -114,6 +114,9 @@ Trust only forwarded headers from selected IPs.
`--entrypoints.<name>.http`: `--entrypoints.<name>.http`:
HTTP configuration. HTTP configuration.
`--entrypoints.<name>.http.encodequerysemicolons`:
Defines whether request query semicolons should be URLEncoded. (Default: ```false```)
`--entrypoints.<name>.http.middlewares`: `--entrypoints.<name>.http.middlewares`:
Default middlewares for the routers linked to the entry point. Default middlewares for the routers linked to the entry point.

View file

@ -123,6 +123,9 @@ HTTP/3 configuration. (Default: ```false```)
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP3_ADVERTISEDPORT`: `TRAEFIK_ENTRYPOINTS_<NAME>_HTTP3_ADVERTISEDPORT`:
UDP port to advertise, on which HTTP/3 is available. (Default: ```0```) UDP port to advertise, on which HTTP/3 is available. (Default: ```0```)
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEQUERYSEMICOLONS`:
Defines whether request query semicolons should be URLEncoded. (Default: ```false```)
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_MIDDLEWARES`: `TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_MIDDLEWARES`:
Default middlewares for the routers linked to the entry point. Default middlewares for the routers linked to the entry point.

View file

@ -30,6 +30,7 @@
trustedIPs = ["foobar", "foobar"] trustedIPs = ["foobar", "foobar"]
[entryPoints.EntryPoint0.http] [entryPoints.EntryPoint0.http]
middlewares = ["foobar", "foobar"] middlewares = ["foobar", "foobar"]
encodeQuerySemicolons = true
[entryPoints.EntryPoint0.http.redirections] [entryPoints.EntryPoint0.http.redirections]
[entryPoints.EntryPoint0.http.redirections.entryPoint] [entryPoints.EntryPoint0.http.redirections.entryPoint]
to = "foobar" to = "foobar"

View file

@ -33,6 +33,7 @@ entryPoints:
- foobar - foobar
- foobar - foobar
http: http:
encodeQuerySemicolons: true
redirections: redirections:
entryPoint: entryPoint:
to: foobar to: foobar

View file

@ -833,6 +833,44 @@ This section is a convenience to enable (permanent) redirecting of all incoming
--entrypoints.foo.http.redirections.entrypoint.priority=10 --entrypoints.foo.http.redirections.entrypoint.priority=10
``` ```
### EncodeQuerySemicolons
_Optional, Default=false_
The `encodeQuerySemicolons` option allows to enable query semicolons encoding.
One could use this option to avoid non-encoded semicolons to be interpreted as query parameter separators by Traefik.
When using this option, the non-encoded semicolons characters in query will be transmitted encoded to the backend.
```yaml tab="File (YAML)"
entryPoints:
websecure:
address: ':443'
http:
encodeQuerySemicolons: true
```
```toml tab="File (TOML)"
[entryPoints.websecure]
address = ":443"
[entryPoints.websecure.http]
encodeQuerySemicolons = true
```
```bash tab="CLI"
--entrypoints.websecure.address=:443
--entrypoints.websecure.http.encodequerysemicolons=true
```
#### Examples
| EncodeQuerySemicolons | Request Query | Resulting Request Query |
|-----------------------|---------------------|-------------------------|
| false | foo=bar;baz=bar | foo=bar&baz=bar |
| true | foo=bar;baz=bar | foo=bar%3Bbaz=bar |
| false | foo=bar&baz=bar;foo | foo=bar&baz=bar&foo |
| true | foo=bar&baz=bar;foo | foo=bar&baz=bar%3Bfoo |
### Middlewares ### Middlewares
The list of middlewares that are prepended by default to the list of middlewares of each router associated to the named entry point. The list of middlewares that are prepended by default to the list of middlewares of each router associated to the named entry point.

View file

@ -0,0 +1,32 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
[entryPoints]
[entryPoints.web]
address = ":8000"
[entryPoints.encodeSemicolons]
address = ":8001"
[entryPoints.encodeSemicolons.http]
encodeQuerySemicolons = true
[api]
insecure = true
[providers.file]
filename = "{{ .SelfFilename }}"
## dynamic configuration ##
[http.routers]
[http.routers.router]
service = "service1"
rule = "Host(`other.localhost`)"
[http.services]
[http.services.service1.loadBalancer]
[[http.services.service1.loadBalancer.servers]]
url = "{{ .Server1 }}"

View file

@ -1412,3 +1412,71 @@ func (s *SimpleSuite) TestDebugLog(c *check.C) {
c.Fail() c.Fail()
} }
} }
func (s *SimpleSuite) TestEncodeSemicolons(c *check.C) {
s.createComposeProject(c, "base")
s.composeUp(c)
defer s.composeDown(c)
whoami1URL := "http://" + net.JoinHostPort(s.getComposeServiceIP(c, "whoami1"), "80")
file := s.adaptFile(c, "fixtures/simple_encode_semicolons.toml", struct {
Server1 string
}{whoami1URL})
defer os.Remove(file)
cmd, output := s.traefikCmd(withConfigFile(file))
defer output(c)
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer s.killCmd(cmd)
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`other.localhost`)"))
c.Assert(err, checker.IsNil)
testCases := []struct {
desc string
request string
target string
body string
expected int
}{
{
desc: "Transforming semicolons",
request: "GET /?bar=toto;boo=titi HTTP/1.1\r\nHost: other.localhost\r\n\r\n",
target: "127.0.0.1:8000",
expected: http.StatusOK,
body: "bar=toto&boo=titi",
},
{
desc: "Encoding semicolons",
request: "GET /?bar=toto&boo=titi;aaaa HTTP/1.1\r\nHost: other.localhost\r\n\r\n",
target: "127.0.0.1:8001",
expected: http.StatusOK,
body: "bar=toto&boo=titi%3Baaaa",
},
}
for _, test := range testCases {
conn, err := net.Dial("tcp", test.target)
c.Assert(err, checker.IsNil)
_, err = conn.Write([]byte(test.request))
c.Assert(err, checker.IsNil)
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
c.Assert(err, checker.IsNil)
if resp.StatusCode != test.expected {
c.Errorf("%s failed with %d instead of %d", test.desc, resp.StatusCode, test.expected)
}
if test.body != "" {
body, err := io.ReadAll(resp.Body)
c.Assert(err, checker.IsNil)
c.Assert(string(body), checker.Contains, test.body)
}
}
}

View file

@ -60,6 +60,7 @@ type HTTPConfig struct {
Redirections *Redirections `description:"Set of redirection" json:"redirections,omitempty" toml:"redirections,omitempty" yaml:"redirections,omitempty" export:"true"` Redirections *Redirections `description:"Set of redirection" json:"redirections,omitempty" toml:"redirections,omitempty" yaml:"redirections,omitempty" export:"true"`
Middlewares []string `description:"Default middlewares for the routers linked to the entry point." json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"` Middlewares []string `description:"Default middlewares for the routers linked to the entry point." json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
TLS *TLSConfig `description:"Default TLS configuration for the routers linked to the entry point." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` TLS *TLSConfig `description:"Default TLS configuration for the routers linked to the entry point." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
EncodeQuerySemicolons bool `description:"Defines whether request query semicolons should be URLEncoded." json:"encodeQuerySemicolons,omitempty" toml:"encodeQuerySemicolons,omitempty" yaml:"encodeQuerySemicolons,omitempty"`
} }
// HTTP2Config is the HTTP2 configuration of an entry point. // HTTP2Config is the HTTP2 configuration of an entry point.

View file

@ -535,7 +535,11 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati
return nil, err return nil, err
} }
if configuration.HTTP.EncodeQuerySemicolons {
handler = encodeQuerySemicolons(handler)
} else {
handler = http.AllowQuerySemicolons(handler) handler = http.AllowQuerySemicolons(handler)
}
if withH2c { if withH2c {
handler = h2c.NewHandler(handler, &http2.Server{ handler = h2c.NewHandler(handler, &http2.Server{
@ -596,3 +600,23 @@ func (t *trackedConnection) Close() error {
t.tracker.RemoveConnection(t.WriteCloser) t.tracker.RemoveConnection(t.WriteCloser)
return t.WriteCloser.Close() return t.WriteCloser.Close()
} }
// This function is inspired by http.AllowQuerySemicolons.
func encodeQuerySemicolons(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if strings.Contains(req.URL.RawQuery, ";") {
r2 := new(http.Request)
*r2 = *req
r2.URL = new(url.URL)
*r2.URL = *req.URL
r2.URL.RawQuery = strings.ReplaceAll(req.URL.RawQuery, ";", "%3B")
// Because the reverse proxy director is building query params from requestURI it needs to be updated as well.
r2.RequestURI = r2.URL.RequestURI()
h.ServeHTTP(rw, r2)
} else {
h.ServeHTTP(rw, req)
}
})
}

View file

@ -48,6 +48,7 @@ func buildProxy(passHostHeader *bool, responseForwarding *dynamic.ResponseForwar
outReq.URL.Path = u.Path outReq.URL.Path = u.Path
outReq.URL.RawPath = u.RawPath outReq.URL.RawPath = u.RawPath
// If a plugin/middleware adds semicolons in query params, they should be urlEncoded.
outReq.URL.RawQuery = strings.ReplaceAll(u.RawQuery, ";", "&") outReq.URL.RawQuery = strings.ReplaceAll(u.RawQuery, ";", "&")
outReq.RequestURI = "" // Outgoing request should not have RequestURI outReq.RequestURI = "" // Outgoing request should not have RequestURI