mirror of https://github.com/status-im/consul.git
Obfuscates ACL tokens appearing in /v1/acl/<verb>/<token> APIs. (#3276)
* Obfuscates ACL tokens appearing in /v1/acl APIs. * Makes test positively identify the desired strings. * Adds an example and explanation of the regular expression.
This commit is contained in:
parent
83d9f0f688
commit
218ac4cb1e
|
@ -6,6 +6,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -166,6 +167,29 @@ func (s *HTTPServer) handler(enableDebug bool) http.Handler {
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// aclEndpointRE is used to find old ACL endpoints that take tokens in the URL
|
||||||
|
// so that we can redact them. The ACL endpoints that take the token in the URL
|
||||||
|
// are all of the form /v1/acl/<verb>/<token>, and can optionally include query
|
||||||
|
// parameters which are indicated by a question mark. We capture the part before
|
||||||
|
// the token, the token, and any query parameters after, and then reassemble as
|
||||||
|
// $1<hidden>$3 (the token in $2 isn't used), which will give:
|
||||||
|
//
|
||||||
|
// /v1/acl/clone/foo -> /v1/acl/clone/<hidden>
|
||||||
|
// /v1/acl/clone/foo?token=bar -> /v1/acl/clone/<hidden>?token=<hidden>
|
||||||
|
//
|
||||||
|
// The query parameter in the example above is obfuscated like any other, after
|
||||||
|
// this regular expression is applied, so the regular expression substitution
|
||||||
|
// results in:
|
||||||
|
//
|
||||||
|
// /v1/acl/clone/foo?token=bar -> /v1/acl/clone/<hidden>?token=bar
|
||||||
|
// ^---- $1 ----^^- $2 -^^-- $3 --^
|
||||||
|
//
|
||||||
|
// And then the loop that looks for parameters called "token" does the last
|
||||||
|
// step to get to the final redacted form.
|
||||||
|
var (
|
||||||
|
aclEndpointRE = regexp.MustCompile("^(/v1/acl/[^/]+/)([^?]+)([?]?.*)$")
|
||||||
|
)
|
||||||
|
|
||||||
// wrap is used to wrap functions to make them more convenient
|
// wrap is used to wrap functions to make them more convenient
|
||||||
func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Request) (interface{}, error)) http.HandlerFunc {
|
func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Request) (interface{}, error)) http.HandlerFunc {
|
||||||
return func(resp http.ResponseWriter, req *http.Request) {
|
return func(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
@ -189,6 +213,7 @@ func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Reque
|
||||||
logURL = strings.Replace(logURL, token, "<hidden>", -1)
|
logURL = strings.Replace(logURL, token, "<hidden>", -1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
logURL = aclEndpointRE.ReplaceAllString(logURL, "$1<hidden>$3")
|
||||||
|
|
||||||
if s.blacklist.Block(req.URL.Path) {
|
if s.blacklist.Block(req.URL.Path) {
|
||||||
errMsg := "Endpoint is blocked by agent configuration"
|
errMsg := "Endpoint is blocked by agent configuration"
|
||||||
|
@ -209,15 +234,6 @@ func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Reque
|
||||||
fmt.Fprint(resp, errMsg)
|
fmt.Fprint(resp, errMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO (slackpad) We may want to consider redacting prepared
|
|
||||||
// query names/IDs here since they are proxies for tokens. But,
|
|
||||||
// knowing one only gives you read access to service listings
|
|
||||||
// which is pretty trivial, so it's probably not worth the code
|
|
||||||
// complexity and overhead of filtering them out. You can't
|
|
||||||
// recover the token it's a proxy for with just the query info;
|
|
||||||
// you'd need the actual token (or a management token) to read
|
|
||||||
// that back.
|
|
||||||
|
|
||||||
// Invoke the handler
|
// Invoke the handler
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|
|
@ -331,16 +331,50 @@ func TestHTTP_wrap_obfuscateLog(t *testing.T) {
|
||||||
a.Start()
|
a.Start()
|
||||||
defer a.Shutdown()
|
defer a.Shutdown()
|
||||||
|
|
||||||
resp := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest("GET", "/some/url?token=secret1&token=secret2", nil)
|
|
||||||
handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
a.srv.wrap(handler)(resp, req)
|
|
||||||
|
|
||||||
// Make sure no tokens from the URL show up in the log
|
for _, pair := range [][]string{
|
||||||
if strings.Contains(buf.String(), "secret") {
|
{
|
||||||
t.Fatalf("bad: %s", buf.String())
|
"/some/url?token=secret1&token=secret2",
|
||||||
|
"/some/url?token=<hidden>&token=<hidden>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/v1/acl/clone/secret1",
|
||||||
|
"/v1/acl/clone/<hidden>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/v1/acl/clone/secret1?token=secret2",
|
||||||
|
"/v1/acl/clone/<hidden>?token=<hidden>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/v1/acl/destroy/secret1",
|
||||||
|
"/v1/acl/destroy/<hidden>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/v1/acl/destroy/secret1?token=secret2",
|
||||||
|
"/v1/acl/destroy/<hidden>?token=<hidden>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/v1/acl/info/secret1",
|
||||||
|
"/v1/acl/info/<hidden>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/v1/acl/info/secret1?token=secret2",
|
||||||
|
"/v1/acl/info/<hidden>?token=<hidden>",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
url, want := pair[0], pair[1]
|
||||||
|
t.Run(url, func(t *testing.T) {
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", url, nil)
|
||||||
|
a.srv.wrap(handler)(resp, req)
|
||||||
|
|
||||||
|
if got := buf.String(); !strings.Contains(got, want) {
|
||||||
|
t.Fatalf("got %s want %s", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue