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 { ConfigService } from '@nestjs/config';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
|
|
||||||
const THUMBNAIL_WIDTH = 180;
|
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()
|
@Injectable()
|
||||||
export class PageCacheService {
|
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 {
|
documentDir(documentId: string): string {
|
||||||
|
if (!UUID.test(documentId)) {
|
||||||
|
throw new BadRequestException('Ungültige Dokument-ID');
|
||||||
|
}
|
||||||
return path.join(this.inboxRoot, documentId);
|
return path.join(this.inboxRoot, documentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
HttpStatus,
|
HttpStatus,
|
||||||
Logger,
|
Logger,
|
||||||
Delete,
|
Delete,
|
||||||
|
ParseIntPipe,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import type { Response } from 'express';
|
import type { Response } from 'express';
|
||||||
import { EmailImportService } from './email-import.service';
|
import { EmailImportService } from './email-import.service';
|
||||||
@@ -201,8 +202,8 @@ export class EmailImportController {
|
|||||||
@Get('attachments/:attachmentId/pages/:page/thumb')
|
@Get('attachments/:attachmentId/pages/:page/thumb')
|
||||||
@RequirePermissions(Permission.VIEW_MAIL)
|
@RequirePermissions(Permission.VIEW_MAIL)
|
||||||
async getPageThumbnail(
|
async getPageThumbnail(
|
||||||
@Param('attachmentId') attachmentId: number,
|
@Param('attachmentId', ParseIntPipe) attachmentId: number,
|
||||||
@Param('page') page: number,
|
@Param('page', ParseIntPipe) page: number,
|
||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
) {
|
) {
|
||||||
const hasPreview = await this.pageCache.hasPreview(attachmentId, page);
|
const hasPreview = await this.pageCache.hasPreview(attachmentId, page);
|
||||||
@@ -217,8 +218,8 @@ export class EmailImportController {
|
|||||||
@Get('attachments/:attachmentId/pages/:page/preview')
|
@Get('attachments/:attachmentId/pages/:page/preview')
|
||||||
@RequirePermissions(Permission.VIEW_MAIL)
|
@RequirePermissions(Permission.VIEW_MAIL)
|
||||||
async getPagePreview(
|
async getPagePreview(
|
||||||
@Param('attachmentId') attachmentId: number,
|
@Param('attachmentId', ParseIntPipe) attachmentId: number,
|
||||||
@Param('page') page: number,
|
@Param('page', ParseIntPipe) page: number,
|
||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
) {
|
) {
|
||||||
const hasPreview = await this.pageCache.hasPreview(attachmentId, page);
|
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 { ConfigService } from '@nestjs/config';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
@@ -6,6 +6,12 @@ import sharp from 'sharp';
|
|||||||
|
|
||||||
const THUMBNAIL_WIDTH = 180;
|
const THUMBNAIL_WIDTH = 180;
|
||||||
|
|
||||||
|
function pruefeSeite(page: number): void {
|
||||||
|
if (!Number.isSafeInteger(page) || page < 1) {
|
||||||
|
throw new BadRequestException('Ungültige Seitenzahl');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EmailPageCacheService {
|
export class EmailPageCacheService {
|
||||||
private readonly logger = new Logger(EmailPageCacheService.name);
|
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 {
|
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 {
|
previewPath(attachmentId: number | string, page: number): string {
|
||||||
|
pruefeSeite(page);
|
||||||
return path.join(
|
return path.join(
|
||||||
this.attachmentDir(attachmentId),
|
this.attachmentDir(attachmentId),
|
||||||
`page-${page}.preview.png`,
|
`page-${page}.preview.png`,
|
||||||
@@ -30,6 +45,7 @@ export class EmailPageCacheService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
thumbnailPath(attachmentId: number | string, page: number): string {
|
thumbnailPath(attachmentId: number | string, page: number): string {
|
||||||
|
pruefeSeite(page);
|
||||||
return path.join(
|
return path.join(
|
||||||
this.attachmentDir(attachmentId),
|
this.attachmentDir(attachmentId),
|
||||||
`page-${page}.thumb.png`,
|
`page-${page}.thumb.png`,
|
||||||
|
|||||||
Reference in new issue
Block a user