fix(audit): H1 (webhook redirect SSRF), H6 (berth-status case), H7 (residential notes URL)

H1: webhook delivery fetch now uses redirect:'manual' and refuses to read
or expose a redirected (un-revalidated) response, closing the SSRF read
primitive. H6: dashboard report queries matched title-case 'Sold'/'Under
offer' that never match the lowercase canonical, silently reporting 0 sold
/ understated occupancy — now lowercase. H7: NotesList maps the entityType
discriminator to its REST path (residential_* -> residential/clients|
interests) instead of interpolating the raw underscore, which 404'd every
residential notes request.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 12:03:35 +02:00
parent 3c9310f81c
commit f4fb7aae84
3 changed files with 35 additions and 12 deletions

View File

@@ -223,6 +223,11 @@ export const webhooksWorker = new Worker(
const response = await fetch(webhook.url, {
method: 'POST',
// SSRF guard (audit H1): never follow redirects. resolveAndCheckHost
// validated the configured host, but a 3xx Location could point at
// cloud-metadata / RFC1918 with no re-validation. With 'manual' the
// redirect is returned, not followed, and we refuse to read its body.
redirect: 'manual',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'PortNimara-Webhook/1.0',
@@ -238,10 +243,19 @@ export const webhooksWorker = new Worker(
clearTimeout(timeoutId);
responseStatus = response.status;
// Read up to 1KB of response body
const rawBody = await response.text();
responseBody = rawBody.slice(0, 1024);
success = response.status >= 200 && response.status < 300;
if (response.status >= 300 && response.status < 400) {
// Redirect not followed — treat as a delivery failure and do NOT
// read/expose the (un-validated) redirected response.
responseBody = `Blocked: redirect (${response.status} -> ${
response.headers.get('location') ?? 'unknown'
}) not followed (SSRF guard)`;
success = false;
} else {
// Read up to 1KB of response body
const rawBody = await response.text();
responseBody = rawBody.slice(0, 1024);
success = response.status >= 200 && response.status < 300;
}
} catch (err) {
// Network error or timeout
logger.warn({ webhookId, deliveryId, err }, 'Webhook delivery network error');