docs(security): scope out the CSRF token being readable from the page

The client attaches the token to every internal API call, so it has to be
readable by page script, and `./bootstrap` reissues one to any same-origin
request carrying the session cookie. Says what a double-submit token
establishes, and points reporters at what is in scope instead: how script
came to run in the app, and genuine failures of the protection.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Elian Doran
2026-08-21 13:44:43 +03:00
co-authored by Claude Opus 5
parent 3975ef7146
commit 3a7be0a814
+9
View File
@@ -69,6 +69,15 @@ User scripting (see Self-XSS above) still intentionally allows arbitrary JavaScr
#### Authenticated User Actions #### Authenticated User Actions
Actions that require valid authentication and only affect the authenticated user's own data are generally not vulnerabilities. Actions that require valid authentication and only affect the authenticated user's own data are generally not vulnerabilities.
#### The CSRF Token Being Readable From the Page
`glob.csrfToken` is readable by any script running on the page, and it is meant to be: the client attaches it to every internal API call as the `x-csrf-token` header, so a token the client could not read would be a client that could not reach its own backend. The half that must stay out of reach is the `trilium-csrf` cookie, which is `httpOnly` and `sameSite: strict`, and does.
That pairing is the whole of what a double-submit token claims to do. It establishes that a request came from our own page rather than from someone else's, and it was never a barrier to anything already running on our page. Concealing the value would not change that either: `./bootstrap` issues a fresh, valid token to any same-origin request carrying the session cookie, which is how the client itself recovers from an expired one.
So "the CSRF token can be read out of the page" is not a finding on its own, and neither is the follow-on that script holding it can call the internal API as the logged-in user. That describes what script execution is worth, not how it was obtained — and only the second half is a vulnerability.
**How it was obtained is the report we want.** Any path that gets JavaScript running in the app from content the user did not author — an imported note, a note arriving over sync, a clipped page, a filename, an attribute value — is in scope, however small the resulting foothold looks. So is a real failure of the protection: a state-changing endpoint that accepts a cross-origin request, a token that validates against a session other than the one it was issued for, or the session cookie losing `httpOnly` or its `sameSite` attribute. On the desktop the check is deliberately skipped for requests the renderer dispatches over the `trilium-app://` protocol, where Express sessions do not round-trip and there is nothing to validate against; requests reaching the desktop's HTTP listener over TCP are checked in full, and a way to have one treated as the other is in scope.
#### Private Addresses Reached by a Configured LLM Endpoint #### Private Addresses Reached by a Configured LLM Endpoint
Outbound requests the server makes on behalf of note content — link previews, image fetches, imports — are vetted against a strict rule: the hostname is resolved, every address behind it must be a public unicast one, and the connection is pinned to the addresses actually checked. Note content is not entitled to the network its host can see. Outbound requests the server makes on behalf of note content — link previews, image fetches, imports — are vetted against a strict rule: the hostname is resolved, every address behind it must be a public unicast one, and the connection is pinned to the addresses actually checked. Note content is not entitled to the network its host can see.