Skip to content

Commit 50e0bb4

Browse files
committed
🐛 Avoid leaking inline attributes from pasted HTML #19155
1 parent ae2ae95 commit 50e0bb4

2 files changed

Lines changed: 60 additions & 0 deletions

File tree

kernel/api/lute.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ func html2BlockDOM(c *gin.Context) {
269269

270270
parse.TextMarks2Inlines(tree) // 先将 TextMark 转换为 Inlines https://github.com/siyuan-note/siyuan/issues/13056
271271
parse.NestedInlines2FlattedSpansHybrid(tree, false)
272+
removeWhitespaceTextMarkStyles(tree)
272273

273274
md, err := lute.FormatNodeSync(tree.Root, luteEngine.ParseOptions, luteEngine.RenderOptions)
274275
if nil != err {
@@ -296,6 +297,23 @@ func html2BlockDOM(c *gin.Context) {
296297
}
297298
}
298299

300+
func removeWhitespaceTextMarkStyles(tree *parse.Tree) {
301+
var unlinks []*ast.Node
302+
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
303+
if entering && n.Type == ast.NodeTextMark && strings.TrimSpace(n.TextMarkTextContent) == "" {
304+
n.RemoveIALAttr("style")
305+
// 空白文本无法绑定 Kramdown 行内属性,移除样式属性节点以免渲染为正文。
306+
if n.Next != nil && n.Next.Type == ast.NodeKramdownSpanIAL && parse.IALVal(n.Next, "style") != "" {
307+
unlinks = append(unlinks, n.Next)
308+
}
309+
}
310+
return ast.WalkContinue
311+
})
312+
for _, n := range unlinks {
313+
n.Unlink()
314+
}
315+
}
316+
299317
func normalizeIFramePosition(node *ast.Node) {
300318
// iframe 的绝对或固定定位依赖原网页容器,粘贴后需让块保留在文档流中。
301319
if isOutOfFlowPosition(node.IALAttr("style")) {

kernel/api/lute_text_style_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ func TestHTML2BlockDOMQuotedFontFamily(t *testing.T) {
6868
styled, bold, link := 0, false, false
6969
ast.Walk(engine.BlockDOM2Tree(response.Data).Root, func(n *ast.Node, entering bool) ast.WalkStatus {
7070
if entering && n.Type == ast.NodeTextMark {
71+
if strings.TrimSpace(n.TextMarkTextContent) == "" {
72+
return ast.WalkContinue
73+
}
7174
style := n.IALAttr("style")
7275
for _, want := range []string{`font-family: "Mona Sans VF", "Segoe UI", Arial;`, "color: rgb(31, 35, 40);", "background-color: white;", "font-size: 14.000000px;"} {
7376
if !strings.Contains(style, want) {
@@ -124,3 +127,42 @@ func TestHTML2BlockDOMMatchesElementsByDefault(t *testing.T) {
124127
t.Fatalf("source appearance survived: %s", response.Data)
125128
}
126129
}
130+
131+
func TestHTML2BlockDOMDoesNotLeakWhitespaceTextMarkStyles(t *testing.T) {
132+
originalConf := model.Conf
133+
model.Conf = model.NewAppConf()
134+
model.Conf.System = &conf.System{}
135+
t.Cleanup(func() { model.Conf = originalConf })
136+
gin.SetMode(gin.TestMode)
137+
input := `<h3 style="color: rgb(31, 35, 40); font-family: Arial; font-size: 20px; font-weight: 600">` +
138+
`author<span>&nbsp;</span>commented<span>&nbsp;</span><relative-time><span>now</span></relative-time></h3>`
139+
body, err := json.Marshal(map[string]any{
140+
"dom": input, "skipBase64Assets": true, "skipInlineSVGAssets": true, "preserveSourceFormat": true,
141+
})
142+
if err != nil {
143+
t.Fatal(err)
144+
}
145+
recorder := httptest.NewRecorder()
146+
context, _ := gin.CreateTestContext(recorder)
147+
context.Request = httptest.NewRequest(http.MethodPost, "/api/lute/html2BlockDOM", strings.NewReader(string(body)))
148+
context.Request.Header.Set("Content-Type", "application/json")
149+
html2BlockDOM(context)
150+
var response struct {
151+
Code int
152+
Data string
153+
}
154+
if err = json.Unmarshal(recorder.Body.Bytes(), &response); err != nil || response.Code != 0 {
155+
t.Fatalf("conversion failed: %s, %v", recorder.Body.String(), err)
156+
}
157+
if strings.Contains(response.Data, "{: style=") {
158+
t.Fatalf("leaked attributes: %s", response.Data)
159+
}
160+
if !strings.Contains(response.Data, `style="color: rgb(31, 35, 40);font-family: Arial;font-size: 20.000000px;"`) {
161+
t.Fatalf("lost visible source formatting: %s", response.Data)
162+
}
163+
for _, text := range []string{"author", "commented", "now"} {
164+
if !strings.Contains(response.Data, text) {
165+
t.Fatalf("lost %q: %s", text, response.Data)
166+
}
167+
}
168+
}

0 commit comments

Comments
 (0)