GHSA-F72R-2H5J-7639

Case-Sensitive Chaos: Bypassing SiYuan Note's Security with a Capital Letter

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 28, 2026·5 min read·1 visit

Executive Summary (TL;DR)

Developers utilized case-sensitive string comparisons in Go to protect 'conf.json'. On Windows/macOS, 'Conf.json' is the same file but a different string. Attackers can bypass the blacklist and read the configuration file containing secrets.

SiYuan Note, a privacy-first personal knowledge management tool, suffered from a classic disconnect between rigid programming logic and lax filesystem standards. By failing to account for case-insensitivity on Windows (NTFS) and macOS (APFS), the kernel allowed attackers to bypass a blacklist designed to protect critical configuration files. A simple change from 'conf.json' to 'Conf.json' was all it took to exfiltrate sensitive secrets.

The Hook: Privacy-First, Security-Maybe?

SiYuan Note pitches itself as a privacy-first, local-first knowledge base. It's the kind of tool used by people who don't trust the cloud—security-conscious folks, developers, and writers. The application runs a local kernel (server) that handles file operations, exposing an API endpoint /api/file/getFile to fetch note data.

The developers knew that some files were too dangerous to share. Specifically, conf.json. This file holds the keys to the kingdom: authentication tokens, sync credentials (potentially AWS S3 keys for backups), and system paths. Naturally, they wrote code to block access to it. But in the world of cross-platform development, assuming the file system behaves the way you want it to is a fatal error.

This vulnerability is a perfect example of 'Logic vs. Reality.' In the logical world of Go, "A" does not equal "a". In the messy reality of Windows NTFS or macOS APFS, "A" is just "a" with a hat on. And that discrepancy is where we live.

The Flaw: A Tale of Two Filesystems

The root cause here is CWE-178: Improper Handling of Case Sensitivity. It’s a bug class that has existed since the dawn of Windows NT, yet it keeps claiming victims in modern software. The SiYuan kernel implemented a blacklist (always a red flag) to protect specific files.

Here is the logic they roughly employed:

// Pseudo-code of the vulnerable logic
if requestedPath == "conf.json" {
    return Error("Forbidden")
}
serveFile(requestedPath)

On a Linux server (case-sensitive), this is fine. Conf.json doesn't exist, so requesting it returns a 404. conf.json is blocked. Secure.

But on Windows, the file system is case-insensitive but case-preserving. If an attacker requests Conf.json:

  1. The Go runtime says: "Conf.json" != "conf.json". The check passes.
  2. The OS says: "You want Conf.json? Sure, here is the file descriptor for that file on disk (which happens to be conf.json)."

It’s like a bouncer checking a guest list for "John". You show up with a nametag saying "JOHN", and he lets you in because "names are case-sensitive in my rulebook," completely ignoring that you are the same person.

The Code: The Smoking Gun

Let's look at the fix, which inadvertently highlights just how broken the original logic was. The patch was pushed in commit 1f02650b3892d2ea3896242dd2422c30bda55e11. The developers introduced a dedicated refuseToAccess function in kernel/api/file.go.

Before the patch, the check was a loose string comparison against the input path. The patch attempts to formalize this by checking absolute paths, but it still relies on Go's strict equality operators.

The Patch Logic:

func refuseToAccess(c *gin.Context, fileAbsPath string, ret *gulu.Result) bool {
    // Forbidden access to config file conf/conf.json
    if filepath.Join(util.ConfDir, "conf.json") == fileAbsPath {
        return true
    }
    // ... checks for snippets and templates ...
    return false
}

> [!NOTE] > Critical Analysis: Even this patch is conceptually shaky on Windows. Unless fileAbsPath has been fully canonicalized (e.g., via filepath.EvalSymlinks or Windows API calls to get the "true" name), fileAbsPath might still carry the attacker's casing. If the attacker sends .../Conf.json, fileAbsPath might end in Conf.json. The check compares it against filepath.Join(..., "conf.json"). Since Conf.json != conf.json, the blacklist might still fail depending on how the upstream code resolves the absolute path.

The Exploit: Asking Politely

Exploiting this does not require shellcode, heap spraying, or complex ROP chains. It requires curl and a Shift key. This is trivial exploitation at its finest.

Scenario: You identify a SiYuan instance running in "publish" mode on a Windows server.

Step 1: The Standard Request (Blocked)

curl "http://target:6806/api/file/getFile?path=conf.json"
# Output: {"code": 403, "msg": "Forbidden"}

Step 2: The Bypass (Success)

curl "http://target:6806/api/file/getFile?path=Conf.json"
# Output: {"conf": {"repo": {...}, "sync": {"s3": {"accessKey": "AKIA..."} ...}}}

By simply capitalizing the 'C', we walk right past the security guard. The kernel reads the file because the OS allows it, and the API returns the JSON content. We now have the cloud storage credentials and can pivot to attacking their backups.

The Impact: Why This Hurts

While SiYuan is a note-taking app, it often stores highly sensitive intellectual property, personal journals, or technical documentation. The conf.json file is the master key.

Exposure Radius:

  • Cloud Credentials: Many users configure S3 or WebDAV for syncing. These credentials (Access Keys, Secrets) are stored in plain text in the config.
  • System Paths: The config reveals the directory structure of the host, aiding further traversal attacks.
  • Snippets & Templates: The vulnerability also exposes data/snippets and data/templates. If the user stores secrets in custom script snippets, those are gone too.

For a "privacy-focused" tool, this is a catastrophic failure of its primary promise. It turns the application into an open file server for anyone who knows how to spell "Conf".

The Fix: A Band-Aid or a Cure?

The official fix in version 2.12.x (specifically commit 1f02650b3892d2ea3896242dd2422c30bda55e11) adds the checks we discussed. However, as noted in the analysis, developers working in Go on Windows must be extremely careful with path normalization.

For Users: Update immediately. If you are running SiYuan as a public-facing service (Docker or direct binary), ensure you are on the latest version. Do not expose the kernel port to the raw internet without a reverse proxy (Nginx/Caddy) handling authentication, as SiYuan's built-in protections have historically been... optimistic.

For Developers: Stop using blacklists for file access control. Use whitelists. Define exactly what can be accessed (e.g., *.md files in specific folders) and block everything else. Furthermore, when dealing with paths, always resolve to the canonical representation on disk (using filepath.EvalSymlinks and OS-specific realpath equivalents) before performing security checks.

Fix Analysis (1)

Technical Appendix

CVSS Score
7.5/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Affected Systems

Windows (NTFS file system)macOS (APFS case-insensitive)SiYuan Note Kernel (prior to commit 1f02650b)

Affected Versions Detail

Product
Affected Versions
Fixed Version
SiYuan Note
SiYuan
< Commit 1f02650bCommit 1f02650b3892d2ea3896242dd2422c30bda55e11
AttributeDetail
CWE IDCWE-178 (Case Sensitivity)
Attack VectorNetwork (API)
CVSS (Est.)7.5 (High)
PlatformWindows / macOS
ImpactSensitive Data Exposure
Fix StatusPatched (Commit 1f02650b)
CWE-178
Improper Handling of Case Sensitivity

The software performs a security check based on a case-sensitive comparison of a filename, but the underlying filesystem is case-insensitive, allowing attackers to bypass the check.

Vulnerability Timeline

Initial concerns raised about read-only publish mode security.
2024-05-12
Vulnerability details formalized in Issue #16603.
2025-12-16
Patch commit 1f02650b pushed to repository.
2026-01-18

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.