GHSA-fph3-ghq9-vw66: SQL Injection

Published Sep 3, 2026
·
Updated

CVE: This vulnerability corresponds to 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/queryonly, 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 loadextension).

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\nUNIQUEMARKER99 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\nUNIQUEMARKER99 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 UNIQUEMARKER99 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 queryonly=1 handle so no reader-reachable path can write or ATTACH.

Affected Software

1 affected componentFixes available
go/github.com/siyuan-note/siyuan/kernel<0.0.0-20260721004815-cf42dd5680c8
0.0.0-20260721004815-cf42dd5680c8

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade go/github.com/siyuan-note/siyuan/kernel to a version that resolves this vulnerability.

    Fixed in 0.0.0-20260721004815-cf42dd5680c8
  2. Configuration

    Apply the same admin guard used by the sibling fullTextSearchBlock: for fullTextSearchAssetContent, when client-supplied method==2 (raw SQL path), reject requests where !IsAdminRoleContext(c) before dispatching to searchAssetContentBySQL / searchAssetContentByRawStmt.

    fullTextSearchAssetContent API handler (router.go / FullTextSearchAssetContent) method == 2 && !IsAdminRoleContext(c) = reject (add guard to block raw SQL for non-admins)
  3. Configuration

    Route the method==2 raw SQL path through CheckSingleStatement and CheckReadonlyStatement so that only a single, read-only statement can be executed on the asset-content database (the current path passes a client-supplied statement to assetContentDB.Query with no single-statement or read-only guard).

    fullTextSearchAssetContent raw SQL execution path (searchAssetContentBySQL / sql.SelectAssetContentsRawStmt / queryAssetContent / assetContentDB.Query) CheckSingleStatement/CheckReadonlyStatement = enforce
  4. Configuration

    In assetContentFieldRegexp(exp), escape single quotes exactly like the block-search twin: add ReplaceAll(exp, "'", "''") before concatenating into the REGEXP string so that SQL breakout via unescaped quotes is not possible (current implementation concatenates exp into the '(name REGEXP ... OR content REGEXP ...)' literal without escaping).

    assetContentFieldRegexp used by method 3 (REGEXP clause builder) ReplaceAll(exp, "'", "''") = apply before building (name REGEXP '<exp>' OR content REGEXP '<exp>')
  5. Compensating control

    Ideally use an asset-content handle configured for query-only/no write and no ATTACH capability (e.g., a handle/DSN with mode=ro / _query_only) for the reader surface, so even if the endpoint is reachable it cannot write or attach files.

  6. Compensating control

    Apply publishing exclusion for sensitive docs (admin: mark the doc forbidden/disable publishing) and verify that the endpoint does not return those forbidden docs; treat this as a compensating mitigation while code changes are pending.

Event History

Sep 3, 2026
Advisory Published
via GitHub·09:01 PM
Data Sourced
via GitHub·09:01 PM
DescriptionSeverityAffected Software

Frequently Asked Questions

1

Do attackers need an authenticated account to reach the vulnerable functionality?

A publish RoleReader token can reach it. Anonymous access is also possible when Publish.Auth.Enable is set to false.

2

What access does successful exploitation provide within the database?

The affected SQL runs against the read-write asset-content database through a driver that supports statement stacking. The database spans cross-notebook asset content, and the issue can permit execution beyond a single read-only query.

3

How can I determine whether anonymous users are exposed?

Check whether the publish surface is enabled and whether Publish.Auth.Enable is false. In that configuration, the POST /api/search/fullTextSearchAssetContent endpoint is reachable by the anonymous account.

4

Are there multiple injection paths to assess?

Yes. Method 2 accepts a client-supplied SQL statement without single-statement or read-only restrictions, while method 3 concatenates a client-provided regular expression into a REGEXP clause without quote escaping.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203