From 2d22faef729e032767842989b0ec11e674e16687 Mon Sep 17 00:00:00 2001 From: Michael Schurter Date: Tue, 6 Jul 2021 15:12:06 -0700 Subject: [PATCH] add Quote type to enable safe concise output of untrusted strings --- intlogger.go | 3 ++ logger.go | 6 ++++ logger_test.go | 83 +++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 85 insertions(+), 7 deletions(-) diff --git a/intlogger.go b/intlogger.go index 6099e67..d491ae8 100644 --- a/intlogger.go +++ b/intlogger.go @@ -295,6 +295,9 @@ func (l *intLogger) logPlain(t time.Time, name string, level Level, msg string, continue FOR case Format: val = fmt.Sprintf(st[0].(string), st[1:]...) + case Quote: + raw = true + val = strconv.Quote(string(st)) default: v := reflect.ValueOf(st) if v.Kind() == reflect.Slice { diff --git a/logger.go b/logger.go index 7f36b1f..6a4665b 100644 --- a/logger.go +++ b/logger.go @@ -67,6 +67,12 @@ type Octal int // text output. For example: L.Info("bits", Binary(17)) type Binary int +// A simple shortcut to format strings with Go quoting. Control and +// non-printable characters will be escaped with their backslash equivalents in +// output. Intended for untrusted or multiline strings which should be logged +// as concisely as possible. +type Quote string + // ColorOption expresses how the output should be colored, if at all. type ColorOption uint8 diff --git a/logger_test.go b/logger_test.go index 6e4bd58..656cfb2 100644 --- a/logger_test.go +++ b/logger_test.go @@ -184,9 +184,9 @@ func TestLogger(t *testing.T) { } logger := New(&LoggerOptions{ - Name: "test", - Output: &buf, - IncludeLocation: true, + Name: "test", + Output: &buf, + IncludeLocation: true, AdditionalLocationOffset: 1, }) @@ -414,6 +414,32 @@ func TestLogger(t *testing.T) { assert.Equal(t, "[INFO] test: this is test: bytes=0xc perms=0755 bits=0b101\n", rest) }) + t.Run("supports quote formatting", func(t *testing.T) { + var buf bytes.Buffer + + logger := New(&LoggerOptions{ + Name: "test", + Output: &buf, + }) + + // unsafe is a string containing control characters and a byte + // sequence which is invalid utf8 ("\xFFa") to assert that all + // characters are properly encoded and produce valid utf8 output + unsafe := "foo\nbar\bbaz\xFFa" + + logger.Info("this is test", + "unquoted", "unquoted", "quoted", Quote("quoted"), + "unsafeq", Quote(unsafe)) + + str := buf.String() + dataIdx := strings.IndexByte(str, ' ') + rest := str[dataIdx+1:] + + assert.Equal(t, "[INFO] test: this is test: "+ + "unquoted=unquoted quoted=\"quoted\" "+ + "unsafeq=\"foo\\nbar\\bbaz\\xffa\"\n", rest) + }) + t.Run("supports resetting the output", func(t *testing.T) { var first, second bytes.Buffer @@ -804,6 +830,49 @@ func TestLogger_JSON(t *testing.T) { assert.Equal(t, float64(5), raw["bits"]) }) + t.Run("ignores quote formatting requests", func(t *testing.T) { + var buf bytes.Buffer + + logger := New(&LoggerOptions{ + Name: "test", + Output: &buf, + JSONFormat: true, + }) + + // unsafe is a string containing control characters and a byte + // sequence which is invalid utf8 ("\xFFa") to assert that all + // characters are properly encoded and produce valid json + unsafe := "foo\nbar\bbaz\xFFa" + + logger.Info("this is test", + "unquoted", "unquoted", "quoted", Quote("quoted"), + "unsafeq", Quote(unsafe), "unsafe", unsafe) + + b := buf.Bytes() + + // Assert the JSON only contains valid utf8 strings with the + // illegal byte replaced with the utf8 replacement character, + // and not invalid json with byte(255) + // Note: testify/assert.Contains did not work here + if needle := []byte(`\ufffda`); !bytes.Contains(b, needle) { + t.Fatalf("could not find %q (%v) in json bytes: %q", needle, needle, b) + } + if needle := []byte{255, 'a'}; bytes.Contains(b, needle) { + t.Fatalf("found %q (%v) in json bytes: %q", needle, needle, b) + } + + var raw map[string]interface{} + if err := json.Unmarshal(b, &raw); err != nil { + t.Fatal(err) + } + + assert.Equal(t, "this is test", raw["@message"]) + assert.Equal(t, "unquoted", raw["unquoted"]) + assert.Equal(t, "quoted", raw["quoted"]) + assert.Equal(t, "foo\nbar\bbaz\uFFFDa", raw["unsafe"]) + assert.Equal(t, "foo\nbar\bbaz\uFFFDa", raw["unsafeq"]) + }) + t.Run("includes the caller location", func(t *testing.T) { var buf bytes.Buffer @@ -837,10 +906,10 @@ func TestLogger_JSON(t *testing.T) { } logger := New(&LoggerOptions{ - Name: "test", - Output: &buf, - JSONFormat: true, - IncludeLocation: true, + Name: "test", + Output: &buf, + JSONFormat: true, + IncludeLocation: true, AdditionalLocationOffset: 1, })