# SiYuan: getAttributeViewSearchTarget returns database row content to anonymous readers with no publish-access check, reopening the class closed one day earlier at the adjacent route

New dev-branch SiYuan endpoint getAttributeViewSearchTarget lacked CheckReadonly, letting anonymous publish-mode readers search and retrieve database rows withheld by publish-access filters.

- Payout: Research
- Severity: high
- Program: SiYuan
- Found by: Shirshakhtml
- Disclosed: 2026-10-01
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-9cqf-hhrq-7v45
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-9cqf-hhrq-7v45
- Tags: area:web domain:api domain:web impact:data-leak kind:research platform:ghsa sev:high tech:go technique:incomplete-fix technique:source-review vuln:access-control vuln:auth-bypass vuln:info-disclosure

**Target:** POST /api/av/getAttributeViewSearchTarget API endpoint (Go/Gin kernel)
**Root cause:** Route registered with only CheckAuth, omitting CheckReadonly and the in-body publish-access/encrypted-notebook gates that sibling attribute-view routes apply, so the handler reaches the render sink unfiltered.
**Steps:**
1. Take the database block id from the published page DOM.
2. Confirm baseline: `POST /api/av/renderAttributeView` with `{"id":"<database block id>"}` returns 200 without rows bound to restricted blocks.
3. Query `POST /api/av/getAttributeViewSearchTarget` with `{"id":"<same database block id>","keywords":"<term>"}`; response 200 includes the withheld matching rows plus `MatchedKeyID`.
**How found:** Noticed a new route registered one day after commit 64c26e74b added CheckReadonly to the adjacent getAttributeViewFieldViews line, and compared guard columns across lines 533-541 to spot the missing middleware; then compared against renderAttributeView's two-layer publish filtering to show the bypass returns more than the closed route.
**Exploitation:** Anonymous reader in publish mode (Publish.Auth.Enable=false) or any RoleReader reuses a database id already exposed in published DOM, then iteratively queries the endpoint with keywords; strings.Contains across all columns plus MatchedKeyID acts as a substring oracle to confirm content, one row per request, no rate limiting.
**Escalation:** Also reads rows in hidden, password-protected or forbidden documents and encrypted notebooks while unlocked, since isEncryptedNotebookDeniedForPublish is absent from av.go.
**Insight:** When a fix lands on one route in a cluster of sibling endpoints, diff the router registration table line-by-line for the same data class; a new route added right after a hardening commit often defaults to the weakest guard. Also check whether the bypassed endpoint reaches the same render sink as the filtered one, since per-item filtering (not just the containing-document gate) is what makes leaked ids exploitable.

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

**Anonymous request to getAttributeViewSearchTarget leaking withheld rows**

```
POST http://127.0.0.1:6808/api/av/getAttributeViewSearchTarget
{"id":"<same database block id>","keywords":"<term>"}

→ 200, matching rows including those step 2 withheld,
       with MatchedKeyID identifying the column that matched
```

**Baseline renderAttributeView request showing rows withheld**

```
POST http://127.0.0.1:6808/api/av/renderAttributeView
{"id":"<database block id>"}

→ 200, rows bound to restricted blocks are absent
```

