mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 01:15:49 +10:00
fix: improve webhook execution (#2608)
Webhook URLs were being fetched without validating whether they resolved to private/loopback addresses, exposing the server to SSRF. Current SSRF is best effort and fail open, you should never host services that you cant risk exposure of. This extracts webhook execution into a shared module that validates URLs against private IP ranges (including DNS resolution), enforces timeouts, and disables redirect following. The resend route now queues through the job system instead of calling fetch inline.
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Race a promise against a timeout. Returns `null` if the timeout
|
||||
* fires before the promise settles.
|
||||
*/
|
||||
export const withTimeout = async <T>(promise: Promise<T>, timeoutMs: number) =>
|
||||
await Promise.race<T | null>([
|
||||
promise,
|
||||
new Promise<null>((resolve) => {
|
||||
setTimeout(() => resolve(null), timeoutMs);
|
||||
}),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Wrapper around `fetch` that aborts the request after `timeoutMs`.
|
||||
* Throws with a descriptive message on timeout.
|
||||
*/
|
||||
export const fetchWithTimeout = async (
|
||||
input: string | URL | Request,
|
||||
init: RequestInit & { timeoutMs: number },
|
||||
) => {
|
||||
const { timeoutMs, ...fetchInit } = init;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
return await fetch(input, { ...fetchInit, signal: controller.signal });
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === 'AbortError') {
|
||||
throw new Error(`Request timed out after ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
throw err;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user