Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlejandro Rodríguez <alejorro70@gmail.com>2018-03-13 05:18:31 +0300
committerAlejandro Rodríguez <alejorro70@gmail.com>2018-03-13 05:18:31 +0300
commit6394204634377e7ec168292de9f6b04885b52bba (patch)
treeb3b94e5bc67398aefb5befef7d7fc596db639e58
parent43e074e155dac96f96b30f028aa6c194f89d3e35 (diff)
parentcd8b33b00fe04821568deff64aacfe5b62818287 (diff)
Merge branch 'fix/credentials-leakage' into 'master'
Sanitize URLs before logging them Closes #1052 See merge request gitlab-org/gitaly!624
-rw-r--r--CHANGELOG.md5
-rw-r--r--internal/logsanitizer/url.go83
-rw-r--r--internal/logsanitizer/url_test.go128
-rw-r--r--internal/server/server.go13
4 files changed, 228 insertions, 1 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8f0aff489..1a24afd61 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
# Gitaly changelog
+UNRELEASED
+
+- Sanitize URLs before logging them
+ https://gitlab.com/gitlab-org/gitaly/merge_requests/624
+
v0.89.0
- Report original exceptions to Sentry instead of wrapped ones by the exception bridge
diff --git a/internal/logsanitizer/url.go b/internal/logsanitizer/url.go
new file mode 100644
index 000000000..ecebe6bdb
--- /dev/null
+++ b/internal/logsanitizer/url.go
@@ -0,0 +1,83 @@
+package logsanitizer
+
+import (
+ "regexp"
+
+ "github.com/sirupsen/logrus"
+)
+
+// Pattern taken from Regular Expressions Cookbook, slightly modified though
+// |Scheme |User |Named/IPv4 host|IPv6+ host
+var hostPattern = regexp.MustCompile(`(?i)([a-z][a-z0-9+\-.]*://)([a-z0-9\-._~%!$&'()*+,;=:]+@)([a-z0-9\-._~%]+|\[[a-z0-9\-._~%!$&'()*+,;=:]+\])`)
+
+// URLSanitizerHook stores which gRPC methods to perform sanitization for.
+type URLSanitizerHook struct {
+ // gRPC methods that are likely to have URLs to sanitize
+ possibleGrpcMethods map[string]bool
+}
+
+// NewURLSanitizerHook returns a new logrus hook for sanitizing URLs.
+func NewURLSanitizerHook() *URLSanitizerHook {
+ return &URLSanitizerHook{possibleGrpcMethods: make(map[string]bool)}
+}
+
+// AddPossibleGrpcMethod adds method names that we should sanitize URLs from their logs.
+func (hook *URLSanitizerHook) AddPossibleGrpcMethod(methods ...string) {
+ for _, method := range methods {
+ hook.possibleGrpcMethods[method] = true
+ }
+}
+
+// Fire is called by logrus.
+func (hook *URLSanitizerHook) Fire(entry *logrus.Entry) error {
+ mth, ok := entry.Data["grpc.method"]
+ if !ok {
+ return nil
+ }
+
+ mthStr, ok := mth.(string)
+ if !ok || !hook.possibleGrpcMethods[mthStr] {
+ return nil
+ }
+
+ if _, ok := entry.Data["args"]; ok {
+ sanitizeSpawnLog(entry)
+ } else if _, ok := entry.Data["error"]; ok {
+ sanitizeErrorLog(entry)
+ } else {
+ entry.Message = sanitizeString(entry.Message)
+ }
+
+ return nil
+}
+
+// Levels is called by logrus.
+func (hook *URLSanitizerHook) Levels() []logrus.Level {
+ return logrus.AllLevels
+}
+
+func sanitizeSpawnLog(entry *logrus.Entry) {
+ args, ok := entry.Data["args"].([]string)
+ if !ok {
+ return
+ }
+
+ for i, arg := range args {
+ args[i] = sanitizeString(arg)
+ }
+
+ entry.Data["args"] = args
+}
+
+func sanitizeErrorLog(entry *logrus.Entry) {
+ err, ok := entry.Data["error"].(error)
+ if !ok {
+ return
+ }
+
+ entry.Data["error"] = sanitizeString(err.Error())
+}
+
+func sanitizeString(str string) string {
+ return hostPattern.ReplaceAllString(str, "$1[FILTERED]@$3$4")
+}
diff --git a/internal/logsanitizer/url_test.go b/internal/logsanitizer/url_test.go
new file mode 100644
index 000000000..b3fc116fd
--- /dev/null
+++ b/internal/logsanitizer/url_test.go
@@ -0,0 +1,128 @@
+package logsanitizer
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "testing"
+
+ log "github.com/sirupsen/logrus"
+ "github.com/stretchr/testify/require"
+)
+
+func TestUrlSanitizerHook(t *testing.T) {
+ outBuf := &bytes.Buffer{}
+
+ urlSanitizer := NewURLSanitizerHook()
+ urlSanitizer.AddPossibleGrpcMethod(
+ "UpdateRemoteMirror",
+ "CreateRepositoryFromURL",
+ )
+
+ logger := log.New()
+ logger.Out = outBuf
+ logger.Hooks.Add(urlSanitizer)
+
+ testCases := []struct {
+ desc string
+ logFunc func()
+ expectedString string
+ }{
+ {
+ desc: "with args",
+ logFunc: func() {
+ logger.WithFields(log.Fields{
+ "grpc.method": "CreateRepositoryFromURL",
+ "args": []string{"/usr/bin/git", "clone", "--bare", "--", "https://foo_the_user:hUntEr1@gitlab.com/foo/bar", "/home/git/repositories/foo/bar"},
+ }).Info("spawn")
+ },
+ expectedString: "[/usr/bin/git clone --bare -- https://[FILTERED]@gitlab.com/foo/bar /home/git/repositories/foo/bar]",
+ },
+ {
+ desc: "with error",
+ logFunc: func() {
+ logger.WithFields(log.Fields{
+ "grpc.method": "UpdateRemoteMirror",
+ "error": fmt.Errorf("rpc error: code = Unknown desc = remote: Invalid username or password. fatal: Authentication failed for 'https://foo_the_user:hUntEr1@gitlab.com/foo/bar'"),
+ }).Error("ERROR")
+ },
+ expectedString: "rpc error: code = Unknown desc = remote: Invalid username or password. fatal: Authentication failed for 'https://[FILTERED]@gitlab.com/foo/bar'",
+ },
+ {
+ desc: "with message",
+ logFunc: func() {
+ logger.WithFields(log.Fields{
+ "grpc.method": "CreateRepositoryFromURL",
+ }).Info("asked for: https://foo_the_user:hUntEr1@gitlab.com/foo/bar")
+ },
+ expectedString: "asked for: https://[FILTERED]@gitlab.com/foo/bar",
+ },
+ {
+ desc: "with gRPC method not added to the list",
+ logFunc: func() {
+ logger.WithFields(log.Fields{
+ "grpc.method": "UserDeleteTag",
+ }).Error("fatal: 'https://foo_the_user:hUntEr1@gitlab.com/foo/bar' is not a valid tag name.")
+ },
+ expectedString: "fatal: 'https://foo_the_user:hUntEr1@gitlab.com/foo/bar' is not a valid tag name.",
+ },
+ {
+ desc: "log with URL that does not require sanitization",
+ logFunc: func() {
+ logger.WithFields(log.Fields{
+ "grpc.method": "CreateRepositoryFromURL",
+ }).Info("asked for: https://gitlab.com/gitlab-org/gitaly")
+ },
+ expectedString: "asked for: https://gitlab.com/gitlab-org/gitaly",
+ },
+ }
+
+ for _, testCase := range testCases {
+ t.Run(testCase.desc, func(t *testing.T) {
+ testCase.logFunc()
+ logOutput := outBuf.String()
+
+ require.Contains(t, logOutput, testCase.expectedString)
+ })
+ }
+}
+
+func BenchmarkUrlSanitizerWithoutSanitization(b *testing.B) {
+ urlSanitizer := NewURLSanitizerHook()
+
+ logger := log.New()
+ logger.Out = ioutil.Discard
+ logger.Hooks.Add(urlSanitizer)
+
+ benchmarkLogging(logger, b)
+}
+
+func BenchmarkUrlSanitizerWithSanitization(b *testing.B) {
+ urlSanitizer := NewURLSanitizerHook()
+ urlSanitizer.AddPossibleGrpcMethod(
+ "UpdateRemoteMirror",
+ "CreateRepositoryFromURL",
+ )
+
+ logger := log.New()
+ logger.Out = ioutil.Discard
+ logger.Hooks.Add(urlSanitizer)
+
+ benchmarkLogging(logger, b)
+}
+
+func benchmarkLogging(logger *log.Logger, b *testing.B) {
+ for n := 0; n < b.N; n++ {
+ logger.WithFields(log.Fields{
+ "grpc.method": "CreateRepositoryFromURL",
+ "args": []string{"/usr/bin/git", "clone", "--bare", "--", "https://foo_the_user:hUntEr1@gitlab.com/foo/bar", "/home/git/repositories/foo/bar"},
+ }).Info("spawn")
+ logger.WithFields(log.Fields{
+ "grpc.method": "UpdateRemoteMirror",
+ "error": fmt.Errorf("rpc error: code = Unknown desc = remote: Invalid username or password. fatal: Authentication failed for 'https://foo_the_user:hUntEr1@gitlab.com/foo/bar'"),
+ }).Error("ERROR")
+ logger.WithFields(log.Fields{
+ "grpc.method": "CreateRepositoryFromURL",
+ }).Info("asked for: https://foo_the_user:hUntEr1@gitlab.com/foo/bar")
+ }
+}
diff --git a/internal/server/server.go b/internal/server/server.go
index 4fa980be4..eb5decb8d 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -4,6 +4,7 @@ import (
log "github.com/sirupsen/logrus"
"gitlab.com/gitlab-org/gitaly/internal/helper/fieldextractors"
+ "gitlab.com/gitlab-org/gitaly/internal/logsanitizer"
"gitlab.com/gitlab-org/gitaly/internal/middleware/cancelhandler"
"gitlab.com/gitlab-org/gitaly/internal/middleware/limithandler"
"gitlab.com/gitlab-org/gitaly/internal/middleware/metadatahandler"
@@ -24,7 +25,17 @@ import (
// New returns a GRPC server with all Gitaly services and interceptors set up.
func New(rubyServer *rubyserver.Server) *grpc.Server {
- logrusEntry := log.NewEntry(log.StandardLogger())
+ logger := log.StandardLogger()
+
+ urlSanitizer := logsanitizer.NewURLSanitizerHook()
+ urlSanitizer.AddPossibleGrpcMethod(
+ "CreateRepositoryFromURL",
+ "FetchRemote",
+ "UpdateRemoteMirror",
+ )
+ logger.Hooks.Add(urlSanitizer)
+
+ logrusEntry := log.NewEntry(logger)
grpc_logrus.ReplaceGrpcLogger(logrusEntry)
ctxTagOpts := []grpc_ctxtags.Option{