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

@@ -220,6 +220,75 @@ describe('LabelTemplatesService', () => {
expect(pdf.toString('latin1')).toContain('/Image');
});
it('embeds an image element whose bytes match the declared PNG mime type', async () => {
// Minimal valid 1x1 transparent PNG, widely used as a test fixture.
const onePixelPng =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=';
const imageElement: LabelElement = {
id: 'el-img',
type: 'image',
x: 5,
y: 5,
width: 10,
height: 10,
src: `data:image/png;base64,${onePixelPng}`,
};
prisma.labelTemplate.findUnique.mockResolvedValue({
...template,
elements: [imageElement],
});
prisma.bottlingBatch.findUnique.mockResolvedValue({
id: 'batch-1',
jarSize: 500,
jarCount: 1,
lotNumber: 'L-001',
date: new Date('2026-06-01T00:00:00.000Z'),
bestBeforeDate: new Date('2028-06-01T00:00:00.000Z'),
honeyBatch: { batchNumber: 'HB-001', honeyType: 'Bluetenhonig' },
product: { name: 'Glas 500g' },
});
const pdf = await service.renderLabelsPdf('template-1', 'batch-1');
// pdfkit always lists /ImageB /ImageC /ImageI in a page's ProcSet
// regardless of whether anything is actually embedded, so a plain
// "/Image" substring check would pass either way - /Subtype /Image is
// the actual embedded-XObject marker.
expect(pdf.toString('latin1')).toMatch(/\/Subtype\s*\/Image\b/);
});
it('silently skips an image element whose bytes do not match the declared mime type', async () => {
const mislabeled = Buffer.from('not-a-png').toString('base64');
const imageElement: LabelElement = {
id: 'el-img',
type: 'image',
x: 5,
y: 5,
width: 10,
height: 10,
src: `data:image/png;base64,${mislabeled}`,
};
prisma.labelTemplate.findUnique.mockResolvedValue({
...template,
elements: [imageElement],
});
prisma.bottlingBatch.findUnique.mockResolvedValue({
id: 'batch-1',
jarSize: 500,
jarCount: 1,
lotNumber: 'L-001',
date: new Date('2026-06-01T00:00:00.000Z'),
bestBeforeDate: new Date('2028-06-01T00:00:00.000Z'),
honeyBatch: { batchNumber: 'HB-001', honeyType: 'Bluetenhonig' },
product: { name: 'Glas 500g' },
});
const pdf = await service.renderLabelsPdf('template-1', 'batch-1');
expect(pdf.subarray(0, 4).toString()).toBe('%PDF');
expect(pdf.toString('latin1')).not.toMatch(/\/Subtype\s*\/Image\b/);
});
function countPages(pdf: Buffer): number {
const matches = pdf.toString('latin1').match(/\/Type\s*\/Page[^s]/g);
return matches ? matches.length : 0;
@@ -3,6 +3,7 @@ import PDFDocument from 'pdfkit';
import * as QRCode from 'qrcode';
import { PrismaService } from '../prisma/prisma.service';
import { AuditLogService } from '../audit/audit-log.service';
import { matchesFileSignature } from '../common/file-signature';
import { LabelTemplate } from '../../generated/prisma';
import { CreateLabelTemplateDto } from './dto/create-label-template.dto';
import { UpdateLabelTemplateDto } from './dto/update-label-template.dto';
@@ -62,7 +63,13 @@ function dataUrlToBuffer(dataUrl: string): Buffer | null {
if (!match) {
return null;
}
return Buffer.from(match[2], 'base64');
const mimeType = match[1] === 'png' ? 'image/png' : 'image/jpeg';
const buffer = Buffer.from(match[2], 'base64');
// Same magic-byte check as every other upload path (avatar, documents) -
// a template's embedded image is only ever entered by a label-template
// editor (label-templates:create/update), but there's no reason to hand
// pdfkit's image parser bytes that don't match what they claim to be.
return matchesFileSignature(buffer, mimeType) ? buffer : null;
}
@Injectable()