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
This commit is contained in:
Nick Craig-Wood
2026-07-15 16:43:37 +01:00
parent d8b4966fa6
commit 2eb6f6d961
2 changed files with 36 additions and 1 deletions
+10 -1
View File
@@ -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),
})
}
+26
View File
@@ -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