fix: Robustheits-Luecken aus zweitem, unabhaengigem Sicherheitsaudit behoben

Ein zweiter, unabhaengiger Audit-Durchlauf (frischer Blick, keine
Vorkenntnis meiner eigenen Fixes) bestaetigte alle bisherigen Fixes als
korrekt verdrahtet und fand 3 LOW-severity Robustheitsluecken - keine
davon war ausnutzbar, aber alle sind jetzt behoben:

- HarvestsService.create/ExtractionsService.create pruefen jetzt, dass
  referenzierte colonyIds/harvestIds tatsaechlich existieren, bevor die
  Transaktion laeuft - vorher fuehrte eine unbekannte Id zu einem
  ungefangenen 500 mit rohem Prisma-Fehler statt einer sauberen 400.
- Etiketten-Bildelemente durchlaufen jetzt denselben Magic-Byte-Check
  wie Avatar-/Dokument-Uploads, statt pdfkits Bildparser ungepruefte
  Bytes zu uebergeben.
- Webhook-Secret-Redaktion im Audit-Log nutzt jetzt explizites delete
  statt sich auf Prismas JSON-Serialisierung zu verlassen, die
  undefined-Werte verwirft (fragil, falls sich die Serialisierung mal
  aendert).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Sonnet 5 committed 2026-09-02 15:13:02 +02:00
1 parent f57cffe6a8
commit 4bd7e9791a
7 files changed
+173 -7

No files matched your search

@@ -1,4 +1,4 @@
import { NotFoundException } from '@nestjs/common';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { HarvestsService } from './harvests.service';
import { PrismaService } from '../prisma/prisma.service';
import { AuditLogService } from '../audit/audit-log.service';
@@ -8,6 +8,7 @@ describe('HarvestsService', () => {
let service: HarvestsService;
let prisma: {
harvest: { findUnique: jest.Mock; update: jest.Mock };
colony: { findMany: jest.Mock };
$transaction: jest.Mock;
};
let auditLog: { record: jest.Mock };
@@ -25,6 +26,11 @@ describe('HarvestsService', () => {
};
prisma = {
harvest: { findUnique: jest.fn(), update: jest.fn() },
colony: {
findMany: jest
.fn()
.mockResolvedValue([{ id: 'colony-1' }, { id: 'colony-2' }]),
},
$transaction: jest.fn((callback: (txArg: typeof tx) => unknown) =>
callback(tx),
),
@@ -66,6 +72,22 @@ describe('HarvestsService', () => {
]);
});
it('create rejects an unknown colony id instead of failing on the DB foreign-key constraint', async () => {
prisma.colony.findMany.mockResolvedValue([{ id: 'colony-1' }]);
await expect(
service.create(
{
apiaryId: 'apiary-1',
date: '2026-07-01',
colonyIds: ['colony-1', 'missing-colony'],
},
'user-1',
),
).rejects.toBeInstanceOf(BadRequestException);
expect(prisma.$transaction).not.toHaveBeenCalled();
});
it('update writes an audit log entry with before and after data', async () => {
const updated = { ...harvest, notes: 'geändert' };
prisma.harvest.findUnique
@@ -1,4 +1,8 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { AuditLogService } from '../audit/audit-log.service';
import { WebhooksService } from '../webhooks/webhooks.service';
@@ -35,7 +39,22 @@ export class HarvestsService {
return harvest;
}
private async assertColoniesExist(colonyIds: string[]): Promise<void> {
const found = await this.prisma.colony.findMany({
where: { id: { in: colonyIds } },
select: { id: true },
});
const foundIds = new Set(found.map((colony) => colony.id));
const missing = colonyIds.filter((id) => !foundIds.has(id));
if (missing.length > 0) {
throw new BadRequestException(
`Unknown colony id(s): ${missing.join(', ')}`,
);
}
}
async create(dto: CreateHarvestDto, actingUserId: string) {
await this.assertColoniesExist(dto.colonyIds);
const harvest = await this.prisma.$transaction(async (tx) => {
const created = await tx.harvest.create({
data: {