From 2eb6f6d961ec6a2c2eedc00f547a084bd812814c Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Wed, 15 Jul 2026 16:35:32 +0100 Subject: [PATCH] fs: don't log the contents of objects without a String method The logging functions take an object which is rendered into the log line. Rendering it with %+v dumps all its fields, which for an object holding backend config would include decrypted credentials. Every object currently logged is a string or has a String method, so render anything else as just its type to keep credentials out of the logs. See: https://github.com/rclone/rclone/security/code-scanning/183 --- fs/log.go | 11 ++++++++++- fs/log_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/fs/log.go b/fs/log.go index dcd726eec..bc9e90bc8 100644 --- a/fs/log.go +++ b/fs/log.go @@ -146,8 +146,17 @@ func logSlog(level LogLevel, text string, attrs []any) { func logSlogWithObject(level LogLevel, o any, text string, attrs []any) { if o != nil { + var object string + switch o.(type) { + case fmt.Stringer, string: + object = fmt.Sprint(o) + default: + // Don't render the fields of arbitrary objects as they + // may contain sensitive data such as credentials. + object = fmt.Sprintf("%T", o) + } attrs = slices.Concat(attrs, []any{ - "object", fmt.Sprintf("%+v", o), + "object", object, "objectType", fmt.Sprintf("%T", o), }) } diff --git a/fs/log_test.go b/fs/log_test.go index 0bb04bd21..e7c790039 100644 --- a/fs/log_test.go +++ b/fs/log_test.go @@ -1,8 +1,10 @@ package fs import ( + "bytes" "encoding/json" "fmt" + "log/slog" "strconv" "testing" @@ -32,6 +34,30 @@ func TestLogValue(t *testing.T) { assert.Equal(t, "", x.String()) } +func TestLogSlogWithObject(t *testing.T) { + var buf bytes.Buffer + oldLogger := logger + defer func() { logger = oldLogger }() + SetLogger(slog.NewTextHandler(&buf, nil)) + + // Objects with a String method are rendered with it + logSlogWithObject(LogLevelError, withString{}, "message", nil) + assert.Contains(t, buf.String(), "object=hello") + + // Plain strings are rendered as themselves + buf.Reset() + logSlogWithObject(LogLevelError, "potato", "message", nil) + assert.Contains(t, buf.String(), "object=potato") + + // Anything else shows only its type as it may contain + // sensitive data such as credentials + buf.Reset() + type secrets struct{ Password string } + logSlogWithObject(LogLevelError, &secrets{Password: "SECRET"}, "message", nil) + assert.Contains(t, buf.String(), "object=*fs.secrets") + assert.NotContains(t, buf.String(), "SECRET") +} + func TestLogLevelString(t *testing.T) { for _, test := range []struct { in LogLevel