From ec83fc82d54bf3728eaa63b20eb4abcb5aef1d97 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:16:26 +0100 Subject: [PATCH] fix: refactor sanitize --- apps/server/package.json | 5 ++- apps/server/src/common/helpers/utils.spec.ts | Bin 0 -> 3319 bytes apps/server/src/common/helpers/utils.ts | 34 ++++++++++++++---- .../core/attachment/attachment.controller.ts | 15 +++++--- .../integrations/export/export.controller.ts | 19 +++++++--- .../import/services/import.service.ts | 5 ++- pnpm-lock.yaml | 12 +++---- 7 files changed, 64 insertions(+), 26 deletions(-) create mode 100644 apps/server/src/common/helpers/utils.spec.ts diff --git a/apps/server/package.json b/apps/server/package.json index 97af8db7..c36e9002 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -110,7 +110,7 @@ "react": "^18.3.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2", - "sanitize-filename-ts": "1.0.2", + "sanitize-filename": "1.6.3", "socket.io": "^4.8.3", "stripe": "^17.7.0", "tlds": "^1.261.0", @@ -165,6 +165,9 @@ "transform": { "^.+\\.(t|j)s$": "ts-jest" }, + "transformIgnorePatterns": [ + "/node_modules/(?!(\\.pnpm/)?(nanoid|uuid|image-dimensions|marked)(@|/))" + ], "collectCoverageFrom": [ "**/*.(t|j)s" ], diff --git a/apps/server/src/common/helpers/utils.spec.ts b/apps/server/src/common/helpers/utils.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f444e007163fb39fade37d42ba08652ec0621ec3 GIT binary patch literal 3319 zcmc&$&2r;J5ayhx=t{~+24~_m>&q^?6l~!WWZ8n#{#0r#wX7+|l6GbkM}$;y;sGdL zBF~XG*(ae}e;(OM2)W>sr`0pl{q@)1v|1Orac~J%X3DAGaG^Aw$~D3jBqm=&AWllB zv<;pg9K>iNqm~#3yWYVB!ZEyf36}=|Telb!S!xHNb-9r%Jhn2y@xU2CIe{`t!*{fm zfA0dT)`dplE*!b~s}GajXI()-CyI5OjVr=-Eggc&iqgU1 z$R5I{KmV%#&;em>o&a7H7&(l4C>f8&816#e>=}=R%fG|0-}$^o=xR*m;?4WuCRE_B zxsHW3X4^nvkjgA3^dQd5rA9B}O6M}RAQOiM@(fA(I2p>tT5jt>AD^7SXf~fM zS|>{zA_SL{bpfHq#DOHOP;?eC@zWoFLza`N0mfn>jU*LqQ>VkaTXp4KJr=N#)~Tcd zsSV1wqNpi!FF5JIr3fZ39d1w4vO+Tfo5QlIzms55bg8nCqNZYjMLI{hNt*?!DJ&^n z@}dMfVWzk?sftqA@Mg?9L@3R}lS3dNp>8?R-h7w15tVDOdRlfuSAW>{29=3r?(Smm z`nP)Nw@Q14r@khc7{&d+-oei2vmO>hZ4sRa|Qtp*Y zn+Es+t^I|9dTqY{SV|pIs9qb83et*LupQ+JgBHy>{#-~Ve_l96Bpqs;a^1vZ8!vA6 zBPcVMmr;s5sSPEujkl#6^w2Em`8cH+BMg45;3pdM;OI#+#9hKLkZ0gbNyF;66QHiY zha0`z>dQD%Qc0~FHQF5jIPP2DDQeY6Bte5tH?B4V*MqYV!r!<{>w9pkCkTJ1D { + try { + return decodeURIComponent(m); + } catch { + return m; + } + }); + + const sanitized = sanitize(decoded); + if (options.preserveSpaces) { + return sanitized; + } + return sanitized.replace(/ /g, '_').replace(/#/g, '_'); } export function removeAccent(str: string): string { diff --git a/apps/server/src/core/attachment/attachment.controller.ts b/apps/server/src/core/attachment/attachment.controller.ts index 784e527e..73605819 100644 --- a/apps/server/src/core/attachment/attachment.controller.ts +++ b/apps/server/src/core/attachment/attachment.controller.ts @@ -53,7 +53,6 @@ import { EnvironmentService } from '../../integrations/environment/environment.s import { TokenService } from '../auth/services/token.service'; import { JwtAttachmentPayload, JwtType } from '../auth/dto/jwt-payload'; import * as path from 'path'; -import { sanitize } from 'sanitize-filename-ts'; import { AttachmentInfoDto, RemoveIconDto } from './dto/attachment.dto'; import { PageAccessService } from '../page/page-access/page-access.service'; import { AuditEvent, AuditResource } from '../../common/events/audit-events'; @@ -357,13 +356,19 @@ export class AttachmentController { throw new BadRequestException('Invalid image attachment type'); } - if (!fileName || sanitize(fileName) !== fileName) { + if (!fileName) { throw new BadRequestException('Invalid file name'); } - const filenameWithoutExt = path.basename(fileName, path.extname(fileName)); - if (!isValidUUID(filenameWithoutExt)) { - throw new BadRequestException('Invalid file id'); + const ext = path.extname(fileName); + const filenameWithoutExt = path.basename(fileName, ext); + + if ( + !ext || + !isValidUUID(filenameWithoutExt) || + `${filenameWithoutExt}${ext}` !== fileName + ) { + throw new BadRequestException('Invalid file name'); } const filePath = `${getAttachmentFolderPath(attachmentType, workspace.id)}/${fileName}`; diff --git a/apps/server/src/integrations/export/export.controller.ts b/apps/server/src/integrations/export/export.controller.ts index 1ce3f8c8..140622da 100644 --- a/apps/server/src/integrations/export/export.controller.ts +++ b/apps/server/src/integrations/export/export.controller.ts @@ -23,9 +23,12 @@ import { SpaceCaslSubject, } from '../../core/casl/interfaces/space-ability.type'; import { FastifyReply } from 'fastify'; -import { sanitize } from 'sanitize-filename-ts'; import { getExportExtension } from './utils'; -import { getMimeType, getPageTitle } from '../../common/helpers'; +import { + getMimeType, + getPageTitle, + sanitizeFileName, +} from '../../common/helpers'; import * as path from 'path'; import { AuditEvent, AuditResource } from '../../common/events/audit-events'; import { @@ -85,7 +88,9 @@ export class ExportController { if (result.type === 'file') { const ext = getExportExtension(dto.format); - const fileName = sanitize(page.title || 'untitled') + ext; + const fileName = + sanitizeFileName(page.title || 'untitled', { preserveSpaces: true }) + + ext; const contentType = getMimeType(path.extname(fileName)); res.headers({ @@ -96,7 +101,9 @@ export class ExportController { res.send(result.content); } else { - const fileName = sanitize(page.title || 'untitled') + '.zip'; + const fileName = + sanitizeFileName(page.title || 'untitled', { preserveSpaces: true }) + + '.zip'; res.headers({ 'Content-Type': 'application/zip', @@ -144,7 +151,9 @@ export class ExportController { 'Content-Type': 'application/zip', 'Content-Disposition': 'attachment; filename="' + - encodeURIComponent(sanitize(exportFile.fileName)) + + encodeURIComponent( + sanitizeFileName(exportFile.fileName, { preserveSpaces: true }), + ) + '"', }); diff --git a/apps/server/src/integrations/import/services/import.service.ts b/apps/server/src/integrations/import/services/import.service.ts index 231a6c89..66c57585 100644 --- a/apps/server/src/integrations/import/services/import.service.ts +++ b/apps/server/src/integrations/import/services/import.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { PageRepo } from '@docmost/db/repos/page/page.repo'; import { MultipartFile } from '@fastify/multipart'; -import { sanitize } from 'sanitize-filename-ts'; import * as path from 'path'; import { htmlToJson, @@ -53,8 +52,8 @@ export class ImportService { const file = await filePromise; const fileBuffer = await file.toBuffer(); const fileExtension = path.extname(file.filename).toLowerCase(); - const fileName = sanitize( - path.basename(file.filename, fileExtension).slice(0, 255), + const fileName = sanitizeFileName( + path.basename(file.filename, fileExtension), ); const fileContent = fileBuffer.toString(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 56dd6d79..fe7a2042 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -697,9 +697,9 @@ importers: rxjs: specifier: ^7.8.2 version: 7.8.2 - sanitize-filename-ts: - specifier: 1.0.2 - version: 1.0.2 + sanitize-filename: + specifier: 1.6.3 + version: 1.6.3 socket.io: specifier: ^4.8.3 version: 4.8.3 @@ -9570,8 +9570,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sanitize-filename-ts@1.0.2: - resolution: {integrity: sha512-bON2VOJoappmaBHlnxvBNk5R7HkUAsirf5m1M5Kz15uZykDGbHfGPCQNcEQKR8HrQhgh9CmQ6Xe9y71yM9ywkw==} + sanitize-filename@1.6.3: + resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} sass@1.51.0: resolution: {integrity: sha512-haGdpTgywJTvHC2b91GSq+clTKGbtkkZmVAb82jZQN/wTy6qs8DdFm2lhEQbEwrY0QDRgSQ3xDurqM977C3noA==} @@ -20900,7 +20900,7 @@ snapshots: safer-buffer@2.1.2: {} - sanitize-filename-ts@1.0.2: + sanitize-filename@1.6.3: dependencies: truncate-utf8-bytes: 1.0.2