Skip to content

/api/lute/spinBlockDOM Accessible to All Authenticated Roles Without Admin Gate or Input Size Limit

Moderate
88250 published GHSA-3j8q-5c8c-grwm Aug 4, 2026

Package

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

Affected versions

3.7.3

Patched versions

v3.7.4

Description

Summary

The /api/lute/spinBlockDOM endpoint is registered with only model.CheckAuth middleware, making it accessible to any authenticated role including RoleEditor and RoleReader. Its sibling endpoint /api/lute/html2BlockDOM, which performs a comparable DOM transformation, is correctly guarded with model.CheckAdminRole and model.CheckReadonly. The inconsistency is annotated in the router source code itself with the comment // 未测试 ("untested"), indicating the endpoint has not been security-reviewed.

The handler accepts an arbitrary dom string from the request body, passes it without length validation to luteEngine.SpinBlockDOM(dom), and returns the transformed result. The Lute library is a large, actively maintained parser written in Go. The security profile of SpinBlockDOM against adversarial input — including its behavior on pathologically nested, large, or malformed DOM blobs — is not documented and has not been fuzz-tested as part of this research.

A second concern is resource consumption. ControlConcurrency serializes all requests to /api/lute/spinBlockDOM through a per-path mutex. A caller who sends a request with a large or complex DOM input causes the mutex to be held for the duration of the parse, blocking any concurrent legitimate request to this endpoint. This is a serialized denial-of-service (starvation) of the endpoint rather than a parallel exhaustion.

All current releases of SiYuan are affected. The endpoint is accessible to publish-mode RoleReader callers when the publish service is active.


Affected Components

Component Location
File kernel/api/router.go
File kernel/api/lute.gospinBlockDOM()
Route POST /api/lute/spinBlockDOM
Middleware model.CheckAuth only (missing model.CheckAdminRole)
Library luteEngine.SpinBlockDOM(dom) (external: github.com/88250/lute)
Middleware model.ControlConcurrency (per-path serialization)

Root Cause Analysis

Inconsistent role gating. The router registers both endpoints in the same file:

ginServer.Handle("POST", "/api/lute/spinBlockDOM",
    model.CheckAuth, spinBlockDOM) // 未测试
 
ginServer.Handle("POST", "/api/lute/html2BlockDOM",
    model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, html2BlockDOM)

Both call into the Lute parser. html2BlockDOM has CheckAdminRole and CheckReadonly. spinBlockDOM has neither. The inline comment // 未测试 confirms the omission was noticed but not resolved.

No input size validation. The handler extracts the DOM string from the request body and passes it directly to the parser:

var dom string
if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("dom", &dom, true, false)) {
    return
}
luteEngine := model.NewLute()
dom = luteEngine.SpinBlockDOM(dom)

util.ParseJsonArgs validates presence and string type but does not constrain len(dom). The gin server has a MaxMultipartMemory setting but JSON body size is not subject to that limit in the reviewed code. No explicit JSON body size limit was identified for this endpoint.

Serialization amplifies starvation. ControlConcurrency serializes the /api/lute/spinBlockDOM path:

mutex.Lock()
defer mutex.Unlock()
c.Next()

A caller sending a very large DOM causes SpinBlockDOM to hold the per-path mutex for the duration of the parse, blocking any concurrent request to the same path. While this prevents parallel resource exhaustion, it creates a deterministic starvation vector for a single caller.


Code Analysis

Route registration with inconsistency visible in source:

// kernel/api/router.go
ginServer.Handle("POST", "/api/lute/spinBlockDOM",
    model.CheckAuth, spinBlockDOM)      // Only CheckAuth — 未测试
 
ginServer.Handle("POST", "/api/lute/html2BlockDOM",
    model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, html2BlockDOM)
// All three middleware — the expected pattern for write/transform endpoints

Handler with no size check:

// kernel/api/lute.go
func spinBlockDOM(c *gin.Context) {
    ret := gulu.Ret.NewResult()
    defer c.JSON(http.StatusOK, ret)
 
    arg, ok := util.JsonArg(c, ret)
    if !ok { return }
 
    var dom string
    if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("dom", &dom, true, false)) {
        return
    }
    // No: if len(dom) > threshold { reject }
 
    luteEngine := model.NewLute()
    dom = luteEngine.SpinBlockDOM(dom)   // Lute parser called on unconstrained input
    ret.Data = map[string]any{"dom": dom}
}

The Lute parser is called with no upper bound on the size or complexity of the input string. The library is a complex parsing system; its behavior on adversarial inputs is out of scope for static analysis.


Attack Prerequisites

For the authorization gap:

  • Authenticated as any role (RoleEditor or RoleReader) via a valid session cookie, Authorization header, or publish-service JWT.

  • Network access to the SiYuan kernel port.
    For endpoint starvation:

  • Authenticated as any role (same as above).

  • Ability to send large POST bodies to /api/lute/spinBlockDOM.
    No user interaction required for either scenario.


Attack Scenarios

Scenario 1 — Access by Reader/Editor

  1. A publish-service RoleReader account (or an Editor who should not have access to system-level DOM transformation) authenticates to the SiYuan publish proxy.
  2. The caller sends:
   POST /api/lute/spinBlockDOM
   Authorization: Token <reader-jwt>
   Content-Type: application/json
   {"dom": "<div>some content</div>"}
  1. SiYuan processes the request and returns the transformed DOM. No role check blocks the call.

Severity of this scenario as described: Low. The endpoint performs a transformation and returns data; it does not mutate persistent state in the reviewed code. The concern escalates if SpinBlockDOM has undiscovered vulnerabilities in the Lute parser when called on attacker-controlled input.

Scenario 2 — Endpoint Starvation

  1. An authenticated caller (any role) repeatedly sends large DOM blobs to /api/lute/spinBlockDOM.
  2. ControlConcurrency serializes these requests through a single per-path mutex.
  3. Each large parse holds the mutex for an extended period, causing all other concurrent requests to this specific endpoint to queue or time out.
  4. Legitimate users who depend on the endpoint receive delayed or failed responses.

Scope limitation: Starvation is confined to the /api/lute/spinBlockDOM path by the per-path mutex design. Other SiYuan endpoints are not affected by this specific starvation.


Proof of Concept

The following PoC is theoretical and requires runtime verification.

Authorization gap:

# Using a RoleReader JWT obtained from the publish service
curl -s -X POST http://localhost:6806/api/lute/spinBlockDOM \
  -H "Authorization: Token <READER_JWT>" \
  -H "Content-Type: application/json" \
  -d '{"dom": "<div data-node-id=\"20240101000000-abcdefg\"><p>test</p></div>"}'
 
# Expected if vulnerable: {"code":0,"msg":"","data":{"dom":"..."}}
# Expected if mitigated: 403 Forbidden

Starvation (conceptual):

# Generate a large DOM blob
python3 -c "print('{\"dom\": \"' + '<div>' * 50000 + '</div>' * 50000 + '\"}')" > large_dom.json
 
# Submit with valid credentials
curl -s -X POST http://localhost:6806/api/lute/spinBlockDOM \
  -H "Authorization: Token <VALID_TOKEN>" \
  -H "Content-Type: application/json" \
  -d @large_dom.json &
 
# While previous request is processing, concurrent request to same endpoint stalls
curl -s -X POST http://localhost:6806/api/lute/spinBlockDOM \
  -H "Authorization: Token <VALID_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"dom": "<p>urgent</p>"}'
# This request queues behind the large parse

The actual impact of the starvation scenario depends on whether SpinBlockDOM exhibits linear or super-linear time complexity for large inputs — this requires empirical measurement.


Security Impact

Dimension Impact
Confidentiality None confirmed — the endpoint returns only a transformation of the caller-supplied input; it does not read persistent state.
Integrity Low for the role gap as currently understood. Reassess if SpinBlockDOM modifies state indirectly.
Availability Low-to-Medium — starvation is bounded to the /api/lute/spinBlockDOM path by the per-path mutex.
Parser safety Unknown — the security profile of luteEngine.SpinBlockDOM against adversarial input is not assessed in this report.

Unknown — parser safety: If the Lute library has latent parsing vulnerabilities (heap exhaustion, panic, memory corruption), exposing it to unauthenticated or low-privilege callers is a meaningful attack surface expansion relative to the current html2BlockDOM gating. This concern is classified as Unknown rather than confirmed.


Suggested Fix

Primary Fix — Add CheckAdminRole to Match Sibling Endpoint

// kernel/api/router.go
ginServer.Handle("POST", "/api/lute/spinBlockDOM",
    model.CheckAuth, model.CheckAdminRole, spinBlockDOM)
// Remove the "// 未测试" comment or replace with a tested annotation

Add Explicit Input Size Limit

// kernel/api/lute.go — spinBlockDOM
const maxSpinDOMBytes = 1 * 1024 * 1024 // 1 MB
 
var dom string
if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("dom", &dom, true, false)) {
    return
}
if len(dom) > maxSpinDOMBytes {
    ret.Code = -1
    ret.Msg = "dom input exceeds maximum permitted size"
    return
}
luteEngine := model.NewLute()
dom = luteEngine.SpinBlockDOM(dom)

Defense in Depth

Conduct fuzz testing of SpinBlockDOM with adversarial inputs before expanding access to lower-privilege roles. Reference the Lute project's issue tracker for any known parsing vulnerabilities.

Severity

Moderate

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
None
Scope
Unchanged
Confidentiality
None
Integrity
Low
Availability
None

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:N/S:U/C:N/I:L/A:N

CVE ID

No known CVE

Weaknesses

Uncontrolled Resource Consumption

The product does not properly control the allocation and maintenance of a limited resource. Learn more on MITRE.

Missing Authorization

The product does not perform an authorization check when an actor attempts to access a resource or perform an action. Learn more on MITRE.

Credits