From 2490c312c97926d0b5975c15780db593b8d7ade6 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 18 Jun 2025 14:20:41 +0000 Subject: [PATCH 01/14] chore: incrementing version number - v4.4.4 --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index fae65ae63f..4033fe1a27 100644 --- a/install/package.json +++ b/install/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "4.4.3", + "version": "4.4.4", "homepage": "https://www.nodebb.org", "repository": { "type": "git", From 7b14e2677544938f3a121363783b06071e15a4f5 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 18 Jun 2025 14:20:41 +0000 Subject: [PATCH 02/14] chore: update changelog for v4.4.4 --- CHANGELOG.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec4faa3658..9010b97b06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,49 @@ +#### v4.4.4 (2025-06-18) + +##### Chores + +* incrementing version number - v4.4.3 (d354c2eb) +* update changelog for v4.4.3 (0c9297f8) +* incrementing version number - v4.4.2 (55c510ae) +* incrementing version number - v4.4.1 (5ae79b4e) +* incrementing version number - v4.4.0 (0a75eee3) +* incrementing version number - v4.3.2 (b92b5d80) +* incrementing version number - v4.3.1 (308e6b9f) +* incrementing version number - v4.3.0 (bff291db) +* incrementing version number - v4.2.2 (17fecc24) +* incrementing version number - v4.2.1 (852a270c) +* incrementing version number - v4.2.0 (87581958) +* incrementing version number - v4.1.1 (b2afbb16) +* incrementing version number - v4.1.0 (36c80850) +* incrementing version number - v4.0.6 (4a52fb2e) +* incrementing version number - v4.0.5 (1792a62b) +* incrementing version number - v4.0.4 (b1125cce) +* incrementing version number - v4.0.3 (2b65c735) +* incrementing version number - v4.0.2 (73fe5fcf) +* incrementing version number - v4.0.1 (a461b758) +* incrementing version number - v4.0.0 (c1eaee45) + +##### New Features + +* link to post in preview timestamp (8c69c6a0) +* Add live reload functionality with Grunt watch and Socket.IO (#13489) (84d99a0f) +* closes #13484, post preview changes (14e30c4b) + +##### Bug Fixes + +* sanitize svg when uploading site-logo, default avatar and og:image (da2597f8) +* Revise package hash check in Docker entrypoint.sh (#13483) (6c5b2268) +* more edge cases (32faaba0) +* #13484, clear tooltip if cursor leaves link (0ebb31fe) + +##### Other Changes + +* fix lint (8ab034d8) + +##### Refactors + +* send single message (dc37789b) + #### v4.4.3 (2025-06-09) ##### Chores From 3e961257ec0904dbc3b3c64dab3d4cbdffcfbbd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Jun 2025 13:25:36 -0400 Subject: [PATCH 03/14] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c5d80e0fd..fb8b8702fe 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Our minimalist "Harmony" theme gets you going right away, no coding experience r NodeBB requires the following software to be installed: * A version of Node.js at least 20 or greater ([installation/upgrade instructions](https://github.com/nodesource/distributions)) -* MongoDB, version 3.6 or greater **or** Redis, version 2.8.9 or greater +* MongoDB, version 5 or greater **or** Redis, version 7.2 or greater * If you are using [clustering](https://docs.nodebb.org/configuring/scaling/) you need Redis installed and configured. * nginx, version 1.3.13 or greater (**only if** intending to use nginx to proxy requests to a NodeBB) From a8f4c5e63a93089ad334e9c927e9a0862e235394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 24 Jul 2025 10:34:37 -0400 Subject: [PATCH 04/14] fix: apply sanitizeSvg to regular uploads and uploads from manage uploads acp page --- src/controllers/admin/uploads.js | 44 -------------------------------- src/file.js | 40 +++++++++++++++++++++++++++++ test/files/dirty.svg | 4 +++ test/uploads.js | 9 +++++++ 4 files changed, 53 insertions(+), 44 deletions(-) create mode 100644 test/files/dirty.svg diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index ccd4261b36..0f70380695 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -4,7 +4,6 @@ const path = require('path'); const nconf = require('nconf'); const fs = require('fs'); const winston = require('winston'); -const sanitizeHtml = require('sanitize-html'); const meta = require('../../meta'); const posts = require('../../posts'); @@ -157,50 +156,11 @@ uploadsController.uploadCategoryPicture = async function (req, res, next) { return next(new Error('[[error:invalid-json]]')); } - if (uploadedFile.path.endsWith('.svg')) { - await sanitizeSvg(uploadedFile.path); - } - await validateUpload(uploadedFile, allowedImageTypes); const filename = `category-${params.cid}${path.extname(uploadedFile.name)}`; await uploadImage(filename, 'category', uploadedFile, req, res, next); }; -async function sanitizeSvg(filePath) { - const dirty = await fs.promises.readFile(filePath, 'utf8'); - const clean = sanitizeHtml(dirty, { - allowedTags: [ - 'svg', 'g', 'defs', 'linearGradient', 'radialGradient', 'stop', - 'circle', 'ellipse', 'polygon', 'polyline', 'path', 'rect', - 'line', 'text', 'tspan', 'use', 'symbol', 'clipPath', 'mask', 'pattern', - 'filter', 'feGaussianBlur', 'feOffset', 'feBlend', 'feColorMatrix', 'feMerge', 'feMergeNode', - ], - allowedAttributes: { - '*': [ - // Geometry - 'x', 'y', 'x1', 'x2', 'y1', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', - 'width', 'height', 'd', 'points', 'viewBox', 'transform', - - // Presentation - 'fill', 'stroke', 'stroke-width', 'opacity', - 'stop-color', 'stop-opacity', 'offset', 'style', 'class', - - // Text - 'text-anchor', 'font-size', 'font-family', - - // Misc - 'id', 'clip-path', 'mask', 'filter', 'gradientUnits', 'gradientTransform', - 'xmlns', 'preserveAspectRatio', - ], - }, - parser: { - lowerCaseTags: false, - lowerCaseAttributeNames: false, - }, - }); - await fs.promises.writeFile(filePath, clean); -} - uploadsController.uploadFavicon = async function (req, res, next) { const uploadedFile = req.files.files[0]; const allowedTypes = ['image/x-icon', 'image/vnd.microsoft.icon']; @@ -296,10 +256,6 @@ uploadsController.uploadOgImage = async function (req, res, next) { async function upload(name, req, res, next) { const uploadedFile = req.files.files[0]; - if (uploadedFile.path.endsWith('.svg')) { - await sanitizeSvg(uploadedFile.path); - } - await validateUpload(uploadedFile, allowedImageTypes); const filename = name + path.extname(uploadedFile.name); await uploadImage(filename, 'system', uploadedFile, req, res, next); diff --git a/src/file.js b/src/file.js index 639cc9f58c..4c3c911950 100644 --- a/src/file.js +++ b/src/file.js @@ -7,6 +7,7 @@ const winston = require('winston'); const { mkdirp } = require('mkdirp'); const mime = require('mime'); const graceful = require('graceful-fs'); +const sanitizeHtml = require('sanitize-html'); const slugify = require('./slugify'); @@ -27,6 +28,10 @@ file.saveFileToLocal = async function (filename, folder, tempPath) { winston.verbose(`Saving file ${filename} to : ${uploadPath}`); await mkdirp(path.dirname(uploadPath)); + if (tempPath.endsWith('.svg')) { + await sanitizeSvg(tempPath); + } + await fs.promises.copyFile(tempPath, uploadPath); return { url: `/assets/uploads/${folder ? `${folder}/` : ''}${filename}`, @@ -155,4 +160,39 @@ file.walk = async function (dir) { return files.reduce((a, f) => a.concat(f), []); }; +async function sanitizeSvg(filePath) { + const dirty = await fs.promises.readFile(filePath, 'utf8'); + const clean = sanitizeHtml(dirty, { + allowedTags: [ + 'svg', 'g', 'defs', 'linearGradient', 'radialGradient', 'stop', + 'circle', 'ellipse', 'polygon', 'polyline', 'path', 'rect', + 'line', 'text', 'tspan', 'use', 'symbol', 'clipPath', 'mask', 'pattern', + 'filter', 'feGaussianBlur', 'feOffset', 'feBlend', 'feColorMatrix', 'feMerge', 'feMergeNode', + ], + allowedAttributes: { + '*': [ + // Geometry + 'x', 'y', 'x1', 'x2', 'y1', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', + 'width', 'height', 'd', 'points', 'viewBox', 'transform', + + // Presentation + 'fill', 'stroke', 'stroke-width', 'opacity', + 'stop-color', 'stop-opacity', 'offset', 'style', 'class', + + // Text + 'text-anchor', 'font-size', 'font-family', + + // Misc + 'id', 'clip-path', 'mask', 'filter', 'gradientUnits', 'gradientTransform', + 'xmlns', 'preserveAspectRatio', + ], + }, + parser: { + lowerCaseTags: false, + lowerCaseAttributeNames: false, + }, + }); + await fs.promises.writeFile(filePath, clean); +} + require('./promisify')(file); diff --git a/test/files/dirty.svg b/test/files/dirty.svg new file mode 100644 index 0000000000..a948be59b2 --- /dev/null +++ b/test/files/dirty.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/test/uploads.js b/test/uploads.js index 3b361e36d3..4fc0e41a64 100644 --- a/test/uploads.js +++ b/test/uploads.js @@ -338,6 +338,15 @@ describe('Upload Controllers', () => { assert.equal(body[0].url, `${nconf.get('relative_path')}/assets/uploads/category/category-1.png`); }); + it('should upload svg as category image after cleaning it up', async () => { + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/admin/category/uploadpicture`, path.join(__dirname, '../test/files/dirty.svg'), { params: JSON.stringify({ cid: cid }) }, jar, csrf_token); + assert.equal(response.statusCode, 200); + assert(Array.isArray(body)); + assert.equal(body[0].url, `${nconf.get('relative_path')}/assets/uploads/category/category-1.svg`); + const svgContents = await fs.readFile(path.join(__dirname, '../test/uploads/category/category-1.svg'), 'utf-8'); + assert.strictEqual(svgContents.includes('