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:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user