mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
fix: refactor sanitize
This commit is contained in:
@@ -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.
@@ -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();
|
||||||
|
|
||||||
|
|||||||
Generated
+6
-6
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user