GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-FPH3-GHQ9-VW66

Vulnerability from github – Published: 2026-09-03 21:01 – Updated: 2026-09-03 21:01
VLAI
Summary
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
Details

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, …)queryAssetContentassetContentDB.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 SelectAssetContentsRawStmtNoParsequeryAssetContent → 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.

  1. 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.

  1. 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.

  1. 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

  1. 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.

  1. 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.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/siyuan-note/siyuan/kernel"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.0.0-20260721004815-cf42dd5680c8"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-69083"
  ],
  "database_specific": {
    "cwe_ids": [],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-03T21:01:05Z",
    "nvd_published_at": null,
    "severity": "CRITICAL"
  },
  "details": "**CVE:** This vulnerability corresponds to [CVE-2026-69083](https://nvd.nist.gov/vuln/detail/CVE-2026-69083).\n\n### Summary\n\nThe `/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`:\n\n1. **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.\n2. **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.\n\nBoth run on a read-write handle through a statement-stacking-capable driver, spanning the cross-notebook asset-content store.\n\n### Details\n\n**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.\n\n**Missing admin guard (contrast with the sibling).** `fullTextSearchBlock` rejects the SQL method for non-admins (`if method == 2 \u0026\u0026 !IsAdminRoleContext(c)`). `fullTextSearchAssetContent` has no such check on its handler, so the SQL method is reachable by a reader.\n\n**method 2 : raw SQL, no statement guard.** Dispatch: `FullTextSearchAssetContent` case 2 \u2192 `searchAssetContentBySQL(query, \u2026)`. After `filterQueryInvisibleChars` + `TrimSpace`, the statement is passed to `sql.SelectAssetContentsRawStmt(stmt, \u2026)` \u2192 `queryAssetContent` \u2192 `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).\n\n**method 3 : unescaped REGEXP concatenation.** Dispatch \u2192 `assetContentFieldRegexp(exp)`, which writes `(name REGEXP \u0027\u003cexp\u003e\u0027 OR content REGEXP \u0027\u003cexp\u003e\u0027)` by concatenation with **no** `\u0027` escaping. `exp` reaches it after only `filterQueryInvisibleChars` (strips invisible characters, not quotes). The parallel block-search builder `fieldRegexp` performs `ReplaceAll(regexp, \"\u0027\", \"\u0027\u0027\")` 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` \u2192 `queryAssetContent` \u2192 direct `assetContentDB.Query`, again with no single/read-only guard.\n\n**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).\n\n**Handle / stacking / scope.** Read-write asset-content DB, `88250/go-sqlite3` stacking-capable driver, `ATTACH` available. The asset-content store spans notebooks cross-boundary.\n\n### Impact\n\nAn 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`).\n\n### PoC Steps\n\n1. Create a doc with a heading and secret body (6806, admin token)\n\n`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\\\"}\"`\nTake the returned notebook id as BOX, then:\n`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\\\"}\"`\nThe returned string is the doc root id \u2192 DOC.\n\n2. Get the heading block id (admin SQL on 68\n\n`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\\\"}\"`\nTake the returned notebook id as BOX, then:\n`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\\\"}\"`\nThe returned string is the doc root id \u2192 DOC.\n\n2. Get the heading block id (admin SQL on 6806)\n\n`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=\u0027h\u0027\\\"}\"`\nCopy the id whose content is SecretSection \u2192 HEADING.\n\n3. Mark the doc forbidden from publishing (admin)\n\n`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}\"`\nNow the doc is explicitly excluded from the\n\n4. Baseline: the admin-gated sibling refuses the reader\n\n`curl -i -X POST http://127.0.0.1:6808/api/block/getBlockDOM -H \"Content-Type: application/json\" -d \"{\\\"id\\\":\\\"HEADING\\\"}\"`\n\u2192 expect 403 (getBlockDOM is CheckAdminRole). This is how raw-DOM retrieval is supposed to be gated.\n\n5. THE PROOF: reader pulls the forbidden doc\u0027s content anyway\n\n`curl -i -X POST http://127.0.0.1:6808/api/block/getHeadingChildrenDOM -H \"Content-Type: application/json\" -d \"{\\\"id\\\":\\\"HEADING\\\"}\"`\n\u2192 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.\n\n### Suggested fix\n\nBring `fullTextSearchAssetContent` in line with its block-search twin: apply the `method == 2 \u0026\u0026 !IsAdminRoleContext` rejection, route the raw-SQL path through `CheckSingleStatement`/`CheckReadonlyStatement`, and add `ReplaceAll(exp, \"\u0027\", \"\u0027\u0027\")` 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`.",
  "id": "GHSA-fph3-ghq9-vw66",
  "modified": "2026-09-03T21:01:05Z",
  "published": "2026-09-03T21:01:05Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/siyuan-note/siyuan/security/advisories/GHSA-fph3-ghq9-vw66"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-69083"
    },
    {
      "type": "WEB",
      "url": "https://github.com/siyuan-note/siyuan/commit/cf42dd5680c8f2d50cebfada5d639c8d59faf50e"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/siyuan-note/siyuan"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/siyuan-before-sql-injection-via-fulltextsearchassetcontent"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…