GHSA-fph3-ghq9-vw66 is a critical-severity (CVSS 10) SQL Injection vulnerability in github.com/siyuan-note/siyuan/kernel. A fix is available for github.com/siyuan-note/siyuan/kernel — see the affected versions and patch details below.
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
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
- A successful exploit gives an attacker total control of the affected component, not partial access.
Exploitation and automatability from CISA’s SSVC triage for GHSA-fph3-ghq9-vw66.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
GHSA-fph3-ghq9-vw66 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 376,715 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
github.com/siyuan-note/siyuan/kernelReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.
Description
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:
- 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
fullTextSearchBlockapplies to the same SQL method. - method 3 builds a
REGEXPclause 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
- 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.
- 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.
- 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.
- 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
- 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.
- 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.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐹Go | github.com/siyuan-note/siyuan/kernel | all versions | 0.0.0-20260721004815-cf42dd5680c8go get github.com/siyuan-note/siyuan/kernel@v0.0.0-20260721004815-cf42dd5680c8 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/siyuan-note/siyuan/kernel, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update github.com/siyuan-note/siyuan/kernel to 0.0.0-20260721004815-cf42dd5680c8 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-fph3-ghq9-vw66 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-fph3-ghq9-vw66 can be triaged on real exposure rather than presence alone.
Tailored to GHSA-fph3-ghq9-vw66. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-fph3-ghq9-vw66 in your dependencies?
O3 Security finds GHSA-fph3-ghq9-vw66 across Go dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.