mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
fix(security): HTML-escape site_name 并对 doc_url 统一应用 sanitizeUrl
Refs #3839 (第 8、12 点)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"html"
|
||||
"strings"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
||||
@@ -163,7 +164,7 @@ func (h *SettingHandler) SendTestEmail(c *gin.Context) {
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>` + siteName + `</h1>
|
||||
<h1>` + html.EscapeString(siteName) + `</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="success">✓</div>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBuildVerifyCodeEmailBody_EscapesSiteName(t *testing.T) {
|
||||
svc := &EmailService{}
|
||||
|
||||
t.Run("escapes_script_injection", func(t *testing.T) {
|
||||
body := svc.buildVerifyCodeEmailBody("123456", `</h1><script>alert(1)</script><h1>`)
|
||||
|
||||
assert.NotContains(t, body, "<script>")
|
||||
assert.Contains(t, body, "<script>")
|
||||
})
|
||||
|
||||
t.Run("escapes_html_entities", func(t *testing.T) {
|
||||
body := svc.buildVerifyCodeEmailBody("123456", `A&B<C>"D`)
|
||||
|
||||
assert.Contains(t, body, "A&B<C>"D")
|
||||
})
|
||||
|
||||
t.Run("normal_site_name_unchanged", func(t *testing.T) {
|
||||
body := svc.buildVerifyCodeEmailBody("654321", "My Site")
|
||||
|
||||
assert.Contains(t, body, "<h1>My Site</h1>")
|
||||
assert.Contains(t, body, "654321")
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildPasswordResetEmailBody_EscapesSiteName(t *testing.T) {
|
||||
svc := &EmailService{}
|
||||
|
||||
t.Run("escapes_html_tags_in_site_name", func(t *testing.T) {
|
||||
body := svc.buildPasswordResetEmailBody("https://example.com/reset?token=abc", `</h1><img src=x onerror=alert(1)>`)
|
||||
|
||||
assert.NotContains(t, body, "<img src=x")
|
||||
assert.True(t, strings.Contains(body, "<img"))
|
||||
})
|
||||
|
||||
t.Run("escapes_html_entities", func(t *testing.T) {
|
||||
body := svc.buildPasswordResetEmailBody("https://example.com/reset", `A&B<C>`)
|
||||
|
||||
assert.Contains(t, body, "A&B<C>")
|
||||
})
|
||||
|
||||
t.Run("normal_site_name_and_url_unchanged", func(t *testing.T) {
|
||||
resetURL := "https://example.com/reset?token=xyz"
|
||||
body := svc.buildPasswordResetEmailBody(resetURL, "Sub2API")
|
||||
|
||||
assert.Contains(t, body, "<h1>Sub2API</h1>")
|
||||
assert.Contains(t, body, resetURL)
|
||||
})
|
||||
|
||||
t.Run("escapes_ampersand_in_reset_url", func(t *testing.T) {
|
||||
resetURL := "https://example.com/reset?a=1&b=2"
|
||||
body := svc.buildPasswordResetEmailBody(resetURL, "Site")
|
||||
|
||||
assert.NotContains(t, body, `href="https://example.com/reset?a=1&b=2"`)
|
||||
assert.Contains(t, body, `href="https://example.com/reset?a=1&b=2"`)
|
||||
})
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html"
|
||||
"log/slog"
|
||||
"math/big"
|
||||
"net"
|
||||
@@ -454,7 +455,7 @@ func (s *EmailService) buildVerifyCodeEmailBody(code, siteName string) string {
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`, siteName, code)
|
||||
`, html.EscapeString(siteName), code)
|
||||
}
|
||||
|
||||
// TestSMTPConnectionWithConfig 使用指定配置测试SMTP连接
|
||||
@@ -673,5 +674,5 @@ func (s *EmailService) buildPasswordResetEmailBody(resetURL, siteName string) st
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`, siteName, resetURL, resetURL)
|
||||
`, html.EscapeString(siteName), html.EscapeString(resetURL), html.EscapeString(resetURL))
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
htmlpkg "html"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
@@ -230,7 +231,7 @@ func injectSiteTitle(html, settingsJSON []byte) []byte {
|
||||
return html
|
||||
}
|
||||
|
||||
newTitle := []byte("<title>" + cfg.SiteName + " - AI API Gateway</title>")
|
||||
newTitle := []byte("<title>" + htmlpkg.EscapeString(cfg.SiteName) + " - AI API Gateway</title>")
|
||||
var buf bytes.Buffer
|
||||
buf.Write(html[:titleStart])
|
||||
buf.Write(newTitle)
|
||||
|
||||
@@ -79,6 +79,25 @@ func TestInjectSiteTitle(t *testing.T) {
|
||||
assert.Equal(t, string(html), string(result))
|
||||
})
|
||||
|
||||
t.Run("escapes_html_in_site_name", func(t *testing.T) {
|
||||
html := []byte(`<html><head><title>Sub2API - AI API Gateway</title></head><body></body></html>`)
|
||||
settingsJSON := []byte(`{"site_name":"</title><script>alert(1)</script><title>"}`)
|
||||
|
||||
result := injectSiteTitle(html, settingsJSON)
|
||||
|
||||
assert.NotContains(t, string(result), "<script>")
|
||||
assert.Contains(t, string(result), "</title><script>alert(1)</script><title>")
|
||||
})
|
||||
|
||||
t.Run("escapes_ampersand_in_site_name", func(t *testing.T) {
|
||||
html := []byte(`<html><head><title>Sub2API</title></head><body></body></html>`)
|
||||
settingsJSON := []byte(`{"site_name":"A&B"}`)
|
||||
|
||||
result := injectSiteTitle(html, settingsJSON)
|
||||
|
||||
assert.Contains(t, string(result), "<title>A&B - AI API Gateway</title>")
|
||||
})
|
||||
|
||||
t.Run("preserves_rest_of_html", func(t *testing.T) {
|
||||
html := []byte(`<html><head><meta charset="UTF-8"><title>Sub2API</title><script src="app.js"></script></head><body><div id="app"></div></body></html>`)
|
||||
settingsJSON := []byte(`{"site_name":"TestSite"}`)
|
||||
|
||||
Reference in New Issue
Block a user