Found via the same technique as the MCP path-traversal finding: tracing
a class of very recently disclosed bugs (e.g. GHSA-p4qw-vx5p-g984,
"publish filter for database backlinks consults the forbidden list
instead of the visibility list", published Jul 30, 2026) to check every
other function with the same shape. Six more instances of the identical
root cause were found, none of them the same code path as the already-
published bug.
Summary
SiYuan's publish-access model has two independent, differently-scoped
controls per notebook/document, stored on the same PublishAccessItem
struct:
type PublishAccessItem struct {
ID string `json:"id"`
Visible bool `json:"visible"` // 是否发布可见 (shown in the published site's navigation)
Password string `json:"password"`
Disable bool `json:"disable"` // 是否禁止发布 (forbidden from publish access entirely)
}
Visible=false only affects navigation/listing cosmetics, an
"unlisted" item is still meant to be reachable if a reader has a direct
reference to it (e.g. via search or a backlink), which is why the
canonical, correct access-control check,
checkBlockTreeAccessableByPublishAccess()
(kernel/model/publish_access.go:224), consults
GetDisablePublishAccess() (the Disable field), not
GetInvisiblePublishAccess() (the Visible field), as the actual
security boundary. Disable=true is meant to make a notebook/document
completely inaccessible to anonymous publish-mode readers regardless of
how they try to reach it.
Six separate reader-facing filter functions in the same file consult
GetInvisiblePublishAccess() instead of GetDisablePublishAccess(),
meaning content explicitly marked Disable=true by the workspace owner
is not filtered out of these six result sets, as long as it is also
Visible=true (a legitimate, independent combination an admin can set:
"listed in navigation" and "forbidden from publish access" are not
mutually exclusive settings). This is CWE-863 (Incorrect Authorization),
the same class and shape as GHSA-p4qw-vx5p-g984, but in six different
code paths that advisory did not cover.
Details
All six functions are in kernel/model/publish_access.go and share the
identical pattern, publishIgnore := GetInvisiblePublishAccess(publishAccess)
where the canonical check uses GetDisablePublishAccess:
| Function |
Endpoint |
Handler location |
FilterBlocksByPublishAccess |
POST /api/search/listInvalidBlockRefs, POST /api/attr/getBookmarkLabels, POST /api/search/fullTextSearchBlock |
kernel/api/search.go:59, kernel/api/bookmark.go:37, kernel/api/search.go:485/524/673 |
FilterSearchDocsByPublishAccess |
POST /api/filetree/searchDocs |
kernel/api/filetree.go:1102 |
FilterPathsByPublishAccess |
POST /api/ref/getBacklink, POST /api/ref/getBacklink2 |
kernel/api/ref.go:180-181, 229-230 |
FilterAssetContentByPublishAccess |
POST /api/search/getAssetContent, POST /api/search/getAssetContentByPath, POST /api/search/fullTextSearchAssetContent |
kernel/api/search.go:84, 110, 153 |
FilterCriteriaByPublishAccess |
POST /api/storage/getCriteria |
kernel/api/storage.go:252 |
FilterRecentDocsByPublishAccess |
POST /api/storage/getRecentDocs |
kernel/api/storage.go:336 |
FilterGraphByPublishIgnore (called with GetInvisiblePublishAccess inline, not even via a named wrapper) |
POST /api/graph/getGraph, POST /api/graph/getLocalGraph |
kernel/api/graph.go:93-97, 153-157 |
FilterTagsByPublishIgnore (same inline pattern) |
POST /api/tag/getTag |
kernel/api/tag.go:56-60 |
That makes eight confirmed reader-facing endpoints/functions sharing
this exact root cause, not six. The graph and tag endpoints don't even
go through a named Filter...ByPublishAccess wrapper function, the
call site itself does publishIgnore := model.GetInvisiblePublishAccess(publishAccess)
directly inline, e.g. kernel/api/graph.go:93-97:
if model.IsReadOnlyRoleContext(c) {
publishAccess := model.GetPublishAccess()
publishIgnore := model.GetInvisiblePublishAccess(publishAccess)
nodes, links = model.FilterGraphByPublishIgnore(publishIgnore, nodes, links)
}
and identically in kernel/api/tag.go:56-60. This means the graph view
(document relationship visualization) and the tag list/counts both leak
the existence and metadata of Disable=true documents to anonymous
publish-mode visitors, in addition to the six functions above.
A ninth instance is in kernel/api/system.go:621-625, the handler for
POST /api/system/getConf (kernel/api/system.go:594, the endpoint the
frontend calls on load to fetch UI configuration, including the saved
UI layout/open-tabs state):
if model.IsReadOnlyRoleContext(c) {
publishAccess := model.GetPublishAccess()
publishIgnore := model.GetInvisiblePublishAccess(publishAccess)
maskedConf = model.FilterConfByPublishIgnore(publishIgnore, maskedConf)
}
Same inline pattern, same wrong list. Impact here is narrower than the
content-disclosure cases (it affects FilterUILayoutByPublishIgnore,
which strips saved-layout tab entries pointing at inaccessible
documents), but it still means a Disable=true document's ID and the
fact that it was open in a saved layout tab is not filtered out purely
because it happens to be Visible=true, a metadata leak of the same
root cause.
Every one of these call sites is gated identically, confirmed by reading
each handler in full:
if model.IsReadOnlyRoleContext(c) {
publishAccess := model.GetPublishAccess()
<result> = model.Filter...ByPublishAccess(c, publishAccess, <result>)
}
IsReadOnlyRoleContext is true specifically for RoleReader/anonymous
publish-mode visitors, confirming this filtering exists precisely to
enforce the publish access boundary for untrusted readers, the same
boundary checkBlockTreeAccessableByPublishAccess enforces correctly
elsewhere.
For contrast, the function covering the already-published
GHSA-p4qw-vx5p-g984 (database/attribute-view backlinks),
FilterAttributeViewBacklinksByPublishAccess
(kernel/model/publish_access.go, further down the same file), was
fixed to use GetDisablePublishAccess correctly; this advisory's six
functions were not part of that fix and still use the other list.
Step-by-step reproduction
- As the workspace admin, enable Publish and set up publish access for
at least two documents/notebooks:
- Document A:
Visible = true, Disable = true (explicitly
forbidden from publishing, but not hidden from navigation)
- Document B:
Visible = true, Disable = false (a normal,
legitimately published document)
- As an anonymous visitor (no session, no
AccessAuthCode), issue a
request that should never be able to see Document A's content, for
example:
curl -s -X POST http://<target>:6806/api/filetree/searchDocs \
-H "Content-Type: application/json" \
-d '{"k":"<a keyword unique to Document A content>"}'
- Expected if correctly protected (matching the behavior of the
already-fixed database-backlinks case): Document A never appears,
regardless of the search keyword, because Disable=true should make
it fully inaccessible.
- Observed: Document A appears in the results, because
FilterSearchDocsByPublishAccess only excludes items on the
Invisible (Visible=false) list, and Document A is Visible=true.
- Repeat the same test against the other seven endpoints in the table
above (getBacklink/getBacklink2 for a block inside Document A,
getAssetContent/getAssetContentByPath/fullTextSearchAssetContent
for an asset embedded in Document A, getCriteria for a saved search
referencing Document A, getRecentDocs if Document A was recently
opened, listInvalidBlockRefs/getBookmarkLabels/
fullTextSearchBlock for blocks within Document A,
POST /api/graph/getGraph/getLocalGraph to confirm Document A's
node still appears in the anonymous visitor's graph view, and
POST /api/tag/getTag to confirm tags used only in Document A still
appear with correct counts), each leaks content from or about the
Disable=true document in the same way.
Impact
A workspace owner who publishes a notebook and marks specific
documents within it as Disable=true, believing this makes them fully
inaccessible to anonymous visitors (the documented purpose of that
field, and the behavior the canonical access check correctly provides),
has that expectation silently violated across six different features:
search, document search, backlinks/backmentions, asset content search,
saved search criteria, and recent documents. Anonymous, unauthenticated
readers of the published site can discover and read content from
documents the owner explicitly tried to forbid from publishing, as long
as those documents remain Visible=true (not hidden from navigation),
which is an independent and entirely reasonable setting for an owner to
have chosen (e.g. "list it in the nav, but the content itself should
never be directly viewable without being explicitly enabled").
## Affected products
| Field | Value |
|---|---|
| Ecosystem | **Go** |
| Package name | `github.com/siyuan-note/siyuan/kernel` |
| Affected versions | Present at current HEAD (commit `eef1056`/`1673b75`, reviewed 2026-08-03); the six affected functions were not part of the `GHSA-p4qw-vx5p-g984` fix (Jul 30, 2026), which corrected the same root cause in a different function. |
| Patched versions | *(none yet, leave blank until a fix is released)* |
## Severity
| Field | Value |
|---|---|
| Vector string | `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N` |
| Score | **7.5 (High)**, matching the severity class of the already-published sibling advisory: network vector, low complexity, no privileges or user interaction required (fully anonymous), high confidentiality impact via disclosure of content the owner explicitly marked forbidden, no integrity/availability impact since these are all read-only endpoints. |
## Weaknesses (CWE)
- **CWE-863**: Incorrect Authorization (primary), same classification as `GHSA-p4qw-vx5p-g984`
## Notes for filing
- This is the same root cause and shape as `GHSA-p4qw-vx5p-g984`, just
in six different functions that fix did not reach. Worth linking to
it directly when filing, and worth the maintainers doing one final
pass grepping `GetInvisiblePublishAccess` across
`kernel/model/publish_access.go` to confirm no seventh instance
remains, since this is now the second time this exact confusion (
`Visible`/navigation list vs. `Disable`/access-control list) has
appeared across multiple call sites in the same file.
- Suggested fix: change all six functions to call
`GetDisablePublishAccess(publishAccess)` instead of
`GetInvisiblePublishAccess(publishAccess)`, matching the canonical
`checkBlockTreeAccessableByPublishAccess` pattern. Given how easy this
specific mix-up has been to reintroduce, consider consolidating all
publish-access filtering through one shared helper that takes the
correct list internally, rather than requiring every new filter
function to remember to call the right getter.
Found via the same technique as the MCP path-traversal finding: tracing
a class of very recently disclosed bugs (e.g. GHSA-p4qw-vx5p-g984,
"publish filter for database backlinks consults the forbidden list
instead of the visibility list", published Jul 30, 2026) to check every
other function with the same shape. Six more instances of the identical
root cause were found, none of them the same code path as the already-
published bug.
Summary
SiYuan's publish-access model has two independent, differently-scoped
controls per notebook/document, stored on the same
PublishAccessItemstruct:
Visible=falseonly affects navigation/listing cosmetics, an"unlisted" item is still meant to be reachable if a reader has a direct
reference to it (e.g. via search or a backlink), which is why the
canonical, correct access-control check,
checkBlockTreeAccessableByPublishAccess()(
kernel/model/publish_access.go:224), consultsGetDisablePublishAccess()(theDisablefield), notGetInvisiblePublishAccess()(theVisiblefield), as the actualsecurity boundary.
Disable=trueis meant to make a notebook/documentcompletely inaccessible to anonymous publish-mode readers regardless of
how they try to reach it.
Six separate reader-facing filter functions in the same file consult
GetInvisiblePublishAccess()instead ofGetDisablePublishAccess(),meaning content explicitly marked
Disable=trueby the workspace owneris not filtered out of these six result sets, as long as it is also
Visible=true(a legitimate, independent combination an admin can set:"listed in navigation" and "forbidden from publish access" are not
mutually exclusive settings). This is CWE-863 (Incorrect Authorization),
the same class and shape as
GHSA-p4qw-vx5p-g984, but in six differentcode paths that advisory did not cover.
Details
All six functions are in
kernel/model/publish_access.goand share theidentical pattern,
publishIgnore := GetInvisiblePublishAccess(publishAccess)where the canonical check uses
GetDisablePublishAccess:FilterBlocksByPublishAccessPOST /api/search/listInvalidBlockRefs,POST /api/attr/getBookmarkLabels,POST /api/search/fullTextSearchBlockkernel/api/search.go:59,kernel/api/bookmark.go:37,kernel/api/search.go:485/524/673FilterSearchDocsByPublishAccessPOST /api/filetree/searchDocskernel/api/filetree.go:1102FilterPathsByPublishAccessPOST /api/ref/getBacklink,POST /api/ref/getBacklink2kernel/api/ref.go:180-181, 229-230FilterAssetContentByPublishAccessPOST /api/search/getAssetContent,POST /api/search/getAssetContentByPath,POST /api/search/fullTextSearchAssetContentkernel/api/search.go:84, 110, 153FilterCriteriaByPublishAccessPOST /api/storage/getCriteriakernel/api/storage.go:252FilterRecentDocsByPublishAccessPOST /api/storage/getRecentDocskernel/api/storage.go:336FilterGraphByPublishIgnore(called withGetInvisiblePublishAccessinline, not even via a named wrapper)POST /api/graph/getGraph,POST /api/graph/getLocalGraphkernel/api/graph.go:93-97, 153-157FilterTagsByPublishIgnore(same inline pattern)POST /api/tag/getTagkernel/api/tag.go:56-60That makes eight confirmed reader-facing endpoints/functions sharing
this exact root cause, not six. The graph and tag endpoints don't even
go through a named
Filter...ByPublishAccesswrapper function, thecall site itself does
publishIgnore := model.GetInvisiblePublishAccess(publishAccess)directly inline, e.g.
kernel/api/graph.go:93-97:and identically in
kernel/api/tag.go:56-60. This means the graph view(document relationship visualization) and the tag list/counts both leak
the existence and metadata of
Disable=truedocuments to anonymouspublish-mode visitors, in addition to the six functions above.
A ninth instance is in
kernel/api/system.go:621-625, the handler forPOST /api/system/getConf(kernel/api/system.go:594, the endpoint thefrontend calls on load to fetch UI configuration, including the saved
UI layout/open-tabs state):
Same inline pattern, same wrong list. Impact here is narrower than the
content-disclosure cases (it affects
FilterUILayoutByPublishIgnore,which strips saved-layout tab entries pointing at inaccessible
documents), but it still means a
Disable=truedocument's ID and thefact that it was open in a saved layout tab is not filtered out purely
because it happens to be
Visible=true, a metadata leak of the sameroot cause.
Every one of these call sites is gated identically, confirmed by reading
each handler in full:
IsReadOnlyRoleContextis true specifically forRoleReader/anonymouspublish-mode visitors, confirming this filtering exists precisely to
enforce the publish access boundary for untrusted readers, the same
boundary
checkBlockTreeAccessableByPublishAccessenforces correctlyelsewhere.
For contrast, the function covering the already-published
GHSA-p4qw-vx5p-g984(database/attribute-view backlinks),FilterAttributeViewBacklinksByPublishAccess(
kernel/model/publish_access.go, further down the same file), wasfixed to use
GetDisablePublishAccesscorrectly; this advisory's sixfunctions were not part of that fix and still use the other list.
Step-by-step reproduction
at least two documents/notebooks:
Visible = true,Disable = true(explicitlyforbidden from publishing, but not hidden from navigation)
Visible = true,Disable = false(a normal,legitimately published document)
AccessAuthCode), issue arequest that should never be able to see Document A's content, for
example:
already-fixed database-backlinks case): Document A never appears,
regardless of the search keyword, because
Disable=trueshould makeit fully inaccessible.
FilterSearchDocsByPublishAccessonly excludes items on theInvisible(Visible=false) list, and Document A isVisible=true.above (
getBacklink/getBacklink2for a block inside Document A,getAssetContent/getAssetContentByPath/fullTextSearchAssetContentfor an asset embedded in Document A,
getCriteriafor a saved searchreferencing Document A,
getRecentDocsif Document A was recentlyopened,
listInvalidBlockRefs/getBookmarkLabels/fullTextSearchBlockfor blocks within Document A,POST /api/graph/getGraph/getLocalGraphto confirm Document A'snode still appears in the anonymous visitor's graph view, and
POST /api/tag/getTagto confirm tags used only in Document A stillappear with correct counts), each leaks content from or about the
Disable=truedocument in the same way.Impact
A workspace owner who publishes a notebook and marks specific
documents within it as
Disable=true, believing this makes them fullyinaccessible to anonymous visitors (the documented purpose of that
field, and the behavior the canonical access check correctly provides),
has that expectation silently violated across six different features:
search, document search, backlinks/backmentions, asset content search,
saved search criteria, and recent documents. Anonymous, unauthenticated
readers of the published site can discover and read content from
documents the owner explicitly tried to forbid from publishing, as long
as those documents remain
Visible=true(not hidden from navigation),which is an independent and entirely reasonable setting for an owner to
have chosen (e.g. "list it in the nav, but the content itself should
never be directly viewable without being explicitly enabled").