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 +}