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 %>'