Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ctx_interface_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

87 changes: 87 additions & 0 deletions ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2096,6 +2096,93 @@ func Test_Ctx_AutoFormat_Struct(t *testing.T) {
)
}

// go test -run Test_Ctx_AutoFormat_XSS_Prevention
func Test_Ctx_AutoFormat_XSS_Prevention(t *testing.T) {
t.Parallel()
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
Comment thread
gaby marked this conversation as resolved.
t.Cleanup(func() {
app.ReleaseCtx(c)
})

// Test basic XSS with script tag
c.Request().Header.Set(HeaderAccept, MIMETextHTML)
err := c.AutoFormat("<script>alert('XSS')</script>")
require.NoError(t, err)
require.Equal(t, MIMETextHTMLCharsetUTF8, c.GetRespHeader(HeaderContentType))
require.Equal(t, "<p>&lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt;</p>", string(c.Response().Body()))
require.NotContains(t, string(c.Response().Body()), "<script>", "Script tags should be escaped")

// Test XSS with img onerror
c.Request().Header.Set(HeaderAccept, MIMETextHTML)
err = c.AutoFormat("<img src=x onerror=alert('XSS')>")
require.NoError(t, err)
require.Equal(t, "<p>&lt;img src=x onerror=alert(&#39;XSS&#39;)&gt;</p>", string(c.Response().Body()))
require.NotContains(t, string(c.Response().Body()), "<img", "Img tags should be escaped")

// Test XSS with iframe
c.Request().Header.Set(HeaderAccept, MIMETextHTML)
err = c.AutoFormat("<iframe src='javascript:alert(\"XSS\")'></iframe>")
require.NoError(t, err)
require.Equal(t, "<p>&lt;iframe src=&#39;javascript:alert(&#34;XSS&#34;)&#39;&gt;&lt;/iframe&gt;</p>", string(c.Response().Body()))
require.NotContains(t, string(c.Response().Body()), "<iframe", "Iframe tags should be escaped")

// Test XSS with event handler attributes
c.Request().Header.Set(HeaderAccept, MIMETextHTML)
err = c.AutoFormat("<div onload=alert('XSS')>test</div>")
require.NoError(t, err)
require.Equal(t, "<p>&lt;div onload=alert(&#39;XSS&#39;)&gt;test&lt;/div&gt;</p>", string(c.Response().Body()))
require.NotContains(t, string(c.Response().Body()), "<div", "Div tags should be escaped")

// Test XSS with link javascript protocol
c.Request().Header.Set(HeaderAccept, MIMETextHTML)
err = c.AutoFormat("<a href='javascript:alert(\"XSS\")'>Click me</a>")
require.NoError(t, err)
require.Equal(t, "<p>&lt;a href=&#39;javascript:alert(&#34;XSS&#34;)&#39;&gt;Click me&lt;/a&gt;</p>", string(c.Response().Body()))
require.NotContains(t, string(c.Response().Body()), "<a href", "Anchor tags should be escaped")

// Test XSS with mixed quotes
c.Request().Header.Set(HeaderAccept, MIMETextHTML)
err = c.AutoFormat(`"><script>alert('XSS')</script><"`)
require.NoError(t, err)
require.Equal(t, `<p>&#34;&gt;&lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt;&lt;&#34;</p>`, string(c.Response().Body()))
require.NotContains(t, string(c.Response().Body()), "<script>", "Script tags with quotes should be escaped")

// Test legitimate HTML special characters are escaped
c.Request().Header.Set(HeaderAccept, MIMETextHTML)
err = c.AutoFormat("Price: 5 < 10 & 20 > 15")
require.NoError(t, err)
require.Equal(t, "<p>Price: 5 &lt; 10 &amp; 20 &gt; 15</p>", string(c.Response().Body()))

// Test that pre-rendered HTML is treated as plain text
c.Request().Header.Set(HeaderAccept, MIMETextHTML)
err = c.AutoFormat("<b>Hello, World!</b>")
require.NoError(t, err)
require.Equal(t, "<p>&lt;b&gt;Hello, World!&lt;/b&gt;</p>", string(c.Response().Body()))

// Test that normal text without special chars works fine
c.Request().Header.Set(HeaderAccept, MIMETextHTML)
err = c.AutoFormat("Hello, World!")
require.NoError(t, err)
require.Equal(t, "<p>Hello, World!</p>", string(c.Response().Body()))

// Test XSS prevention with byte slice
c.Request().Header.Set(HeaderAccept, MIMETextHTML)
err = c.AutoFormat([]byte("<script>alert('XSS')</script>"))
require.NoError(t, err)
require.Equal(t, "<p>&lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt;</p>", string(c.Response().Body()))

// Test XSS prevention with complex struct formatting
c.Request().Header.Set(HeaderAccept, MIMETextHTML)
err = c.AutoFormat(struct {
Value string
}{Value: "<script>alert('XSS')</script>"})
require.NoError(t, err)
// When formatted as a string via fmt.Sprintf with %v, the struct becomes something like {<script>...}
require.NotContains(t, string(c.Response().Body()), "<script>", "Script tags in struct should be escaped")
require.Contains(t, string(c.Response().Body()), "&lt;script&gt;", "Escaped script tags should be present")
}

// go test -v -run=^$ -bench=Benchmark_Ctx_AutoFormat -benchmem -count=4
func Benchmark_Ctx_AutoFormat(b *testing.B) {
app := New()
Expand Down
3 changes: 2 additions & 1 deletion res.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ func (r *DefaultRes) Format(handlers ...ResFmt) error {
// AutoFormat performs content-negotiation on the Accept HTTP header.
// It uses Accepts to select a proper format.
// The supported content types are text/html, text/plain, application/json, application/xml, application/vnd.msgpack, and application/cbor.
// When text/html is selected, the body is treated as plain text and HTML-escaped before being wrapped in a `<p>` element.
// For more flexible content negotiation, use Format.
// If the header is not specified or there is no proper format, text/plain is used.
func (r *DefaultRes) AutoFormat(body any) error {
Expand Down Expand Up @@ -457,7 +458,7 @@ func (r *DefaultRes) AutoFormat(body any) error {
case "xml":
return r.XML(body)
case "html":
return r.SendString("<p>" + b + "</p>")
return r.SendString("<p>" + template.HTMLEscapeString(b) + "</p>")
Comment thread
gaby marked this conversation as resolved.
case "msgpack":
Comment thread
gaby marked this conversation as resolved.
return r.MsgPack(body)
case "cbor":
Expand Down
1 change: 1 addition & 0 deletions res_interface_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading