From bc2075ae9d01ec602ba68e5b918d1cd3a78b157b Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Wed, 26 Nov 2025 19:20:54 +0100 Subject: [PATCH] refactor: use user-token for historyDelete too Previously, the user token was only used for the endpoint to delete the user itself. This commit adds that token to the history deletion as well. Signed-off-by: Philip Molares --- docs/content/dev/api.md | 18 +++++++------- lib/history.js | 39 +++++++++++++++++-------------- lib/web/statusRouter.js | 3 ++- public/docs/release-notes.md | 1 + public/js/history.js | 2 +- public/js/lib/common/constant.ejs | 1 + 6 files changed, 36 insertions(+), 28 deletions(-) diff --git a/docs/content/dev/api.md b/docs/content/dev/api.md index a8cd22a2..0ac65ef5 100644 --- a/docs/content/dev/api.md +++ b/docs/content/dev/api.md @@ -23,15 +23,15 @@ You have to replace *\* with either the alias or id of a note you want to ## User / History These endpoints return information about the current logged-in user and it's note history. If no user is logged-in, the most of this requests will fail with either a HTTP 403 or a JSON object containing `{"status":"forbidden"}`. -| Endpoint | HTTP-Method | Description | -| ----------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `/me` | `GET` | **Returns the profile data of the current logged-in user.**
The data is returned as a JSON object containing the user-id, the user's name and a url to the profile picture. | -| `/me/export` | `GET` | **Exports a zip-archive with all notes of the current user.** | -| `/history` | `GET` | **Returns a list of the last viewed notes.**
The list is returned as a JSON object with an array containing for each entry it's id, title, tags, last visit time and pinned status. | -| `/history` | `POST` | **Replace user's history with a new one.**
The body must be form-encoded and contain a field `history` with a JSON-encoded array like its returned from the server when exporting the history. | -| `/history` | `DELETE` | **Deletes the user's history.** | -| `/history/` | `POST` | **Toggles the pinned status in the history for a note.**
The body must be form-encoded and contain a field `pinned` that is either `true` or `false`. | -| `/history/` | `DELETE` | **Deletes a note from the user's history.** | +| Endpoint | HTTP-Method | Description | +| ------------------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/me` | `GET` | **Returns the profile data of the current logged-in user.**
The data is returned as a JSON object containing the user-id, the user's name and a url to the profile picture. | +| `/me/export` | `GET` | **Exports a zip-archive with all notes of the current user.** | +| `/history` | `GET` | **Returns a list of the last viewed notes.**
The list is returned as a JSON object with an array containing for each entry it's id, title, tags, last visit time and pinned status. | +| `/history` | `POST` | **Replace user's history with a new one.**
The body must be form-encoded and contain a field `history` with a JSON-encoded array like its returned from the server when exporting the history. | +| `/history?token=` | `DELETE` | **Deletes the user's history.**
Requires the user token since HedgeDoc 1.10.4 to prevent CSRF-attacks. The token can be obtained from the `/config` endpoint when logged-in. | +| `/history/` | `POST` | **Toggles the pinned status in the history for a note.**
The body must be form-encoded and contain a field `pinned` that is either `true` or `false`. | +| `/history/` | `DELETE` | **Deletes a note from the user's history.** | ## HedgeDoc-server These endpoints return information about the running HedgeDoc instance. diff --git a/lib/history.js b/lib/history.js index e0c16da5..9c9d35c0 100644 --- a/lib/history.js +++ b/lib/history.js @@ -174,26 +174,31 @@ function historyPost (req, res) { } function historyDelete (req, res) { - if (req.isAuthenticated()) { - const noteId = req.params.noteId - if (!noteId) { - setHistory(req.user.id, [], function (err, count) { + if (!req.isAuthenticated()) { + return errors.errorForbidden(res) + } + + const token = req.query.token + if (!token || token !== req.user.deleteToken) { + return errors.errorForbidden(res) + } + + const noteId = req.params.noteId + if (!noteId) { + setHistory(req.user.id, [], function (err, count) { + if (err) return errors.errorInternalError(res) + res.end() + }) + } else { + getHistory(req.user.id, function (err, history) { + if (err) return errors.errorInternalError(res) + if (!history) return errors.errorNotFound(res) + delete history[noteId] + setHistory(req.user.id, history, function (err, count) { if (err) return errors.errorInternalError(res) res.end() }) - } else { - getHistory(req.user.id, function (err, history) { - if (err) return errors.errorInternalError(res) - if (!history) return errors.errorNotFound(res) - delete history[noteId] - setHistory(req.user.id, history, function (err, count) { - if (err) return errors.errorInternalError(res) - res.end() - }) - }) - } - } else { - return errors.errorForbidden(res) + }) } } diff --git a/lib/web/statusRouter.js b/lib/web/statusRouter.js index 73540398..503cadda 100644 --- a/lib/web/statusRouter.js +++ b/lib/web/statusRouter.js @@ -112,7 +112,8 @@ statusRouter.get('/config', function (req, res) { allowedUploadMimeTypes: config.allowedUploadMimeTypes, linkifyHeaderStyle: config.linkifyHeaderStyle, cookiePolicy: config.cookiePolicy, - enableUploads: config.enableUploads + enableUploads: config.enableUploads, + userToken: req.user ? req.user.deleteToken : '' } res.set({ 'Cache-Control': 'private', // only cache by client diff --git a/public/docs/release-notes.md b/public/docs/release-notes.md index b6f81e44..8c457e33 100644 --- a/public/docs/release-notes.md +++ b/public/docs/release-notes.md @@ -8,6 +8,7 @@ - Allow links to protocols such as xmpp, webcal or geo - Switch from deprecated shortid to nanoid module, with 10 character long aliases in "public" links - Ensure compatibility with Node 24 +- Protect user history from accidental or malicious deletion by adding a CSRF-like token ### Bugfixes - Ignore the healthcheck endpoint in the "too busy" limiter diff --git a/public/js/history.js b/public/js/history.js index 55e6d61a..b262323e 100644 --- a/public/js/history.js +++ b/public/js/history.js @@ -295,7 +295,7 @@ export function postHistoryToServer (noteId, data, callback) { export function deleteServerHistory (noteId, callback) { $.ajax({ - url: `${serverurl}/history${noteId ? '/' + noteId : ''}`, + url: `${serverurl}/history${noteId ? '/' + noteId : ''}?token=${window.userToken}`, type: 'DELETE' }) .done(result => callback(null, result)) diff --git a/public/js/lib/common/constant.ejs b/public/js/lib/common/constant.ejs index e377bdf2..751e84b6 100644 --- a/public/js/lib/common/constant.ejs +++ b/public/js/lib/common/constant.ejs @@ -11,3 +11,4 @@ window.linkifyHeaderStyle = '<%- linkifyHeaderStyle %>' window.DROPBOX_APP_KEY = '<%- DROPBOX_APP_KEY %>' window.cookiePolicy = '<%- cookiePolicy %>' +window.userToken = '<%- userToken %>'