diff --git a/app/electron-builder-darwin-arm64.yml b/app/electron-builder-darwin-arm64.yml
index 47431d46c..37d792d15 100644
--- a/app/electron-builder-darwin-arm64.yml
+++ b/app/electron-builder-darwin-arm64.yml
@@ -64,3 +64,5 @@ extraResources:
filter: "!**/{.DS_Store}"
- from: "pandoc/pandoc-darwin-arm64.zip"
to: "pandoc.zip"
+ - from: "pandoc/pandoc-template.docx"
+ to: "pandoc-template.docx"
diff --git a/app/electron-builder-darwin.yml b/app/electron-builder-darwin.yml
index db8dc11e5..9cedf2323 100644
--- a/app/electron-builder-darwin.yml
+++ b/app/electron-builder-darwin.yml
@@ -64,3 +64,5 @@ extraResources:
filter: "!**/{.DS_Store}"
- from: "pandoc/pandoc-darwin-amd64.zip"
to: "pandoc.zip"
+ - from: "pandoc/pandoc-template.docx"
+ to: "pandoc-template.docx"
diff --git a/app/electron-builder-linux-arm64.yml b/app/electron-builder-linux-arm64.yml
index fda09074b..01faaebfe 100644
--- a/app/electron-builder-linux-arm64.yml
+++ b/app/electron-builder-linux-arm64.yml
@@ -67,4 +67,6 @@ extraResources:
to: "appearance/fonts"
filter: "!**/{.DS_Store}"
- from: "pandoc/pandoc-linux-arm64.zip"
- to: "pandoc.zip"
\ No newline at end of file
+ to: "pandoc.zip"
+ - from: "pandoc/pandoc-template.docx"
+ to: "pandoc-template.docx"
diff --git a/app/electron-builder-linux.yml b/app/electron-builder-linux.yml
index 1e5d86b61..014c607ae 100644
--- a/app/electron-builder-linux.yml
+++ b/app/electron-builder-linux.yml
@@ -64,4 +64,6 @@ extraResources:
to: "appearance/fonts"
filter: "!**/{.DS_Store}"
- from: "pandoc/pandoc-linux-amd64.zip"
- to: "pandoc.zip"
\ No newline at end of file
+ to: "pandoc.zip"
+ - from: "pandoc/pandoc-template.docx"
+ to: "pandoc-template.docx"
diff --git a/app/electron-builder.yml b/app/electron-builder.yml
index f5b455610..bc36976b4 100644
--- a/app/electron-builder.yml
+++ b/app/electron-builder.yml
@@ -71,3 +71,5 @@ extraResources:
filter: "!**/{.DS_Store}"
- from: "pandoc/pandoc-windows-amd64.zip"
to: "pandoc.zip"
+ - from: "pandoc/pandoc-template.docx"
+ to: "pandoc-template.docx"
diff --git a/app/pandoc/pandoc-template.docx b/app/pandoc/pandoc-template.docx
new file mode 100644
index 000000000..876688a40
Binary files /dev/null and b/app/pandoc/pandoc-template.docx differ
diff --git a/app/src/emoji/index.ts b/app/src/emoji/index.ts
index 044df7a1f..cb1ebeffb 100644
--- a/app/src/emoji/index.ts
+++ b/app/src/emoji/index.ts
@@ -27,8 +27,10 @@ export const unicode2Emoji = (unicode: string, className = "", needSpan = false,
let emoji = "";
if (unicode.startsWith("api/icon/getDynamicIcon")) {
emoji = `
`;
+ emoji = Lute.Sanitize(emoji);
} else if (unicode.indexOf(".") > -1) {
emoji = `
`;
+ emoji = Lute.Sanitize(emoji);
} else {
try {
unicode.split("-").forEach(item => {
diff --git a/app/src/protyle/header/Title.ts b/app/src/protyle/header/Title.ts
index 3da8e6d9c..bdb7c6b83 100644
--- a/app/src/protyle/header/Title.ts
+++ b/app/src/protyle/header/Title.ts
@@ -104,8 +104,12 @@ export class Title {
event.stopPropagation();
let textPlain = await readText() || "";
if (textPlain) {
+ // 对 <> 进行内部转义 https://github.com/siyuan-note/siyuan/issues/11992
+ textPlain = textPlain.replace(/<>/g, "__@gt2@__");
// 对 HTML 标签进行内部转义,避免被 Lute 解析以后变为小写 https://github.com/siyuan-note/siyuan/issues/10620
textPlain = textPlain.replace(//g, ";;;gt;;;");
+ // 反转义 <>
+ textPlain = textPlain.replace(/__@lt2assets\/@__/g, "<>");
enableLuteMarkdownSyntax(protyle);
let content = protyle.lute.BlockDOM2EscapeMarkerContent(protyle.lute.Md2BlockDOM(textPlain));
restoreLuteMarkdownSyntax(protyle);
@@ -273,7 +277,9 @@ export class Title {
accelerator: "⇧⌘V",
click: async () => {
let textPlain = await readText() || "";
+ textPlain = textPlain.replace(/<>/g, "__@gt2@__");
textPlain = textPlain.replace(//g, ";;;gt;;;");
+ textPlain = textPlain.replace(/__@lt2assets\/@__/g, "<>");
enableLuteMarkdownSyntax(protyle);
let content = protyle.lute.BlockDOM2EscapeMarkerContent(protyle.lute.Md2BlockDOM(textPlain));
restoreLuteMarkdownSyntax(protyle);
diff --git a/app/src/protyle/util/paste.ts b/app/src/protyle/util/paste.ts
index b8f7d3efc..c00403f70 100644
--- a/app/src/protyle/util/paste.ts
+++ b/app/src/protyle/util/paste.ts
@@ -175,9 +175,15 @@ export const pasteAsPlainText = async (protyle: IProtyle) => {
// 删掉 text 标签,只保留文本
textPlain = textPlain.replace(/(.*?)<\/span>/g, "$1");
+ // 对 <> 进行内部转义 https://github.com/siyuan-note/siyuan/issues/11992
+ textPlain = textPlain.replace(/<>/g, "__@gt2@__");
+
// 对 HTML 标签进行内部转义,避免被 Lute 解析以后变为小写 https://github.com/siyuan-note/siyuan/issues/10620
textPlain = textPlain.replace(//g, ";;;gt;;;");
+ // 反转义 <>
+ textPlain = textPlain.replace(/__@lt2assets\/@__/g, "<>");
+
// 反转义内置需要解析的 HTML 标签
textPlain = textPlain.replace(/__@sub@__/g, "").replace(/__@\/sub@__/g, "");
textPlain = textPlain.replace(/__@sup@__/g, "").replace(/__@\/sup@__/g, "");
diff --git a/kernel/api/file.go b/kernel/api/file.go
index 5d7543bde..61da124e2 100644
--- a/kernel/api/file.go
+++ b/kernel/api/file.go
@@ -66,14 +66,26 @@ func globalCopyFiles(c *gin.Context) {
srcs = append(srcs, s.(string))
}
- for _, src := range srcs {
- if !filelock.IsExist(src) {
+ for i, src := range srcs {
+ absSrc, _ := filepath.Abs(src)
+
+ if !filelock.IsExist(absSrc) {
msg := fmt.Sprintf("file [%s] does not exist", src)
logging.LogErrorf(msg)
ret.Code = -1
ret.Msg = msg
return
}
+
+ if util.IsSensitivePath(absSrc) {
+ msg := fmt.Sprintf("refuse to copy sensitive file [%s]", src)
+ logging.LogErrorf(msg)
+ ret.Code = -2
+ ret.Msg = msg
+ return
+ }
+
+ srcs[i] = absSrc
}
destDir := arg["destDir"].(string) // 相对于工作空间的路径
@@ -155,6 +167,13 @@ func getFile(c *gin.Context) {
c.JSON(http.StatusAccepted, ret)
return
}
+ if !filelock.IsExist(fileAbsPath) {
+ ret.Code = http.StatusNotFound
+ ret.Msg = "file does not exist"
+ c.JSON(http.StatusAccepted, ret)
+ return
+ }
+
info, err := os.Stat(fileAbsPath)
if os.IsNotExist(err) {
ret.Code = http.StatusNotFound
@@ -178,19 +197,8 @@ func getFile(c *gin.Context) {
}
// REF: https://github.com/siyuan-note/siyuan/issues/11364
- if role := model.GetGinContextRole(c); !model.IsValidRole(role, []model.Role{
- model.RoleAdministrator,
- }) {
- if relPath, err := filepath.Rel(util.ConfDir, fileAbsPath); err != nil {
- logging.LogErrorf("Get a relative path from [%s] to [%s] failed: %s", util.ConfDir, fileAbsPath, err)
- ret.Code = http.StatusInternalServerError
- ret.Msg = err.Error()
- c.JSON(http.StatusAccepted, ret)
- return
- } else if relPath == "conf.json" {
- ret.Code = http.StatusForbidden
- ret.Msg = http.StatusText(http.StatusForbidden)
- c.JSON(http.StatusAccepted, ret)
+ if !model.IsAdminRoleContext(c) {
+ if refuseToAccess(c, fileAbsPath, ret) {
return
}
}
@@ -216,6 +224,33 @@ func getFile(c *gin.Context) {
c.Data(http.StatusOK, contentType, data)
}
+func refuseToAccess(c *gin.Context, fileAbsPath string, ret *gulu.Result) bool {
+ // 禁止访问配置文件 conf/conf.json
+ if filepath.Join(util.ConfDir, "conf.json") == fileAbsPath {
+ ret.Code = http.StatusForbidden
+ ret.Msg = http.StatusText(http.StatusForbidden)
+ c.JSON(http.StatusAccepted, ret)
+ return true
+ }
+
+ // 禁止访问 data/snippets/conf.json
+ if filepath.Join(util.DataDir, "snippets", "conf.json") == fileAbsPath {
+ ret.Code = http.StatusForbidden
+ ret.Msg = http.StatusText(http.StatusForbidden)
+ c.JSON(http.StatusAccepted, ret)
+ return true
+ }
+
+ // 禁止访问 data/templates 目录
+ if util.IsSubPath(filepath.Join(util.DataDir, "templates"), fileAbsPath) {
+ ret.Code = http.StatusForbidden
+ ret.Msg = http.StatusText(http.StatusForbidden)
+ c.JSON(http.StatusAccepted, ret)
+ return true
+ }
+ return false
+}
+
func readDir(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
diff --git a/kernel/api/icon.go b/kernel/api/icon.go
index c0d56123d..f6f629ebc 100644
--- a/kernel/api/icon.go
+++ b/kernel/api/icon.go
@@ -164,6 +164,10 @@ func getDynamicIcon(c *gin.Context) {
svg = generateTypeOneSVG(color, lang, dateInfo)
}
+ if !model.Conf.Editor.AllowSVGScript {
+ svg = util.RemoveScriptsInSVG(svg)
+ }
+
c.Header("Content-Type", "image/svg+xml")
c.Header("Cache-Control", "no-cache")
c.Header("Pragma", "no-cache")
diff --git a/kernel/go.mod b/kernel/go.mod
index c4161f972..33b692db8 100644
--- a/kernel/go.mod
+++ b/kernel/go.mod
@@ -44,6 +44,7 @@ require (
github.com/jaypipes/ghw v0.21.2
github.com/jinzhu/copier v0.4.0
github.com/json-iterator/go v1.1.12
+ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klippa-app/go-pdfium v1.17.2
github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/mitchellh/go-ps v1.0.0
diff --git a/kernel/go.sum b/kernel/go.sum
index df0a1e6ce..2dc52c264 100644
--- a/kernel/go.sum
+++ b/kernel/go.sum
@@ -262,6 +262,8 @@ github.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM=
github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8=
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
github.com/juju/testing v0.0.0-20191001232224-ce9dec17d28b/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
diff --git a/kernel/model/assets.go b/kernel/model/assets.go
index 5f6dfdabf..6ca2aa97f 100644
--- a/kernel/model/assets.go
+++ b/kernel/model/assets.go
@@ -248,7 +248,18 @@ func netAssets2LocalAssets0(tree *parse.Tree, onlyImg bool, originalURL string,
u = u[:strings.Index(u, "?")]
}
- if !gulu.File.IsExist(u) || gulu.File.IsDir(u) {
+ if !gulu.File.IsExist(u) {
+ logging.LogErrorf("local file asset [%s] not exist", u)
+ continue
+ }
+
+ if gulu.File.IsDir(u) {
+ logging.LogWarnf("ignore converting directory path [%s] to local asset", u)
+ continue
+ }
+
+ if util.IsSensitivePath(u) {
+ logging.LogWarnf("ignore converting sensitive path [%s] to local asset", u)
continue
}
diff --git a/kernel/model/conf.go b/kernel/model/conf.go
index bf76a5367..34dd01abc 100644
--- a/kernel/model/conf.go
+++ b/kernel/model/conf.go
@@ -1262,6 +1262,14 @@ func subscribeConfEvents() {
eventbus.Subscribe(util.EvtConfPandocInitialized, func() {
logging.LogInfof("pandoc initialized, set pandoc bin to [%s]", util.PandocBinPath)
Conf.Export.PandocBin = util.PandocBinPath
+
+ params := util.RemoveInvalid(Conf.Export.PandocParams)
+ if !strings.Contains(params, "--reference-doc") && "" != util.PandocTemplatePath {
+ params += " --reference-doc"
+ params += " \"" + util.PandocTemplatePath + "\""
+ Conf.Export.PandocParams = strings.TrimSpace(params)
+ }
+
Conf.Save()
})
}
diff --git a/kernel/model/export.go b/kernel/model/export.go
index 2b28e6064..07a68e185 100644
--- a/kernel/model/export.go
+++ b/kernel/model/export.go
@@ -45,6 +45,7 @@ import (
"github.com/emirpasic/gods/sets/hashset"
"github.com/emirpasic/gods/stacks/linkedliststack"
"github.com/imroc/req/v3"
+ shellquote "github.com/kballard/go-shellquote"
"github.com/pdfcpu/pdfcpu/pkg/api"
"github.com/pdfcpu/pdfcpu/pkg/font"
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu"
@@ -773,7 +774,12 @@ func ExportDocx(id, savePath string, removeAssets, merge bool) (fullPath string,
params := util.RemoveInvalid(Conf.Export.PandocParams)
if "" != params {
- args = append(args, strings.Split(params, " ")...)
+ customArgs, parseErr := shellquote.Split(params)
+ if nil != parseErr {
+ logging.LogErrorf("parse pandoc custom params [%s] failed: %s", params, parseErr)
+ } else {
+ args = append(args, customArgs...)
+ }
}
pandoc := exec.Command(Conf.Export.PandocBin, args...)
diff --git a/kernel/util/pandoc.go b/kernel/util/pandoc.go
index 2d9053789..d0e5d2085 100644
--- a/kernel/util/pandoc.go
+++ b/kernel/util/pandoc.go
@@ -101,7 +101,8 @@ func Pandoc(from, to, o, content string) (err error) {
}
var (
- PandocBinPath string // Pandoc 可执行文件路径
+ PandocBinPath string // Pandoc 可执行文件路径
+ PandocTemplatePath string // Pandoc Docx 模板文件路径
)
func InitPandoc() {
@@ -128,6 +129,18 @@ func InitPandoc() {
}
}
+ PandocTemplatePath = filepath.Join(pandocDir, "pandoc-template.docx")
+ if !gulu.File.IsExist(PandocTemplatePath) {
+ PandocTemplatePath = filepath.Join(WorkingDir, "pandoc-template.docx")
+ if "dev" == Mode || !gulu.File.IsExist(PandocTemplatePath) {
+ PandocTemplatePath = filepath.Join(WorkingDir, "pandoc/pandoc-template.docx")
+ }
+ }
+ if !gulu.File.IsExist(PandocTemplatePath) {
+ PandocTemplatePath = ""
+ logging.LogWarnf("pandoc template file [%s] not found", PandocTemplatePath)
+ }
+
defer eventbus.Publish(EvtConfPandocInitialized)
if gulu.OS.IsWindows() {
diff --git a/kernel/util/path.go b/kernel/util/path.go
index fd4a1d5e4..6fae2db3a 100644
--- a/kernel/util/path.go
+++ b/kernel/util/path.go
@@ -347,3 +347,96 @@ func IsPartitionRootPath(path string) bool {
return cleanPath == "/"
}
}
+
+// IsSensitivePath 对传入路径做统一的敏感性检测。
+func IsSensitivePath(p string) bool {
+ if p == "" {
+ return false
+ }
+ pp := filepath.Clean(strings.ToLower(p))
+
+ // 精确敏感文件
+ exact := []string{
+ "/etc/passwd",
+ "/etc/shadow",
+ "/etc/gshadow",
+ "/var/run/secrets/kubernetes.io/serviceaccount/token",
+ }
+ for _, e := range exact {
+ if pp == e {
+ return true
+ }
+ }
+
+ // 敏感目录前缀(UNIX 风格)
+ prefixes := []string{
+ "/etc/ssh",
+ "/root",
+ "/etc/ssl",
+ "/etc/letsencrypt",
+ "/var/lib/docker",
+ "/.gnupg",
+ "/.ssh",
+ "/.aws",
+ "/.kube",
+ "/.docker",
+ "/.config/gcloud",
+ }
+ for _, pre := range prefixes {
+ if strings.HasPrefix(pp, pre) {
+ return true
+ }
+ }
+
+ // Windows 常见敏感目录(小写比较)
+ winPrefixes := []string{
+ `c:\windows\system32`,
+ `c:\windows\system`,
+ `c:\users\`,
+ }
+ for _, wp := range winPrefixes {
+ if strings.HasPrefix(pp, strings.ToLower(wp)) {
+ return true
+ }
+ }
+
+ // 文件名级别检查
+ base := filepath.Base(pp)
+ n := strings.ToLower(base)
+ sensitiveNames := map[string]struct{}{
+ ".env": {},
+ ".env.local": {},
+ ".npmrc": {},
+ ".netrc": {},
+ "id_rsa": {},
+ "id_dsa": {},
+ "id_ecdsa": {},
+ "id_ed25519": {},
+ "authorized_keys": {},
+ "passwd": {},
+ "shadow": {},
+ "pgpass": {},
+ "hosts": {},
+ "credentials": {}, // 如 aws credentials
+ "config.json": {}, // docker config.json 可能含 token
+ }
+ if _, ok := sensitiveNames[n]; ok {
+ return true
+ }
+ // 支持 .env.* 之类的模式
+ if n == ".env" || strings.HasPrefix(n, ".env.") {
+ return true
+ }
+
+ // 扩展名级别检查
+ ext := strings.ToLower(filepath.Ext(n))
+ sensitiveExts := []string{
+ ".pem", ".key", ".p12", ".pfx", ".ppk", ".asc", ".gpg",
+ }
+ for _, se := range sensitiveExts {
+ if ext == se {
+ return true
+ }
+ }
+ return false
+}