Medium severity6.3OSV Advisory· Published Aug 26, 2025· Updated Apr 15, 2026
CVE-2025-57818
CVE-2025-57818
Description
Firecrawl turns entire websites into LLM-ready markdown or structured data. Prior to version 2.0.1, a server-side request forgery (SSRF) vulnerability was discovered in Firecrawl's webhook functionality. Authenticated users could configure a webhook to an internal URL and send POST requests with arbitrary headers, which may have allowed access to internal systems. This has been fixed in version 2.0.1. If upgrading is not possible, it is recommend to isolate Firecrawl from any sensitive internal systems.
Affected products
1Patches
21 file changed · +43 −40
apps/api/src/services/webhook.ts+43 −40 modified@@ -1,12 +1,12 @@ -import axios, { AxiosError } from "axios"; +import undici from "undici"; +import { secureDispatcher } from "../scraper/scrapeURL/engines/utils/safeFetch"; import { logger as _logger, logger } from "../lib/logger"; import { supabase_rr_service, supabase_service } from "./supabase"; import { WebhookEventType } from "../types"; import { configDotenv } from "dotenv"; import { z } from "zod"; import { webhookSchema } from "../controllers/v1/types"; import { redisEvictConnection } from "./redis"; -import { index_supabase_service } from "."; configDotenv(); const WEBHOOK_INSERT_QUEUE_KEY = "webhook-insert-queue"; @@ -172,9 +172,13 @@ export const callWebhook = async ({ if (awaitWebhook) { try { - const res = await axios.post( - webhookUrl.url, - { + const res = await undici.fetch(webhookUrl.url, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...webhookUrl.headers, + }, + body: JSON.stringify({ success: !v1 ? data.success : eventType === "crawl.page" @@ -189,15 +193,13 @@ export const callWebhook = async ({ ? data?.error || undefined : undefined, metadata: webhookUrl.metadata || undefined, - }, - { - headers: { - "Content-Type": "application/json", - ...webhookUrl.headers, - }, - timeout: v1 ? 10000 : 30000, // 10 seconds timeout (v1) - }, - ); + }), + dispatcher: secureDispatcher, + signal: AbortSignal.timeout(v1 ? 10000 : 30000), // 10 seconds timeout (v1) + }); + if (!res.ok) { + throw { status: res.status }; + } logWebhook({ success: res.status >= 200 && res.status < 300, teamId, @@ -222,37 +224,38 @@ export const callWebhook = async ({ url: webhookUrl.url, event: eventType, error: error instanceof Error ? error.message : (typeof error === "string" ? error : undefined), - statusCode: error instanceof AxiosError ? error.response?.status : undefined, + statusCode: typeof (error as any)?.status === "number" ? (error as any).status : undefined, }); } } else { - axios - .post( - webhookUrl.url, - { - success: !v1 + undici.fetch(webhookUrl.url, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...webhookUrl.headers, + }, + body: JSON.stringify({ + success: !v1 + ? data.success + : eventType === "crawl.page" ? data.success - : eventType === "crawl.page" - ? data.success - : true, - type: eventType, - [v1 ? "id" : "jobId"]: crawlId, - data: dataToSend, - error: !v1 + : true, + type: eventType, + [v1 ? "id" : "jobId"]: crawlId, + data: dataToSend, + error: !v1 + ? data?.error || undefined + : eventType === "crawl.page" ? data?.error || undefined - : eventType === "crawl.page" - ? data?.error || undefined - : undefined, - metadata: webhookUrl.metadata || undefined, - }, - { - headers: { - "Content-Type": "application/json", - ...webhookUrl.headers, - }, - }, - ) + : undefined, + metadata: webhookUrl.metadata || undefined, + }), + dispatcher: secureDispatcher, + }) .then((res) => { + if (!res.ok) { + throw { status: res.status }; + } logWebhook({ success: res.status >= 200 && res.status < 300, teamId, @@ -278,7 +281,7 @@ export const callWebhook = async ({ url: webhookUrl.url, event: eventType, error: error instanceof Error ? error.message : (typeof error === "string" ? error : undefined), - statusCode: error instanceof AxiosError ? error.response?.status : undefined, + statusCode: typeof (error as any)?.status === "number" ? (error as any).status : undefined, }); }); }
b15fae51a760fix(webhook): use secureDispatcher to avoid SSRF vulnerability
1 file changed · +43 −40
apps/api/src/services/webhook.ts+43 −40 modified@@ -1,12 +1,12 @@ -import axios, { AxiosError } from "axios"; +import undici from "undici"; +import { secureDispatcher } from "../scraper/scrapeURL/engines/utils/safeFetch"; import { logger as _logger, logger } from "../lib/logger"; import { supabase_rr_service, supabase_service } from "./supabase"; import { WebhookEventType } from "../types"; import { configDotenv } from "dotenv"; import { z } from "zod"; import { webhookSchema } from "../controllers/v1/types"; import { redisEvictConnection } from "./redis"; -import { index_supabase_service } from "."; configDotenv(); const WEBHOOK_INSERT_QUEUE_KEY = "webhook-insert-queue"; @@ -172,9 +172,13 @@ export const callWebhook = async ({ if (awaitWebhook) { try { - const res = await axios.post( - webhookUrl.url, - { + const res = await undici.fetch(webhookUrl.url, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...webhookUrl.headers, + }, + body: JSON.stringify({ success: !v1 ? data.success : eventType === "crawl.page" @@ -189,15 +193,13 @@ export const callWebhook = async ({ ? data?.error || undefined : undefined, metadata: webhookUrl.metadata || undefined, - }, - { - headers: { - "Content-Type": "application/json", - ...webhookUrl.headers, - }, - timeout: v1 ? 10000 : 30000, // 10 seconds timeout (v1) - }, - ); + }), + dispatcher: secureDispatcher, + signal: AbortSignal.timeout(v1 ? 10000 : 30000), // 10 seconds timeout (v1) + }); + if (!res.ok) { + throw { status: res.status }; + } logWebhook({ success: res.status >= 200 && res.status < 300, teamId, @@ -222,37 +224,38 @@ export const callWebhook = async ({ url: webhookUrl.url, event: eventType, error: error instanceof Error ? error.message : (typeof error === "string" ? error : undefined), - statusCode: error instanceof AxiosError ? error.response?.status : undefined, + statusCode: typeof (error as any)?.status === "number" ? (error as any).status : undefined, }); } } else { - axios - .post( - webhookUrl.url, - { - success: !v1 + undici.fetch(webhookUrl.url, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...webhookUrl.headers, + }, + body: JSON.stringify({ + success: !v1 + ? data.success + : eventType === "crawl.page" ? data.success - : eventType === "crawl.page" - ? data.success - : true, - type: eventType, - [v1 ? "id" : "jobId"]: crawlId, - data: dataToSend, - error: !v1 + : true, + type: eventType, + [v1 ? "id" : "jobId"]: crawlId, + data: dataToSend, + error: !v1 + ? data?.error || undefined + : eventType === "crawl.page" ? data?.error || undefined - : eventType === "crawl.page" - ? data?.error || undefined - : undefined, - metadata: webhookUrl.metadata || undefined, - }, - { - headers: { - "Content-Type": "application/json", - ...webhookUrl.headers, - }, - }, - ) + : undefined, + metadata: webhookUrl.metadata || undefined, + }), + dispatcher: secureDispatcher, + }) .then((res) => { + if (!res.ok) { + throw { status: res.status }; + } logWebhook({ success: res.status >= 200 && res.status < 300, teamId, @@ -278,7 +281,7 @@ export const callWebhook = async ({ url: webhookUrl.url, event: eventType, error: error instanceof Error ? error.message : (typeof error === "string" ? error : undefined), - statusCode: error instanceof AxiosError ? error.response?.status : undefined, + statusCode: typeof (error as any)?.status === "number" ? (error as any).status : undefined, }); }); }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.