Merge branch 'main' into fire-pdf

This commit is contained in:
Philipinho
2026-04-30 03:05:06 +01:00
18 changed files with 1136 additions and 983 deletions
+6 -6
View File
@@ -1,7 +1,7 @@
{ {
"name": "client", "name": "client",
"private": true, "private": true,
"version": "0.80.0", "version": "0.80.1",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
@@ -31,8 +31,8 @@
"emoji-mart": "^5.6.0", "emoji-mart": "^5.6.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"highlightjs-sap-abap": "^0.3.0", "highlightjs-sap-abap": "^0.3.0",
"i18next": "^25.10.1", "i18next": "25.10.1",
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "3.0.6",
"jotai": "^2.18.1", "jotai": "^2.18.1",
"jotai-optics": "^0.4.0", "jotai-optics": "^0.4.0",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
@@ -42,7 +42,7 @@
"mantine-form-zod-resolver": "^1.3.0", "mantine-form-zod-resolver": "^1.3.0",
"mermaid": "^11.13.0", "mermaid": "^11.13.0",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"posthog-js": "1.363.1", "posthog-js": "1.372.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-arborist": "3.4.0", "react-arborist": "3.4.0",
"react-clear-modal": "^2.0.18", "react-clear-modal": "^2.0.18",
@@ -50,7 +50,7 @@
"react-drawio": "^1.0.7", "react-drawio": "^1.0.7",
"react-error-boundary": "^6.1.1", "react-error-boundary": "^6.1.1",
"react-helmet-async": "^3.0.0", "react-helmet-async": "^3.0.0",
"react-i18next": "^16.5.8", "react-i18next": "16.5.8",
"react-router-dom": "^7.13.1", "react-router-dom": "^7.13.1",
"semver": "^7.7.4", "semver": "^7.7.4",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",
@@ -74,7 +74,7 @@
"eslint-plugin-react-refresh": "^0.5.2", "eslint-plugin-react-refresh": "^0.5.2",
"globals": "^15.13.0", "globals": "^15.13.0",
"optics-ts": "^2.4.1", "optics-ts": "^2.4.1",
"postcss": "^8.5.8", "postcss": "^8.5.12",
"postcss-preset-mantine": "^1.18.0", "postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"prettier": "^3.8.1", "prettier": "^3.8.1",
@@ -140,7 +140,7 @@ export function PagePermissionList({
)} )}
</Group> </Group>
<ScrollArea mah={250} viewportRef={viewportRef}> <ScrollArea.Autosize mah={400} viewportRef={viewportRef}>
{sortedMembers.map((member) => ( {sortedMembers.map((member) => (
<PagePermissionItem <PagePermissionItem
key={`${member.type}-${member.id}`} key={`${member.type}-${member.id}`}
@@ -158,7 +158,7 @@ export function PagePermissionList({
<Loader size="xs" /> <Loader size="xs" />
</Center> </Center>
)} )}
</ScrollArea> </ScrollArea.Autosize>
</> </>
); );
} }
@@ -19,7 +19,9 @@ export const uploadAttachmentAction = handleAttachmentUpload({
}, },
validateFn: (file, allowMedia: boolean) => { validateFn: (file, allowMedia: boolean) => {
if ( if (
(file.type.includes("image/") || file.type.includes("video/")) && (file.type.includes("image/") ||
file.type.includes("video/") ||
file.type === "application/pdf") &&
!allowMedia !allowMedia
) { ) {
return false; return false;
+21 -17
View File
@@ -1,6 +1,6 @@
{ {
"name": "server", "name": "server",
"version": "0.80.0", "version": "0.80.1",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,
@@ -33,13 +33,13 @@
"@ai-sdk/google": "^3.0.52", "@ai-sdk/google": "^3.0.52",
"@ai-sdk/openai": "^3.0.47", "@ai-sdk/openai": "^3.0.47",
"@ai-sdk/openai-compatible": "^2.0.37", "@ai-sdk/openai-compatible": "^2.0.37",
"@aws-sdk/client-s3": "3.1014.0", "@aws-sdk/client-s3": "3.1037.0",
"@aws-sdk/lib-storage": "3.1014.0", "@aws-sdk/lib-storage": "3.1037.0",
"@aws-sdk/s3-request-presigner": "3.1014.0", "@aws-sdk/s3-request-presigner": "3.1037.0",
"@clickhouse/client": "^1.18.2", "@clickhouse/client": "^1.18.2",
"@fastify/cookie": "^11.0.2", "@fastify/cookie": "^11.0.2",
"@fastify/multipart": "^9.4.0", "@fastify/multipart": "^10.0.0",
"@fastify/static": "^9.0.0", "@fastify/static": "^9.1.3",
"@keyv/redis": "^5.1.6", "@keyv/redis": "^5.1.6",
"@langchain/core": "1.1.39", "@langchain/core": "1.1.39",
"@langchain/textsplitters": "1.0.1", "@langchain/textsplitters": "1.0.1",
@@ -48,19 +48,19 @@
"@nestjs-labs/nestjs-ioredis": "^11.0.4", "@nestjs-labs/nestjs-ioredis": "^11.0.4",
"@nestjs/bullmq": "^11.0.4", "@nestjs/bullmq": "^11.0.4",
"@nestjs/cache-manager": "^3.1.0", "@nestjs/cache-manager": "^3.1.0",
"@nestjs/common": "^11.1.18", "@nestjs/common": "^11.1.19",
"@nestjs/config": "^4.0.3", "@nestjs/config": "^4.0.4",
"@nestjs/core": "^11.1.18", "@nestjs/core": "^11.1.19",
"@nestjs/event-emitter": "^3.0.1", "@nestjs/event-emitter": "^3.0.1",
"@nestjs/jwt": "11.0.2", "@nestjs/jwt": "11.0.2",
"@nestjs/mapped-types": "^2.1.1", "@nestjs/mapped-types": "^2.1.1",
"@nestjs/passport": "^11.0.5", "@nestjs/passport": "^11.0.5",
"@nestjs/platform-fastify": "^11.1.18", "@nestjs/platform-fastify": "^11.1.19",
"@nestjs/platform-socket.io": "^11.1.18", "@nestjs/platform-socket.io": "^11.1.19",
"@nestjs/schedule": "^6.1.1", "@nestjs/schedule": "^6.1.3",
"@nestjs/terminus": "^11.1.1", "@nestjs/terminus": "^11.1.1",
"@nestjs/throttler": "^6.5.0", "@nestjs/throttler": "^6.5.0",
"@nestjs/websockets": "^11.1.18", "@nestjs/websockets": "^11.1.19",
"@node-saml/passport-saml": "^5.1.0", "@node-saml/passport-saml": "^5.1.0",
"@react-email/components": "1.0.10", "@react-email/components": "1.0.10",
"@react-email/render": "2.0.4", "@react-email/render": "2.0.4",
@@ -69,7 +69,7 @@
"ai-sdk-ollama": "^3.8.1", "ai-sdk-ollama": "^3.8.1",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"bowser": "^2.14.1", "bowser": "^2.14.1",
"bullmq": "^5.71.0", "bullmq": "^5.76.0",
"cache-manager": "^7.2.8", "cache-manager": "^7.2.8",
"cheerio": "^1.2.0", "cheerio": "^1.2.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
@@ -110,22 +110,23 @@
"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",
"tmp-promise": "^3.0.3", "tmp-promise": "^3.0.3",
"tseep": "^1.3.1", "tseep": "^1.3.1",
"typesense": "^3.0.5", "typesense": "^3.0.5",
"undici": "7.24.0",
"ws": "^8.20.0", "ws": "^8.20.0",
"yauzl": "^3.2.1", "yauzl": "^3.2.1",
"zod": "^4.3.6" "zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.28.0", "@eslint/js": "^9.28.0",
"@nestjs/cli": "^11.0.18", "@nestjs/cli": "^11.0.21",
"@nestjs/schematics": "^11.0.10", "@nestjs/schematics": "^11.0.10",
"@nestjs/testing": "^11.1.18", "@nestjs/testing": "^11.1.19",
"@types/bcrypt": "^6.0.0", "@types/bcrypt": "^6.0.0",
"@types/debounce": "^1.2.4", "@types/debounce": "^1.2.4",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
@@ -165,6 +166,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 {
@@ -356,9 +356,19 @@ export class AttachmentController {
throw new BadRequestException('Invalid image attachment type'); throw new BadRequestException('Invalid image attachment type');
} }
const filenameWithoutExt = path.basename(fileName, path.extname(fileName)); if (!fileName) {
if (!isValidUUID(filenameWithoutExt)) { throw new BadRequestException('Invalid file name');
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}`; const filePath = `${getAttachmentFolderPath(attachmentType, workspace.id)}/${fileName}`;
@@ -13,10 +13,6 @@ import { CreateUserDto } from '../../auth/dto/create-user.dto';
export class UpdateUserDto extends PartialType( export class UpdateUserDto extends PartialType(
OmitType(CreateUserDto, ['password'] as const), OmitType(CreateUserDto, ['password'] as const),
) { ) {
@IsOptional()
@IsString()
avatarUrl: string;
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
fullPageWidth: boolean; fullPageWidth: boolean;
@@ -110,10 +110,6 @@ export class UserService {
user.email = updateUserDto.email; user.email = updateUserDto.email;
} }
if (updateUserDto.avatarUrl) {
user.avatarUrl = updateUserDto.avatarUrl;
}
if (updateUserDto.locale) { if (updateUserDto.locale) {
user.locale = updateUserDto.locale; user.locale = updateUserDto.locale;
} }
@@ -5,15 +5,10 @@ import {
IsBoolean, IsBoolean,
IsInt, IsInt,
IsOptional, IsOptional,
IsString,
Min, Min,
} from 'class-validator'; } from 'class-validator';
export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) { export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {
@IsOptional()
@IsString()
logo: string;
@IsOptional() @IsOptional()
@IsArray() @IsArray()
emailDomains: string[]; emailDomains: string[];
@@ -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 }),
) +
'"', '"',
}); });
@@ -39,6 +39,8 @@ import {
} from '../../common/helpers/prosemirror/utils'; } from '../../common/helpers/prosemirror/utils';
import { htmlToMarkdown } from '@docmost/editor-ext'; import { htmlToMarkdown } from '@docmost/editor-ext';
type AllowedAttachment = { id: string; fileName: string; filePath: string };
@Injectable() @Injectable()
export class ExportService { export class ExportService {
private readonly logger = new Logger(ExportService.name); private readonly logger = new Logger(ExportService.name);
@@ -272,6 +274,12 @@ export class ExportService {
computeLocalPath(tree, format, null, '', slugIdToPath); computeLocalPath(tree, format, null, '', slugIdToPath);
// Batch resolve attachments once for the whole export so we only run the
// owning-page view check a single time, regardless of page count.
const allowedAttachments = includeAttachments
? await this.resolveAccessibleAttachments(tree, userId, ignorePermissions)
: new Map<string, AllowedAttachment>();
const stack: { folder: JSZip; parentPageId: string | null }[] = [ const stack: { folder: JSZip; parentPageId: string | null }[] = [
{ folder: zip, parentPageId: null }, { folder: zip, parentPageId: null },
]; ];
@@ -301,7 +309,7 @@ export class ExportService {
); );
if (includeAttachments) { if (includeAttachments) {
await this.zipAttachments(updatedJsonContent, page.spaceId, folder); await this.zipAttachments(updatedJsonContent, folder, allowedAttachments);
updatedJsonContent = updatedJsonContent =
updateAttachmentUrlsToLocalPaths(updatedJsonContent); updateAttachmentUrlsToLocalPaths(updatedJsonContent);
} }
@@ -347,31 +355,80 @@ export class ExportService {
zip.file('docmost-metadata.json', JSON.stringify(metadata, null, 2)); zip.file('docmost-metadata.json', JSON.stringify(metadata, null, 2));
} }
async zipAttachments(prosemirrorJson: any, spaceId: string, zip: JSZip) { async zipAttachments(
prosemirrorJson: any,
zip: JSZip,
allowed: Map<string, AllowedAttachment>,
) {
const attachmentIds = getAttachmentIds(prosemirrorJson); const attachmentIds = getAttachmentIds(prosemirrorJson);
if (attachmentIds.length > 0) { await Promise.all(
const attachments = await this.db attachmentIds.map(async (id) => {
.selectFrom('attachments') const attachment = allowed.get(id);
.select(['id', 'fileName', 'filePath']) if (!attachment) return;
.where('id', 'in', attachmentIds) try {
.where('spaceId', '=', spaceId) const fileBuffer = await this.storageService.read(
.execute(); attachment.filePath,
);
const filePath = `/files/${attachment.id}/${attachment.fileName}`;
zip.file(filePath, fileBuffer);
} catch (err) {
this.logger.debug(`Attachment export error ${attachment.id}`, err);
}
}),
);
}
await Promise.all( private async resolveAccessibleAttachments(
attachments.map(async (attachment) => { tree: PageExportTree,
try { userId: string | undefined,
const fileBuffer = await this.storageService.read( ignorePermissions: boolean,
attachment.filePath, ): Promise<Map<string, AllowedAttachment>> {
); const allAttachmentIds = new Set<string>();
const filePath = `/files/${attachment.id}/${attachment.fileName}`; let spaceId: string | undefined;
zip.file(filePath, fileBuffer); for (const siblings of Object.values(tree)) {
} catch (err) { for (const page of siblings) {
this.logger.debug(`Attachment export error ${attachment.id}`, err); if (!spaceId) spaceId = page.spaceId;
} for (const id of getAttachmentIds(getProsemirrorContent(page.content))) {
}), allAttachmentIds.add(id);
}
}
}
if (allAttachmentIds.size === 0 || !spaceId) {
return new Map();
}
const attachments = await this.db
.selectFrom('attachments')
.select(['id', 'fileName', 'filePath', 'pageId'])
.where('id', 'in', [...allAttachmentIds])
.where('spaceId', '=', spaceId)
.execute();
let visible = attachments;
if (!ignorePermissions && userId) {
const ownerPageIds = [
...new Set(
attachments
.map((a) => a.pageId)
.filter((id): id is string => !!id),
),
];
const accessible = ownerPageIds.length
? await this.pagePermissionRepo.filterAccessiblePageIds({
pageIds: ownerPageIds,
userId,
spaceId,
})
: [];
const accessibleSet = new Set(accessible);
visible = attachments.filter(
(a) => a.pageId && accessibleSet.has(a.pageId),
); );
} }
return new Map(visible.map((a) => [a.id, a]));
} }
async turnPageMentionsToLinks( async turnPageMentionsToLinks(
@@ -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();
@@ -0,0 +1,67 @@
import { resolve, sep } from 'path';
import { LocalDriver } from './local.driver';
type FullPath = (filePath: string) => string;
describe('LocalDriver._fullPath', () => {
const ROOT = resolve('/data/storage');
const driver = new LocalDriver({ storagePath: ROOT });
const fullPath = ((driver as any)._fullPath as FullPath).bind(driver);
describe('legitimate inputs (behavior preserved)', () => {
it.each([
['workspace-id/avatars/uuid.png', `${ROOT}${sep}workspace-id${sep}avatars${sep}uuid.png`],
['workspace-id/files/uuid/file.pdf', `${ROOT}${sep}workspace-id${sep}files${sep}uuid${sep}file.pdf`],
['a/b/c/d/e.bin', `${ROOT}${sep}a${sep}b${sep}c${sep}d${sep}e.bin`],
['', ROOT],
['.', ROOT],
['./x/y.png', `${ROOT}${sep}x${sep}y.png`],
['a//b', `${ROOT}${sep}a${sep}b`],
['a/b/../c', `${ROOT}${sep}a${sep}c`],
])('resolves %j to %j', (input, expected) => {
expect(fullPath(input)).toBe(expected);
});
});
describe('traversal rejected', () => {
it.each([
'../etc/passwd',
'../../../etc/passwd',
'workspace/../../../etc/passwd',
'..',
'../..',
'a/../../..',
])('throws for %j', (input) => {
expect(() => fullPath(input)).toThrow('Invalid file path');
});
});
describe('absolute path rejected', () => {
it.each([
'/etc/passwd',
'/root/.ssh/id_rsa',
sep + 'absolute',
])('throws for %j', (input) => {
expect(() => fullPath(input)).toThrow('Invalid file path');
});
});
describe('prefix-confusion rejected', () => {
it('rejects a sibling directory whose name starts with the storage root', () => {
const siblingDriver = new LocalDriver({ storagePath: '/data/storage' });
const siblingFullPath = ((siblingDriver as any)._fullPath as FullPath).bind(siblingDriver);
// Attempt to reach /data/storage-evil/secret by traversal:
// resolve('/data/storage', '../storage-evil/secret') === '/data/storage-evil/secret'
// Without the `+ sep` guard, a startsWith check would match.
expect(() => siblingFullPath('../storage-evil/secret')).toThrow('Invalid file path');
});
});
describe('storage root itself', () => {
it('accepts the root when input resolves to it', () => {
expect(fullPath('')).toBe(ROOT);
expect(fullPath('.')).toBe(ROOT);
expect(fullPath('a/..')).toBe(ROOT);
});
});
});
@@ -3,7 +3,7 @@ import {
LocalStorageConfig, LocalStorageConfig,
StorageOption, StorageOption,
} from '../interfaces'; } from '../interfaces';
import { join, dirname } from 'path'; import { dirname, resolve, sep } from 'path';
import * as fs from 'fs-extra'; import * as fs from 'fs-extra';
import { Readable } from 'stream'; import { Readable } from 'stream';
import { createReadStream, createWriteStream } from 'node:fs'; import { createReadStream, createWriteStream } from 'node:fs';
@@ -17,7 +17,12 @@ export class LocalDriver implements StorageDriver {
} }
private _fullPath(filePath: string): string { private _fullPath(filePath: string): string {
return join(this.config.storagePath, filePath); const storageRoot = resolve(this.config.storagePath);
const fullPath = resolve(storageRoot, filePath);
if (fullPath !== storageRoot && !fullPath.startsWith(storageRoot + sep)) {
throw new Error('Invalid file path');
}
return fullPath;
} }
async upload(filePath: string, file: Buffer | Readable): Promise<void> { async upload(filePath: string, file: Buffer | Readable): Promise<void> {
+10 -9
View File
@@ -1,7 +1,7 @@
{ {
"name": "docmost", "name": "docmost",
"homepage": "https://docmost.com", "homepage": "https://docmost.com",
"version": "0.80.0", "version": "0.80.1",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "nx run-many -t build", "build": "nx run-many -t build",
@@ -62,7 +62,7 @@
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"diff": "8.0.3", "diff": "8.0.3",
"dompurify": "^3.3.3", "dompurify": "3.4.1",
"fractional-indexing-jittered": "^1.0.0", "fractional-indexing-jittered": "^1.0.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"image-dimensions": "^2.5.0", "image-dimensions": "^2.5.0",
@@ -72,7 +72,7 @@
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"rfc6902": "5.2.0", "rfc6902": "5.2.0",
"uuid": "^13.0.0", "uuid": "^14.0.0",
"y-indexeddb": "^9.0.12", "y-indexeddb": "^9.0.12",
"y-prosemirror": "1.3.7", "y-prosemirror": "1.3.7",
"yjs": "^13.6.30" "yjs": "^13.6.30"
@@ -102,9 +102,9 @@
"y-prosemirror": "1.3.7", "y-prosemirror": "1.3.7",
"glob": "13.0.6", "glob": "13.0.6",
"ws": "8.20.0", "ws": "8.20.0",
"dompurify": "3.3.3", "dompurify": "3.4.1",
"tmp": "0.2.5", "tmp": "0.2.5",
"hono": "4.12.12", "hono": "4.12.14",
"mermaid": "11.13.0", "mermaid": "11.13.0",
"nanoid@^3": "3.3.8", "nanoid@^3": "3.3.8",
"socket.io-parser": "4.2.6", "socket.io-parser": "4.2.6",
@@ -123,16 +123,17 @@
"flatted": "3.4.2", "flatted": "3.4.2",
"picomatch@<2.3.2": "2.3.2", "picomatch@<2.3.2": "2.3.2",
"picomatch@>=4.0.0 <4.0.4": "4.0.4", "picomatch@>=4.0.0 <4.0.4": "4.0.4",
"fastify": "5.8.3", "fastify": "5.8.5",
"yaml@>=1.0.0 <1.10.3": "1.10.3", "yaml@>=1.0.0 <1.10.3": "1.10.3",
"yaml@>=2.0.0 <2.8.3": "2.8.3", "yaml@>=2.0.0 <2.8.3": "2.8.3",
"path-to-regexp@^8": "8.4.0", "path-to-regexp@^8": "8.4.0",
"brace-expansion@^5": "5.0.5", "brace-expansion@^5": "5.0.5",
"@xmldom/xmldom": "0.8.12", "@xmldom/xmldom": "0.8.13",
"handlebars": "4.7.9", "handlebars": "4.7.9",
"axios": "1.15.0", "axios": "1.15.0",
"langsmith": "0.5.18", "langsmith": "0.5.19",
"follow-redirects": "1.16.0" "follow-redirects": "1.16.0",
"protobufjs": "7.5.5"
}, },
"neverBuiltDependencies": [] "neverBuiltDependencies": []
} }
+884 -894
View File
File diff suppressed because it is too large Load Diff