fix: refactor sanitize

This commit is contained in:
Philipinho
2026-04-27 15:16:26 +01:00
parent a573acedd0
commit ec83fc82d5
7 changed files with 64 additions and 26 deletions
+4 -1
View File
@@ -110,7 +110,7 @@
"react": "^18.3.1", "react": "^18.3.1",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"sanitize-filename-ts": "1.0.2", "sanitize-filename": "1.6.3",
"socket.io": "^4.8.3", "socket.io": "^4.8.3",
"stripe": "^17.7.0", "stripe": "^17.7.0",
"tlds": "^1.261.0", "tlds": "^1.261.0",
@@ -165,6 +165,9 @@
"transform": { "transform": {
"^.+\\.(t|j)s$": "ts-jest" "^.+\\.(t|j)s$": "ts-jest"
}, },
"transformIgnorePatterns": [
"/node_modules/(?!(\\.pnpm/)?(nanoid|uuid|image-dimensions|marked)(@|/))"
],
"collectCoverageFrom": [ "collectCoverageFrom": [
"**/*.(t|j)s" "**/*.(t|j)s"
], ],
Binary file not shown.
+28 -6
View File
@@ -1,6 +1,6 @@
import * as path from 'path'; import * as path from 'path';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
import { sanitize } from 'sanitize-filename-ts'; import sanitize = require('sanitize-filename');
import { FastifyRequest } from 'fastify'; import { FastifyRequest } from 'fastify';
import { Readable, Transform } from 'stream'; import { Readable, Transform } from 'stream';
@@ -72,11 +72,33 @@ export function extractDateFromUuid7(uuid7: string) {
return new Date(timestamp); return new Date(timestamp);
} }
export function sanitizeFileName(fileName: string): string { export type SanitizeFileNameOptions = {
const sanitizedFilename = sanitize(fileName) /** Keep spaces and `#` instead of replacing them with `_`. Useful for
.replace(/ /g, '_') * download filenames where readability matters. Defaults to false. */
.replace(/#/g, '_'); preserveSpaces?: boolean;
return sanitizedFilename.slice(0, 255); };
export function sanitizeFileName(
fileName: string,
options: SanitizeFileNameOptions = {},
): string {
// Decode percent-encoded sequences so that bypasses like "..%2F" reach
// sanitize() as literal "../" and get stripped. sanitize-filename only
// strips literal characters and won't catch encoded path separators
// on its own.
const decoded = fileName.replace(/%[0-9a-fA-F]{2}/g, (m) => {
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 { export function removeAccent(str: string): string {
@@ -53,7 +53,6 @@ import { EnvironmentService } from '../../integrations/environment/environment.s
import { TokenService } from '../auth/services/token.service'; import { TokenService } from '../auth/services/token.service';
import { JwtAttachmentPayload, JwtType } from '../auth/dto/jwt-payload'; import { JwtAttachmentPayload, JwtType } from '../auth/dto/jwt-payload';
import * as path from 'path'; import * as path from 'path';
import { sanitize } from 'sanitize-filename-ts';
import { AttachmentInfoDto, RemoveIconDto } from './dto/attachment.dto'; import { AttachmentInfoDto, RemoveIconDto } from './dto/attachment.dto';
import { PageAccessService } from '../page/page-access/page-access.service'; import { PageAccessService } from '../page/page-access/page-access.service';
import { AuditEvent, AuditResource } from '../../common/events/audit-events'; import { AuditEvent, AuditResource } from '../../common/events/audit-events';
@@ -357,13 +356,19 @@ export class AttachmentController {
throw new BadRequestException('Invalid image attachment type'); throw new BadRequestException('Invalid image attachment type');
} }
if (!fileName || sanitize(fileName) !== fileName) { if (!fileName) {
throw new BadRequestException('Invalid file name'); throw new BadRequestException('Invalid file name');
} }
const filenameWithoutExt = path.basename(fileName, path.extname(fileName)); const ext = path.extname(fileName);
if (!isValidUUID(filenameWithoutExt)) { const filenameWithoutExt = path.basename(fileName, ext);
throw new BadRequestException('Invalid file id');
if (
!ext ||
!isValidUUID(filenameWithoutExt) ||
`${filenameWithoutExt}${ext}` !== fileName
) {
throw new BadRequestException('Invalid file name');
} }
const filePath = `${getAttachmentFolderPath(attachmentType, workspace.id)}/${fileName}`; const filePath = `${getAttachmentFolderPath(attachmentType, workspace.id)}/${fileName}`;
@@ -23,9 +23,12 @@ import {
SpaceCaslSubject, SpaceCaslSubject,
} from '../../core/casl/interfaces/space-ability.type'; } from '../../core/casl/interfaces/space-ability.type';
import { FastifyReply } from 'fastify'; import { FastifyReply } from 'fastify';
import { sanitize } from 'sanitize-filename-ts';
import { getExportExtension } from './utils'; import { getExportExtension } from './utils';
import { getMimeType, getPageTitle } from '../../common/helpers'; import {
getMimeType,
getPageTitle,
sanitizeFileName,
} from '../../common/helpers';
import * as path from 'path'; import * as path from 'path';
import { AuditEvent, AuditResource } from '../../common/events/audit-events'; import { AuditEvent, AuditResource } from '../../common/events/audit-events';
import { import {
@@ -85,7 +88,9 @@ export class ExportController {
if (result.type === 'file') { if (result.type === 'file') {
const ext = getExportExtension(dto.format); 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)); const contentType = getMimeType(path.extname(fileName));
res.headers({ res.headers({
@@ -96,7 +101,9 @@ export class ExportController {
res.send(result.content); res.send(result.content);
} else { } else {
const fileName = sanitize(page.title || 'untitled') + '.zip'; const fileName =
sanitizeFileName(page.title || 'untitled', { preserveSpaces: true }) +
'.zip';
res.headers({ res.headers({
'Content-Type': 'application/zip', 'Content-Type': 'application/zip',
@@ -144,7 +151,9 @@ export class ExportController {
'Content-Type': 'application/zip', 'Content-Type': 'application/zip',
'Content-Disposition': 'Content-Disposition':
'attachment; filename="' + 'attachment; filename="' +
encodeURIComponent(sanitize(exportFile.fileName)) + encodeURIComponent(
sanitizeFileName(exportFile.fileName, { preserveSpaces: true }),
) +
'"', '"',
}); });
@@ -1,7 +1,6 @@
import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { PageRepo } from '@docmost/db/repos/page/page.repo'; import { PageRepo } from '@docmost/db/repos/page/page.repo';
import { MultipartFile } from '@fastify/multipart'; import { MultipartFile } from '@fastify/multipart';
import { sanitize } from 'sanitize-filename-ts';
import * as path from 'path'; import * as path from 'path';
import { import {
htmlToJson, htmlToJson,
@@ -53,8 +52,8 @@ export class ImportService {
const file = await filePromise; const file = await filePromise;
const fileBuffer = await file.toBuffer(); const fileBuffer = await file.toBuffer();
const fileExtension = path.extname(file.filename).toLowerCase(); const fileExtension = path.extname(file.filename).toLowerCase();
const fileName = sanitize( const fileName = sanitizeFileName(
path.basename(file.filename, fileExtension).slice(0, 255), path.basename(file.filename, fileExtension),
); );
const fileContent = fileBuffer.toString(); const fileContent = fileBuffer.toString();
+6 -6
View File
@@ -697,9 +697,9 @@ importers:
rxjs: rxjs:
specifier: ^7.8.2 specifier: ^7.8.2
version: 7.8.2 version: 7.8.2
sanitize-filename-ts: sanitize-filename:
specifier: 1.0.2 specifier: 1.6.3
version: 1.0.2 version: 1.6.3
socket.io: socket.io:
specifier: ^4.8.3 specifier: ^4.8.3
version: 4.8.3 version: 4.8.3
@@ -9570,8 +9570,8 @@ packages:
safer-buffer@2.1.2: safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sanitize-filename-ts@1.0.2: sanitize-filename@1.6.3:
resolution: {integrity: sha512-bON2VOJoappmaBHlnxvBNk5R7HkUAsirf5m1M5Kz15uZykDGbHfGPCQNcEQKR8HrQhgh9CmQ6Xe9y71yM9ywkw==} resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==}
sass@1.51.0: sass@1.51.0:
resolution: {integrity: sha512-haGdpTgywJTvHC2b91GSq+clTKGbtkkZmVAb82jZQN/wTy6qs8DdFm2lhEQbEwrY0QDRgSQ3xDurqM977C3noA==} resolution: {integrity: sha512-haGdpTgywJTvHC2b91GSq+clTKGbtkkZmVAb82jZQN/wTy6qs8DdFm2lhEQbEwrY0QDRgSQ3xDurqM977C3noA==}
@@ -20900,7 +20900,7 @@ snapshots:
safer-buffer@2.1.2: {} safer-buffer@2.1.2: {}
sanitize-filename-ts@1.0.2: sanitize-filename@1.6.3:
dependencies: dependencies:
truncate-utf8-bytes: 1.0.2 truncate-utf8-bytes: 1.0.2