Skip to content

Commit 88a4315

Browse files
committed
chore: add html escaping tests
Signed-off-by: Danny Kopping <danny@coder.com>
1 parent 685e526 commit 88a4315

2 files changed

Lines changed: 111 additions & 2 deletions

File tree

internal/encoding/json/encode.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -481,9 +481,11 @@ func marshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) {
481481

482482
b, err := m.MarshalJSON()
483483
if err == nil {
484-
// EDIT(begin): skip appendCompact - MarshalJSON output is already valid compact JSON
484+
// EDIT(begin): skip appendCompact validation - MarshalJSON output is already valid compact JSON.
485485
// appendCompact scans every byte to validate/compact, which is O(n) per nested MarshalJSON call.
486486
// For deeply nested structures this becomes a significant bottleneck.
487+
// HTML escaping is also skipped because MarshalJSON implementations in this SDK use Marshal()
488+
// internally, which already performs HTML escaping when escapeHTML is enabled (the default).
487489
e.Buffer.Write(b)
488490
// EDIT(end)
489491
}
@@ -508,7 +510,9 @@ func addrMarshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) {
508510
m := va.Interface().(Marshaler)
509511
b, err := m.MarshalJSON()
510512
if err == nil {
511-
// EDIT(begin): skip appendCompact - MarshalJSON output is already valid compact JSON
513+
// EDIT(begin): skip appendCompact validation - MarshalJSON output is already valid compact JSON.
514+
// HTML escaping is also skipped because MarshalJSON implementations in this SDK use Marshal()
515+
// internally, which already performs HTML escaping when escapeHTML is enabled (the default).
512516
e.Buffer.Write(b)
513517
// EDIT(end)
514518
}

internal/encoding/json/encode_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package json
22

33
import (
4+
"bytes"
5+
"strings"
46
"testing"
57
)
68

@@ -92,3 +94,106 @@ func BenchmarkMarshalSliceOfNestedMarshalJSON(b *testing.B) {
9294
}
9395
}
9496
}
97+
98+
// Test that HTML escaping is preserved for nested MarshalJSON calls
99+
type htmlTestInner struct {
100+
Content string `json:"content"`
101+
}
102+
103+
func (h htmlTestInner) MarshalJSON() ([]byte, error) {
104+
return Marshal(struct {
105+
Content string `json:"content"`
106+
}{h.Content})
107+
}
108+
109+
type htmlTestOuter struct {
110+
Inner htmlTestInner `json:"inner"`
111+
}
112+
113+
func (h htmlTestOuter) MarshalJSON() ([]byte, error) {
114+
return Marshal(struct {
115+
Inner htmlTestInner `json:"inner"`
116+
}{h.Inner})
117+
}
118+
119+
func TestMarshalHTMLEscapeWithNestedMarshalJSON(t *testing.T) {
120+
// Test that HTML-sensitive characters are escaped in nested MarshalJSON
121+
data := htmlTestOuter{
122+
Inner: htmlTestInner{
123+
Content: "<script>alert('xss')</script>",
124+
},
125+
}
126+
127+
result, err := Marshal(data)
128+
if err != nil {
129+
t.Fatalf("Marshal failed: %v", err)
130+
}
131+
132+
// The < and > should be escaped as \u003c and \u003e
133+
if strings.Contains(string(result), "<script>") {
134+
t.Errorf("HTML was not escaped in Marshal output: %s", result)
135+
}
136+
if !strings.Contains(string(result), `\u003cscript\u003e`) {
137+
t.Errorf("Expected escaped HTML in output, got: %s", result)
138+
}
139+
// Verify no double-escaping (e.g., \u003c should not become \\u003c)
140+
if strings.Contains(string(result), `\\u003c`) {
141+
t.Errorf("HTML was double-escaped in output: %s", result)
142+
}
143+
}
144+
145+
func TestEncoderHTMLEscapeWithNestedMarshalJSON(t *testing.T) {
146+
// Test with Encoder (which has escapeHTML=true by default)
147+
data := htmlTestOuter{
148+
Inner: htmlTestInner{
149+
Content: "<div>&amp;</div>",
150+
},
151+
}
152+
153+
var buf bytes.Buffer
154+
enc := NewEncoder(&buf)
155+
if err := enc.Encode(data); err != nil {
156+
t.Fatalf("Encode failed: %v", err)
157+
}
158+
159+
result := buf.String()
160+
// The < > & should be escaped
161+
if strings.Contains(result, "<div>") {
162+
t.Errorf("HTML was not escaped in Encoder output: %s", result)
163+
}
164+
if !strings.Contains(result, `\u003cdiv\u003e`) {
165+
t.Errorf("Expected escaped < and > in output, got: %s", result)
166+
}
167+
if !strings.Contains(result, `\u0026`) {
168+
t.Errorf("Expected escaped & in output, got: %s", result)
169+
}
170+
}
171+
172+
func TestEncoderNoHTMLEscapeWithNestedMarshalJSON(t *testing.T) {
173+
// Test with SetEscapeHTML(false)
174+
// Note: Inner MarshalJSON calls use Marshal() which has escapeHTML=true by default,
175+
// so HTML escaping still occurs in the nested output. This is expected behavior
176+
// since the inner calls don't inherit the outer encoder's settings.
177+
data := htmlTestOuter{
178+
Inner: htmlTestInner{
179+
Content: "<div>&</div>",
180+
},
181+
}
182+
183+
var buf bytes.Buffer
184+
enc := NewEncoder(&buf)
185+
enc.SetEscapeHTML(false)
186+
if err := enc.Encode(data); err != nil {
187+
t.Fatalf("Encode failed: %v", err)
188+
}
189+
190+
result := buf.String()
191+
// Inner Marshal calls still escape HTML since they use default settings
192+
if strings.Contains(result, "<div>") {
193+
t.Logf("Note: HTML in nested MarshalJSON is escaped because inner Marshal uses default escapeHTML=true")
194+
}
195+
// Just verify we got valid JSON output
196+
if !strings.Contains(result, "content") {
197+
t.Errorf("Expected content field in output, got: %s", result)
198+
}
199+
}

0 commit comments

Comments
 (0)