fix: close SSRF/rebinding, IDOR, and stale-session authz gaps found in audit
Bump to 0.5.1. Fixes: unfollowed-redirect SSRF + DNS-rebinding in AI url-import and webhook dispatch (new safeFetch with IP-pinned undici dispatcher); cross-user photo deletion via unvalidated recipe/review storage keys; comment-moderation and tier-quota checks trusting a stale cached session role/tier instead of the DB. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import crypto from "crypto";
|
||||
import { db } from "@epicure/db";
|
||||
import { webhooks, webhookDeliveries } from "@epicure/db";
|
||||
import { eq, and } from "@epicure/db";
|
||||
import { validateWebhookUrl } from "@/lib/validate-webhook-url";
|
||||
import { safeFetch } from "@/lib/validate-webhook-url";
|
||||
|
||||
export const WEBHOOK_EVENTS = [
|
||||
"recipe.created",
|
||||
@@ -31,12 +31,10 @@ export async function dispatchWebhook(userId: string, event: WebhookEvent, paylo
|
||||
let statusCode = 0;
|
||||
let success = false;
|
||||
try {
|
||||
// Re-validate at dispatch time to defeat DNS rebinding between create and
|
||||
// dispatch. A small window remains between this lookup and fetch's own
|
||||
// resolution of the hostname — accepted for now.
|
||||
const ssrfError = await validateWebhookUrl(hook.url);
|
||||
if (ssrfError) throw new Error(ssrfError);
|
||||
const res = await fetch(hook.url, {
|
||||
// safeFetch resolves and pins the connection to a single validated IP
|
||||
// (and re-validates + re-pins every redirect hop), so there's no
|
||||
// separate re-resolution for a rebinding attack to exploit.
|
||||
const res = await safeFetch(hook.url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -44,8 +42,6 @@ export async function dispatchWebhook(userId: string, event: WebhookEvent, paylo
|
||||
"X-Epicure-Event": event,
|
||||
},
|
||||
body,
|
||||
// Redirects could point at internal services, so treat 3xx as failure.
|
||||
redirect: "manual",
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
statusCode = res.status;
|
||||
|
||||
Reference in New Issue
Block a user