Pentest R104.1: Salutation-Endpunkte 404 statt 500
INFO-Finding: GET/PUT /api/customers/:id/salutation-preference
warfen bei nicht-existierendem Customer 500 statt 404.
canAccessCustomer prüft für Staff-User nur den Portal-Flag, kein
Existenz-Check. Der Service warf `new Error('Kunde nicht
gefunden')`, was der Controller-Catch generisch auf 500 mappte.
set/clear kamen als Prisma-P2003 (FK-Constraint) durch.
Fix:
- Neuer Helper assertCustomerExists() im customer.service, wirft
ApiError(404, ...). Wird von get/set/clearSalutationPreference
aufgerufen.
- Die drei Controller-Catches respektieren jetzt
ApiError.statusCode → 404 kommt sauber durch, alle anderen
Fehler bleiben bei 500.
Doku: docs/todo.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
|||||||
canAccessCustomer,
|
canAccessCustomer,
|
||||||
getPortalAllowedCustomerIds,
|
getPortalAllowedCustomerIds,
|
||||||
} from '../utils/accessControl.js';
|
} from '../utils/accessControl.js';
|
||||||
|
import { ApiError } from '../utils/apiError.js';
|
||||||
|
|
||||||
// Customer CRUD
|
// Customer CRUD
|
||||||
export async function getCustomers(req: AuthRequest, res: Response): Promise<void> {
|
export async function getCustomers(req: AuthRequest, res: Response): Promise<void> {
|
||||||
@@ -1293,7 +1294,10 @@ export async function getSalutationPreference(req: AuthRequest, res: Response):
|
|||||||
const result = await customerService.getSalutationPreference(userId, customerId);
|
const result = await customerService.getSalutationPreference(userId, customerId);
|
||||||
res.json({ success: true, data: result } as ApiResponse);
|
res.json({ success: true, data: result } as ApiResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({
|
// Pentest R104.1: ApiError-Statuscodes durchreichen (404 statt 500
|
||||||
|
// bei nicht-existierendem Kunden).
|
||||||
|
const status = error instanceof ApiError ? error.statusCode : 500;
|
||||||
|
res.status(status).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : 'Fehler beim Laden der Anrede-Präferenz',
|
error: error instanceof Error ? error.message : 'Fehler beim Laden der Anrede-Präferenz',
|
||||||
} as ApiResponse);
|
} as ApiResponse);
|
||||||
@@ -1325,7 +1329,8 @@ export async function setSalutationPreference(req: AuthRequest, res: Response):
|
|||||||
const result = await customerService.getSalutationPreference(userId, customerId);
|
const result = await customerService.getSalutationPreference(userId, customerId);
|
||||||
res.json({ success: true, data: result } as ApiResponse);
|
res.json({ success: true, data: result } as ApiResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({
|
const status = error instanceof ApiError ? error.statusCode : 500;
|
||||||
|
res.status(status).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : 'Fehler beim Speichern der Anrede-Präferenz',
|
error: error instanceof Error ? error.message : 'Fehler beim Speichern der Anrede-Präferenz',
|
||||||
} as ApiResponse);
|
} as ApiResponse);
|
||||||
@@ -1349,7 +1354,8 @@ export async function clearSalutationPreference(req: AuthRequest, res: Response)
|
|||||||
const result = await customerService.getSalutationPreference(userId, customerId);
|
const result = await customerService.getSalutationPreference(userId, customerId);
|
||||||
res.json({ success: true, data: result } as ApiResponse);
|
res.json({ success: true, data: result } as ApiResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({
|
const status = error instanceof ApiError ? error.statusCode : 500;
|
||||||
|
res.status(status).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : 'Fehler beim Zurücksetzen der Anrede-Präferenz',
|
error: error instanceof Error ? error.message : 'Fehler beim Zurücksetzen der Anrede-Präferenz',
|
||||||
} as ApiResponse);
|
} as ApiResponse);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { CustomerType, ContractStatus } from '@prisma/client';
|
import { CustomerType, ContractStatus } from '@prisma/client';
|
||||||
import prisma from '../lib/prisma.js';
|
import prisma from '../lib/prisma.js';
|
||||||
import { generateCustomerNumber, paginate, buildPaginationResponse } from '../utils/helpers.js';
|
import { generateCustomerNumber, paginate, buildPaginationResponse } from '../utils/helpers.js';
|
||||||
|
import { ApiError } from '../utils/apiError.js';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
@@ -1028,6 +1029,22 @@ function isValidPreference(v: unknown): v is SalutationPreference {
|
|||||||
return v === 'DU' || v === 'SIE';
|
return v === 'DU' || v === 'SIE';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wirft ApiError(404), wenn kein Kunde mit dieser ID existiert. Vor
|
||||||
|
* jedem Salutation-Preference-Zugriff aufrufen – ohne den Check würde
|
||||||
|
* `upsert`/`deleteMany` einen Prisma-FK-Error werfen, den der Controller
|
||||||
|
* als 500 mappt (Pentest R104.1).
|
||||||
|
*/
|
||||||
|
async function assertCustomerExists(customerId: number): Promise<void> {
|
||||||
|
const exists = await prisma.customer.findUnique({
|
||||||
|
where: { id: customerId },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (!exists) {
|
||||||
|
throw new ApiError(404, 'Kunde nicht gefunden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Liefert die effektive Anrede für einen User/Kunde: eigene Präferenz
|
* Liefert die effektive Anrede für einen User/Kunde: eigene Präferenz
|
||||||
* bevorzugt, sonst Kunden-Default. `source` sagt der UI, ob's ein eigener
|
* bevorzugt, sonst Kunden-Default. `source` sagt der UI, ob's ein eigener
|
||||||
@@ -1051,7 +1068,10 @@ export async function getSalutationPreference(
|
|||||||
select: { useInformalAddress: true },
|
select: { useInformalAddress: true },
|
||||||
});
|
});
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
throw new Error('Kunde nicht gefunden');
|
// Pentest R104.1 (INFO, 2026-07-04): vorher `throw new Error(...)`,
|
||||||
|
// was der Controller-Catch als 500 mappte. Sauberes 404 macht die
|
||||||
|
// Response semantisch korrekt (existiert nicht / gerade gelöscht).
|
||||||
|
throw new ApiError(404, 'Kunde nicht gefunden');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
preference: customer.useInformalAddress ? 'DU' : 'SIE',
|
preference: customer.useInformalAddress ? 'DU' : 'SIE',
|
||||||
@@ -1064,6 +1084,7 @@ export async function setSalutationPreference(
|
|||||||
customerId: number,
|
customerId: number,
|
||||||
preference: SalutationPreference,
|
preference: SalutationPreference,
|
||||||
) {
|
) {
|
||||||
|
await assertCustomerExists(customerId);
|
||||||
return prisma.userCustomerSalutation.upsert({
|
return prisma.userCustomerSalutation.upsert({
|
||||||
where: { userId_customerId: { userId, customerId } },
|
where: { userId_customerId: { userId, customerId } },
|
||||||
create: { userId, customerId, preference },
|
create: { userId, customerId, preference },
|
||||||
@@ -1077,6 +1098,7 @@ export async function setSalutationPreference(
|
|||||||
* idempotent gedacht ("stelle sicher, dass kein Override existiert").
|
* idempotent gedacht ("stelle sicher, dass kein Override existiert").
|
||||||
*/
|
*/
|
||||||
export async function clearSalutationPreference(userId: number, customerId: number) {
|
export async function clearSalutationPreference(userId: number, customerId: number) {
|
||||||
|
await assertCustomerExists(customerId);
|
||||||
await prisma.userCustomerSalutation.deleteMany({
|
await prisma.userCustomerSalutation.deleteMany({
|
||||||
where: { userId, customerId },
|
where: { userId, customerId },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -97,6 +97,17 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ Erledigt
|
||||||
|
|
||||||
|
- [x] **🔒 Pentest R104.1 – Salutation-Endpunkte: 404 statt 500**
|
||||||
|
- INFO-Finding: `GET/PUT /api/customers/:id/salutation-preference`
|
||||||
|
warfen bei nicht-existierendem Customer 500 statt 404.
|
||||||
|
- Ursache: `canAccessCustomer` prüft für Staff-User nur Portal-Flag,
|
||||||
|
kein Existenz-Check. Service warf `new Error(...)`, Controller-
|
||||||
|
Catch mappte generisch auf 500.
|
||||||
|
- Fix: neuer Helper `assertCustomerExists()` im Service wirft
|
||||||
|
`ApiError(404, ...)`. Die drei Controller-Catches respektieren
|
||||||
|
jetzt `ApiError.statusCode`. Auch `set`/`clear` prüfen Existenz
|
||||||
|
(sonst käme ein Prisma-P2003 als 500 durch).
|
||||||
|
|
||||||
- [x] **🔒 Pentest R102 – Interne Vertragsnummer nachziehen**
|
- [x] **🔒 Pentest R102 – Interne Vertragsnummer nachziehen**
|
||||||
- INFO-Finding: `contractNumber` (interne CRM-Nummer) hatte im
|
- INFO-Finding: `contractNumber` (interne CRM-Nummer) hatte im
|
||||||
Update-Path keine Whitelist. `<script>alert(1)</script>` →
|
Update-Path keine Whitelist. `<script>alert(1)</script>` →
|
||||||
|
|||||||
Reference in New Issue
Block a user