🔒 Do not execute scripts in assets SVG by default to prevent XSS https://github.com/siyuan-note/siyuan/issues/16844

Signed-off-by: Daniel <845765@qq.com>
This commit is contained in:
Daniel 2026-01-16 18:11:55 +08:00
parent 65532aec99
commit 11115da3d0
No known key found for this signature in database
GPG key ID: 86211BA83DF03017
23 changed files with 125 additions and 3 deletions

View file

@ -19,6 +19,7 @@ package conf
import "github.com/siyuan-note/siyuan/kernel/util"
type Editor struct {
AllowSVGScript bool `json:"allowSVGScript"` // 允许执行 SVG 内脚本
AllowHTMLBLockScript bool `json:"allowHTMLBLockScript"` // 允许执行 HTML 块内脚本
FontSize int `json:"fontSize"` // 字体大小
FontSizeScrollZoom bool `json:"fontSizeScrollZoom"` // 字体大小是否支持滚轮缩放

View file

@ -545,8 +545,7 @@ func serveAssets(ginServer *gin.Engine) {
}
}
if serveThumbnail(context, p, requestPath) {
// 如果请求缩略图服务成功则返回
if serveThumbnail(context, p, requestPath) || serveSVG(context, p) {
return
}
@ -562,6 +561,24 @@ func serveAssets(ginServer *gin.Engine) {
})
}
func serveSVG(context *gin.Context, assetAbsPath string) bool {
if strings.HasSuffix(assetAbsPath, ".svg") {
data, err := os.ReadFile(assetAbsPath)
if err != nil {
logging.LogErrorf("read svg file failed: %s", err)
return false
}
if !model.Conf.Editor.AllowSVGScript {
data = []byte(util.RemoveScriptsInSVG(string(data)))
}
context.Data(200, "image/svg+xml", data)
return true
}
return false
}
func serveThumbnail(context *gin.Context, assetAbsPath, requestPath string) bool {
if style := context.Query("style"); style == "thumb" && model.NeedGenerateAssetsThumbnail(assetAbsPath) { // 请求缩略图
thumbnailPath := filepath.Join(util.TempDir, "thumbnails", "assets", requestPath)

View file

@ -27,6 +27,7 @@ import (
"unicode"
"github.com/88250/lute/html"
"github.com/siyuan-note/logging"
)
func init() {
@ -220,3 +221,52 @@ func ReplaceStr(strs []string, old, new string) (ret []string, changed bool) {
ret = strs
return
}
// RemoveScriptsInSVG 移除 SVG 中的 <script> 标签及其内部所有内容
func RemoveScriptsInSVG(svgInput string) string {
// 1. 将字符串解析为节点树
doc, err := html.Parse(strings.NewReader(svgInput))
if err != nil {
logging.LogWarnf("parse svg failed: %v", err)
return svgInput
}
// 2. 定义递归移除逻辑
var walk func(*html.Node)
walk = func(n *html.Node) {
// 倒序遍历子节点,确保删除操作不影响后续迭代
for c := n.FirstChild; c != nil; {
next := c.NextSibling
// 检查标签名是否为 script
if c.Type == html.ElementNode && strings.EqualFold(c.Data, "script") {
n.RemoveChild(c)
} else {
// 递归处理子节点
walk(c)
}
c = next
}
}
// 3. 执行移除
walk(doc)
// 4. 将处理后的树重新渲染回字符串
var buf bytes.Buffer
if err = html.Render(&buf, doc); err != nil {
logging.LogWarnf("render svg failed: %v", err)
return svgInput
}
// 5. 提取 SVG 部分 (html.Render 会自动加上 <html><body> 标签)
return extractSVG(buf.String())
}
func extractSVG(fullHTML string) string {
start := strings.Index(fullHTML, "<svg")
end := strings.LastIndex(fullHTML, "</svg>")
if start == -1 || end == -1 {
return fullHTML
}
return fullHTML[start : end+6]
}