返回列表

SiYuan: Unauthenticated SQL execution and REGEXP injection via fullTextSearchAssetContent (publish mode): reader-reachable raw SQL (method 2) and unescaped REGEXP (method 3) on read-write asset-content DB

CVE-2026-69083RCE2026-09-03

漏洞描述

**CVE:** This vulnerability corresponds to [CVE-2026-69083](https://nvd.nist.gov/vuln/detail/CVE-2026-69083). ### Summary The `/api/search/fullTextSearchAssetContent` endpoint exposes two SQL flaws on the asset-content database, both reachable by the publish `RoleReader` token and by the anonymous account when `Publish.Auth.Enable` is `false`: 1. **method 2** passes a client-supplied SQL statement to the read-write asset-content DB with no single-statement or read-only guard, and without the admin restriction its sibling `fullTextSearchBlock` applies to the same SQL method. 2. **method 3** builds a `REGEXP` clause by concatenating the client expression with no quote-escaping, permitting SQL breakout while the equivalent block-search builder does escape. Both run on a read-write handle through a statement-stacking-capable driver, spanning the cross-notebook asset-content store. ### Details **Route / auth tier.** `router.go`: `Handle("POST", "/api/search/fullTextSearchAssetContent", model.CheckAuth, fullTextSearchAssetContent)`, `CheckAuth` only. Anonymous/reader reachable on the publish surface. `parseSearchAssetContentArgs` reads `method` and `query` straight from the JSON body with no constraint, so both are fully client-controlled. **Missing admin guard (contrast with the sibling).** `fullTextSearchBlock` rejects the SQL method for non-admins (`if method == 2 && !IsAdminRoleContext(c)`). `fullTextSearchAssetContent` has no such check on its handler, so the SQL method is reachable by a reader. **method 2 : raw SQL, no statement guard.** Dispatch: `FullTextSearchAssetContent` case 2 → `searchAssetContentBySQL(query, …)`. After `filterQueryInvisibleChars` + `TrimSpace`, the statement is passed to `sql.SelectAssetContentsRawStmt(stmt, …)` → `queryAssetContent` → `assetContentDB.Query(query)` directly, with no `CheckSingleStatement`/`CheckReadonlyStatement`. The `assetContentDB` DSN sets no `mode=ro`/`_query_only`, so the handle is read-write (same `88250/go-sqlite3` fork). **method 3 : unescaped REGEXP concatenation.** Dispatch → `assetContentFieldRegexp(exp)`, which writes `(name REGEXP '<exp>' OR content REGEXP '<exp>')` by concatenation with **no** `'` escaping. `exp` reaches it after only `filterQueryInvisibleChars` (strips invisible characters, not quotes). The parallel block-search builder `fieldRegexp` performs `ReplaceAll(regexp, "'", "''")` before wrapping, this asset builder omits that step. A single quote in `exp` breaks out of the literal into SQL context. The result runs via `SelectAssetContentsRawStmtNoParse` → `queryAssetContent` → direct `assetContentDB.Query`, again with no single/read-only guard. **Post-hoc filter.** `FilterAssetContentByPublishAccess` runs on the results after the query executes; it filters rows and does not constrain the statement (same timing as the accepted `searchDocs`/`searchEmbedBlock` findings). **Handle / stacking / scope.** Read-write asset-content DB, `88250/go-sqlite3` stacking-capable driver, `ATTACH` available. The asset-content store spans notebooks cross-boundary. ### Impact An unauthenticated request (publish mode with auth disabled) or any publish `RoleReader` can, via method 2, execute arbitrary SQL on the read-write asset-content database, and via method 3, inject SQL through the unescaped `REGEXP` clause. Both permit cross-notebook read disclosure of asset-content data and, via the read-write handle and statement stacking, modification of database content and `ATTACH`-reachable files. No admin role or write permission through the normal API is required. Code execution is not reachable in the default build (no `load_extension`). ### PoC Steps 1. Create a doc with a heading and secret body (6806, admin token) `curl -s -X POST http://127.0.0.1:6806/api/notebook/createNotebook -H "Content-Type: application/json" -H "Authorization: Token g4wj3r04ntobe9m4" -d "{\"name\":\"F3\"}"` Take the returned notebook id as BOX, then: `curl -s -X POST http://127.0.0.1:6806/api/filetree/createDocWithMd -H "Content-Type: application/json" -H "Authorization: Token g4wj3r04ntobe9m4" -d "{\"notebook\":\"BOX\",\"path\":\"/f3-secret\",\"markdown\":\"## SecretSection\n\nUNIQUE_MARKER_99 hidden body text\"}"` The returned string is the doc root id → DOC. 2. Get the heading block id (admin SQL on 68 `curl -s -X POST http://127.0.0.1:6806/api/notebook/createNotebook -H "Content-Type: application/json" -H "Authorization: Token g4wj3r04ntobe9m4" -d "{\"name\":\"F3\"}"` Take the returned notebook id as BOX, then: `curl -s -X POST http://127.0.0.1:6806/api/fintent-Type: application/json" -H"Authorization: Token g4wj3r04ntobe9m4" -d "{\"notebook\":\"BOX\",\"path\":\"/f3-secret\",\"markdown\":\"## SecretSection\n\nUNIQUE_MARKER_99 hidden body text\"}"` The returned string is the doc root id → DOC. 2. Get the heading block id (admin SQL on 6806) `curl -s -X POST http://127.0.0.1:6806/api/query/sql -H "Content-Type: application/json" -H "Authorization: Token g4wj3r04ntobe9m4" -d "{\"stmt\":\"SELECT id,type,content FROM blocks WHERE type='h'\"}"` Copy the id whose content is SecretSection → HEADING. 3. Mark the doc forbidden from publishing (admin) `curl -s -X POST http://127.0.0.1:6806/api/filetree/setPublishAccess -H "Content-Type: application/json" -H "Authorization: Token g4wj3r04ntobe9m4" -d "{\"id\":\"DOC\",\"visible\":false,\"password\":\"\",\"disable\":true}"` Now the doc is explicitly excluded from the 4. Baseline: the admin-gated sibling refuses the reader `curl -i -X POST http://127.0.0.1:6808/api/block/getBlockDOM -H "Content-Type: application/json" -d "{\"id\":\"HEADING\"}"` → expect 403 (getBlockDOM is CheckAdminRole). This is how raw-DOM retrieval is supposed to be gated. 5. THE PROOF: reader pulls the forbidden doc's content anyway `curl -i -X POST http://127.0.0.1:6808/api/block/getHeadingChildrenDOM -H "Content-Type: application/json" -d "{\"id\":\"HEADING\"}"` → 200 status code and data contains the rendered HTML including UNIQUE_MARKER_99 hidden body text full content of a doc that is disabled from publishing, returned to an anonymous reader with no filter. ### Suggested fix Bring `fullTextSearchAssetContent` in line with its block-search twin: apply the `method == 2 && !IsAdminRoleContext` rejection, route the raw-SQL path through `CheckSingleStatement`/`CheckReadonlyStatement`, and add `ReplaceAll(exp, "'", "''")` in `assetContentFieldRegexp` to match `fieldRegexp`. Ideally run reader-reachable asset-content reads on a `_query_only=1` handle so no reader-reachable path can write or `ATTACH`. Source Code Location: https://github.com/siyuan-note/siyuan Affected Packages: - go:github.com/siyuan-note/siyuan/kernel, affected < 0.0.0-20260721004815-cf42dd5680c8, patched in 0.0.0-20260721004815-cf42dd5680c8 CVSS: - Primary: score 10.0, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N - CVSS_V3: score 10.0, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N References: - https://github.com/siyuan-note/siyuan/security/advisories/GHSA-fph3-ghq9-vw66 - https://nvd.nist.gov/vuln/detail/CVE-2026-69083 - https://github.com/siyuan-note/siyuan/commit/cf42dd5680c8f2d50cebfada5d639c8d59faf50e - https://www.vulncheck.com/advisories/siyuan-before-sql-injection-via-fulltextsearchassetcontent - https://github.com/advisories/GHSA-fph3-ghq9-vw66

查看原文