diff --git a/backend/controllers/wechatcallback/page.go b/backend/controllers/wechatcallback/page.go index 91c4f28..8599eb4 100644 --- a/backend/controllers/wechatcallback/page.go +++ b/backend/controllers/wechatcallback/page.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "html/template" "net/http" + "strings" ) // 页面随主程序发布,无需外部字体、脚本或手机浏览器登录状态。 @@ -15,7 +16,14 @@ import ( var pageHTML string //go:embed page.css -var pageCSS string +var embeddedCSS string + +var pageCSS = normalizeStyle(embeddedCSS) + +// 与浏览器解析内嵌样式时的换行规则一致,避免跨平台构建导致摘要不匹配。 +func normalizeStyle(css string) string { + return strings.ReplaceAll(strings.ReplaceAll(css, "\r\n", "\n"), "\r", "\n") +} var resultTemplate = template.Must(template.New("wechat-result").Parse(pageHTML)) diff --git a/backend/controllers/wechatcallback/page_test.go b/backend/controllers/wechatcallback/page_test.go index 4d9f8d2..8bf0445 100644 --- a/backend/controllers/wechatcallback/page_test.go +++ b/backend/controllers/wechatcallback/page_test.go @@ -7,6 +7,8 @@ import ( "net/http/httptest" "strings" "testing" + + "golang.org/x/net/html" ) func TestWriteAuthorizationResult(t *testing.T) { @@ -57,6 +59,38 @@ func TestWriteAuthorizationResult(t *testing.T) { } } +func TestStyleHashAfterHTMLParsing(t *testing.T) { + for name, lineEnding := range map[string]string{"LF": "\n", "CRLF": "\r\n", "CR": "\r"} { + t.Run(name, func(t *testing.T) { + original := pageCSS + defer func() { pageCSS = original }() + pageCSS = normalizeStyle(strings.ReplaceAll(original, "\n", lineEnding)) + for _, authorized := range []bool{true, false} { + response := httptest.NewRecorder() + Write(response, authorized) + tokenizer := html.NewTokenizer(strings.NewReader(response.Body.String())) + var styles []string + for token := tokenizer.Next(); token != html.ErrorToken; token = tokenizer.Next() { + if token == html.StartTagToken && tokenizer.Token().Data == "style" { + if tokenizer.Next() != html.TextToken { + t.Fatal("内嵌样式必须包含有效文本") + } + styles = append(styles, tokenizer.Token().Data) + } + } + if len(styles) != 1 || styles[0] != original { + t.Fatal("浏览器解析后的样式必须与规范化样式一致") + } + digest := sha256.Sum256([]byte(styles[0])) + expected := "style-src 'sha256-" + base64.StdEncoding.EncodeToString(digest[:]) + "'" + if !strings.Contains(response.Header().Get("Content-Security-Policy"), expected) { + t.Fatal("安全策略必须允许浏览器实际解析出的样式摘要") + } + } + }) + } +} + func TestWriteResultSecurityHeaders(t *testing.T) { for _, authorized := range []bool{true, false} { response := httptest.NewRecorder()