fix(security): Befunde aus dem Security-Audit beheben #7
No files matched your search
@@ -1,10 +1,11 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs/promises';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const THUMBNAIL_WIDTH = 180;
|
||||
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
@Injectable()
|
||||
export class PageCacheService {
|
||||
@@ -18,7 +19,11 @@ export class PageCacheService {
|
||||
);
|
||||
}
|
||||
|
||||
/** Scanner-Dokumente haben UUIDs – alles andere würde den Pfad verlassen. */
|
||||
documentDir(documentId: string): string {
|
||||
if (!UUID.test(documentId)) {
|
||||
throw new BadRequestException('Ungültige Dokument-ID');
|
||||
}
|
||||
return path.join(this.inboxRoot, documentId);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
HttpStatus,
|
||||
Logger,
|
||||
Delete,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import type { Response } from 'express';
|
||||
import { EmailImportService } from './email-import.service';
|
||||
@@ -201,8 +202,8 @@ export class EmailImportController {
|
||||
@Get('attachments/:attachmentId/pages/:page/thumb')
|
||||
@RequirePermissions(Permission.VIEW_MAIL)
|
||||
async getPageThumbnail(
|
||||
@Param('attachmentId') attachmentId: number,
|
||||
@Param('page') page: number,
|
||||
@Param('attachmentId', ParseIntPipe) attachmentId: number,
|
||||
@Param('page', ParseIntPipe) page: number,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const hasPreview = await this.pageCache.hasPreview(attachmentId, page);
|
||||
@@ -217,8 +218,8 @@ export class EmailImportController {
|
||||
@Get('attachments/:attachmentId/pages/:page/preview')
|
||||
@RequirePermissions(Permission.VIEW_MAIL)
|
||||
async getPagePreview(
|
||||
@Param('attachmentId') attachmentId: number,
|
||||
@Param('page') page: number,
|
||||
@Param('attachmentId', ParseIntPipe) attachmentId: number,
|
||||
@Param('page', ParseIntPipe) page: number,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const hasPreview = await this.pageCache.hasPreview(attachmentId, page);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { EmailPageCacheService } from './email-page-cache.service';
|
||||
import { PageCacheService } from '../barcode/page-cache.service';
|
||||
|
||||
const config = {
|
||||
get: (key: string) =>
|
||||
key === 'MAILS_DATA_DIR' ? '/data/mails' : '/data/inbox',
|
||||
} as unknown as ConfigService;
|
||||
|
||||
describe('EmailPageCacheService – Pfade', () => {
|
||||
const cache = new EmailPageCacheService(config);
|
||||
|
||||
it('baut Pfade für numerische Anhang-IDs', () => {
|
||||
expect(cache.previewPath(17, 2)).toBe('/data/mails/17/page-2.preview.png');
|
||||
expect(cache.thumbnailPath('17', 1)).toBe(
|
||||
'/data/mails/17/page-1.thumb.png',
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'../inbox/0b6c2b6e-7c1e-4d8a-9a51-3f0c2d1e4b5a',
|
||||
'..',
|
||||
'17/../../etc',
|
||||
'-1',
|
||||
'1.5',
|
||||
'',
|
||||
])('lehnt die Anhang-ID %p ab', (id) => {
|
||||
expect(() => cache.previewPath(id, 1)).toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it.each([0, -1, 1.5, NaN])('lehnt die Seite %p ab', (page) => {
|
||||
expect(() => cache.thumbnailPath(17, page)).toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PageCacheService (Scanner) – Pfade', () => {
|
||||
const cache = new PageCacheService(config);
|
||||
const uuid = '0b6c2b6e-7c1e-4d8a-9a51-3f0c2d1e4b5a';
|
||||
|
||||
it('baut Pfade für UUIDs', () => {
|
||||
expect(cache.documentDir(uuid)).toBe(`/data/inbox/${uuid}`);
|
||||
});
|
||||
|
||||
it.each(['../mails/17', '..', `${uuid}/..`, 'abc'])(
|
||||
'lehnt die Dokument-ID %p ab',
|
||||
(id) => {
|
||||
expect(() => cache.documentDir(id)).toThrow(BadRequestException);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs/promises';
|
||||
@@ -6,6 +6,12 @@ import sharp from 'sharp';
|
||||
|
||||
const THUMBNAIL_WIDTH = 180;
|
||||
|
||||
function pruefeSeite(page: number): void {
|
||||
if (!Number.isSafeInteger(page) || page < 1) {
|
||||
throw new BadRequestException('Ungültige Seitenzahl');
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class EmailPageCacheService {
|
||||
private readonly logger = new Logger(EmailPageCacheService.name);
|
||||
@@ -18,11 +24,20 @@ export class EmailPageCacheService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Die ID landet im Dateipfad – nur positive ganze Zahlen zulassen, sonst
|
||||
* führt z.B. `..%2Finbox%2F<uuid>` aus dem Mail-Cache in den Scanner-Cache.
|
||||
*/
|
||||
attachmentDir(attachmentId: number | string): string {
|
||||
return path.join(this.mailsRoot, attachmentId.toString());
|
||||
const id = String(attachmentId);
|
||||
if (!/^[1-9]\d*$/.test(id)) {
|
||||
throw new BadRequestException('Ungültige Anhang-ID');
|
||||
}
|
||||
return path.join(this.mailsRoot, id);
|
||||
}
|
||||
|
||||
previewPath(attachmentId: number | string, page: number): string {
|
||||
pruefeSeite(page);
|
||||
return path.join(
|
||||
this.attachmentDir(attachmentId),
|
||||
`page-${page}.preview.png`,
|
||||
@@ -30,6 +45,7 @@ export class EmailPageCacheService {
|
||||
}
|
||||
|
||||
thumbnailPath(attachmentId: number | string, page: number): string {
|
||||
pruefeSeite(page);
|
||||
return path.join(
|
||||
this.attachmentDir(attachmentId),
|
||||
`page-${page}.thumb.png`,
|
||||
|
||||
Reference in new issue
Block a user