Skip to content

Commit dfa7d7a

Browse files
committed
slog: JSONHandler does not escape HTML
Don't escape HTML characters. Log output does not need to be safe for embedding in HTML. Change-Id: I545d701239b8fbf5273ce27090f754b8ecc9082a Reviewed-on: https://go-review.googlesource.com/c/exp/+/464215 Run-TryBot: Jonathan Amsterdam <jba@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: Alan Donovan <adonovan@google.com>
1 parent aae9b4e commit dfa7d7a

File tree

2 files changed

+30
-20
lines changed

2 files changed

+30
-20
lines changed

slog/json_handler.go

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package slog
66

77
import (
8+
"bytes"
89
"context"
910
"encoding/json"
1011
"errors"
@@ -81,6 +82,7 @@ func (h *JSONHandler) WithGroup(name string) Handler {
8182
// - Floating-point NaNs and infinities are formatted as one of the strings
8283
// "NaN", "+Inf" or "-Inf".
8384
// - Levels are formatted as with Level.String.
85+
// - HTML characters are not escaped.
8486
//
8587
// Each call to Handle results in a single serialized call to io.Writer.Write.
8688
func (h *JSONHandler) Handle(r Record) error {
@@ -146,11 +148,15 @@ func appendJSONValue(s *handleState, v Value) error {
146148
}
147149

148150
func appendJSONMarshal(buf *buffer.Buffer, v any) error {
149-
b, err := json.Marshal(v)
150-
if err != nil {
151+
// Use a json.Encoder to avoid escaping HTML.
152+
var bb bytes.Buffer
153+
enc := json.NewEncoder(&bb)
154+
enc.SetEscapeHTML(false)
155+
if err := enc.Encode(v); err != nil {
151156
return err
152157
}
153-
buf.Write(b)
158+
bs := bb.Bytes()
159+
buf.Write(bs[:len(bs)-1]) // remove final newline
154160
return nil
155161
}
156162

@@ -168,7 +174,7 @@ func appendEscapedJSONString(buf []byte, s string) []byte {
168174
start := 0
169175
for i := 0; i < len(s); {
170176
if b := s[i]; b < utf8.RuneSelf {
171-
if htmlSafeSet[b] {
177+
if safeSet[b] {
172178
i++
173179
continue
174180
}
@@ -187,10 +193,6 @@ func appendEscapedJSONString(buf []byte, s string) []byte {
187193
char('t')
188194
default:
189195
// This encodes bytes < 0x20 except for \t, \n and \r.
190-
// It also escapes <, >, and &
191-
// because they can lead to security holes when
192-
// user-controlled strings are rendered into JSON
193-
// and served to some browsers.
194196
str(`u00`)
195197
char(hex[b>>4])
196198
char(hex[b&0xF])
@@ -236,23 +238,22 @@ func appendEscapedJSONString(buf []byte, s string) []byte {
236238

237239
var hex = "0123456789abcdef"
238240

239-
// Copied from encoding/json/encode.go:encodeState.string.
241+
// Copied from encoding/json/tables.go.
240242
//
241-
// htmlSafeSet holds the value true if the ASCII character with the given
242-
// array position can be safely represented inside a JSON string, embedded
243-
// inside of HTML <script> tags, without any additional escaping.
243+
// safeSet holds the value true if the ASCII character with the given array
244+
// position can be represented inside a JSON string without any further
245+
// escaping.
244246
//
245247
// All values are true except for the ASCII control characters (0-31), the
246-
// double quote ("), the backslash character ("\"), HTML opening and closing
247-
// tags ("<" and ">"), and the ampersand ("&").
248-
var htmlSafeSet = [utf8.RuneSelf]bool{
248+
// double quote ("), and the backslash character ("\").
249+
var safeSet = [utf8.RuneSelf]bool{
249250
' ': true,
250251
'!': true,
251252
'"': false,
252253
'#': true,
253254
'$': true,
254255
'%': true,
255-
'&': false,
256+
'&': true,
256257
'\'': true,
257258
'(': true,
258259
')': true,
@@ -274,9 +275,9 @@ var htmlSafeSet = [utf8.RuneSelf]bool{
274275
'9': true,
275276
':': true,
276277
';': true,
277-
'<': false,
278+
'<': true,
278279
'=': true,
279-
'>': false,
280+
'>': true,
280281
'?': true,
281282
'@': true,
282283
'A': true,

slog/json_handler_test.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,26 @@ func TestAppendJSONValue(t *testing.T) {
8383
jsonMarshaler{"xyz"},
8484
} {
8585
got := jsonValueString(t, AnyValue(value))
86-
b, err := json.Marshal(value)
86+
want, err := marshalJSON(value)
8787
if err != nil {
8888
t.Fatal(err)
8989
}
90-
want := string(b)
9190
if got != want {
9291
t.Errorf("%v: got %s, want %s", value, got, want)
9392
}
9493
}
9594
}
9695

96+
func marshalJSON(x any) (string, error) {
97+
var buf bytes.Buffer
98+
enc := json.NewEncoder(&buf)
99+
enc.SetEscapeHTML(false)
100+
if err := enc.Encode(x); err != nil {
101+
return "", err
102+
}
103+
return strings.TrimSpace(buf.String()), nil
104+
}
105+
97106
func TestJSONAppendAttrValueSpecial(t *testing.T) {
98107
// Attr values that render differently from json.Marshal.
99108
for _, test := range []struct {

0 commit comments

Comments
 (0)