mirror of
https://github.com/documenso/documenso.git
synced 2026-07-24 08:54:20 +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:
@@ -19,20 +19,16 @@ export type WebhookLogsSheetProps = {
|
||||
};
|
||||
|
||||
export const WebhookLogsSheet = createCallable<WebhookLogsSheetProps, string | null>(
|
||||
({ call, webhookCall: initialWebhookCall }) => {
|
||||
({ call, webhookCall }) => {
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [webhookCall, setWebhookCall] = useState(initialWebhookCall);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'request' | 'response'>('request');
|
||||
|
||||
const { mutateAsync: resendWebhookCall, isPending: isResending } =
|
||||
trpc.webhook.calls.resend.useMutation({
|
||||
onSuccess: (result) => {
|
||||
toast({ title: t`Webhook successfully sent` });
|
||||
|
||||
setWebhookCall(result);
|
||||
onSuccess: () => {
|
||||
toast({ title: t`Webhook queued for resend` });
|
||||
},
|
||||
onError: () => {
|
||||
toast({ title: t`Something went wrong` });
|
||||
@@ -71,20 +67,20 @@ export const WebhookLogsSheet = createCallable<WebhookLogsSheetProps, string | n
|
||||
<h2 className="text-lg font-semibold">
|
||||
<Trans>Webhook Details</Trans>
|
||||
</h2>
|
||||
<p className="text-muted-foreground font-mono text-xs">{webhookCall.id}</p>
|
||||
<p className="font-mono text-xs text-muted-foreground">{webhookCall.id}</p>
|
||||
</SheetTitle>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="mt-6">
|
||||
<div className="flex items-end justify-between">
|
||||
<h4 className="text-muted-foreground mb-3 text-xs font-semibold uppercase tracking-wider">
|
||||
<h4 className="mb-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
<Trans>Details</Trans>
|
||||
</h4>
|
||||
|
||||
<Button
|
||||
onClick={() =>
|
||||
resendWebhookCall({
|
||||
void resendWebhookCall({
|
||||
webhookId: webhookCall.webhookId,
|
||||
webhookCallId: webhookCall.id,
|
||||
})
|
||||
@@ -98,15 +94,15 @@ export const WebhookLogsSheet = createCallable<WebhookLogsSheetProps, string | n
|
||||
<Trans>Resend</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border-border overflow-hidden rounded-lg border">
|
||||
<div className="overflow-hidden rounded-lg border border-border">
|
||||
<table className="w-full text-left text-sm">
|
||||
<tbody className="divide-border bg-muted/30 divide-y">
|
||||
<tbody className="divide-y divide-border bg-muted/30">
|
||||
{generalWebhookDetails.map(({ header, value }, index) => (
|
||||
<tr key={index}>
|
||||
<td className="text-muted-foreground border-border w-1/3 border-r px-4 py-2 font-mono text-xs">
|
||||
<td className="w-1/3 border-r border-border px-4 py-2 font-mono text-xs text-muted-foreground">
|
||||
{header}
|
||||
</td>
|
||||
<td className="text-foreground break-all px-4 py-2 font-mono text-xs">
|
||||
<td className="break-all px-4 py-2 font-mono text-xs text-foreground">
|
||||
{value}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -118,13 +114,13 @@ export const WebhookLogsSheet = createCallable<WebhookLogsSheetProps, string | n
|
||||
|
||||
{/* Payload Tabs */}
|
||||
<div className="py-6">
|
||||
<div className="border-border mb-4 flex items-center gap-4 border-b">
|
||||
<div className="mb-4 flex items-center gap-4 border-b border-border">
|
||||
<button
|
||||
onClick={() => setActiveTab('request')}
|
||||
className={cn(
|
||||
'relative pb-2 text-sm font-medium transition-colors',
|
||||
activeTab === 'request'
|
||||
? 'text-foreground after:bg-primary after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5'
|
||||
? 'text-foreground after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-primary'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
@@ -136,7 +132,7 @@ export const WebhookLogsSheet = createCallable<WebhookLogsSheetProps, string | n
|
||||
className={cn(
|
||||
'relative pb-2 text-sm font-medium transition-colors',
|
||||
activeTab === 'response'
|
||||
? 'text-foreground after:bg-primary after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5'
|
||||
? 'text-foreground after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-primary'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
@@ -155,7 +151,7 @@ export const WebhookLogsSheet = createCallable<WebhookLogsSheetProps, string | n
|
||||
onCopySuccess={() => toast({ title: t`Copied to clipboard` })}
|
||||
/>
|
||||
</div>
|
||||
<pre className="bg-muted/50 border-border text-foreground overflow-x-auto rounded-lg border p-4 font-mono text-xs leading-relaxed">
|
||||
<pre className="overflow-x-auto rounded-lg border border-border bg-muted/50 p-4 font-mono text-xs leading-relaxed text-foreground">
|
||||
{JSON.stringify(
|
||||
activeTab === 'request' ? webhookCall.requestBody : webhookCall.responseBody,
|
||||
null,
|
||||
@@ -166,19 +162,19 @@ export const WebhookLogsSheet = createCallable<WebhookLogsSheetProps, string | n
|
||||
|
||||
{activeTab === 'response' && (
|
||||
<div className="mt-6">
|
||||
<h4 className="text-muted-foreground mb-3 text-xs font-semibold uppercase tracking-wider">
|
||||
<h4 className="mb-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
<Trans>Response Headers</Trans>
|
||||
</h4>
|
||||
<div className="border-border overflow-hidden rounded-lg border">
|
||||
<div className="overflow-hidden rounded-lg border border-border">
|
||||
<table className="w-full text-left text-sm">
|
||||
<tbody className="divide-border bg-muted/30 divide-y">
|
||||
<tbody className="divide-y divide-border bg-muted/30">
|
||||
{Object.entries(webhookCall.responseHeaders as Record<string, string>).map(
|
||||
([key, value]) => (
|
||||
<tr key={key}>
|
||||
<td className="text-muted-foreground border-border w-1/3 border-r px-4 py-2 font-mono text-xs">
|
||||
<td className="w-1/3 border-r border-border px-4 py-2 font-mono text-xs text-muted-foreground">
|
||||
{key}
|
||||
</td>
|
||||
<td className="text-foreground break-all px-4 py-2 font-mono text-xs">
|
||||
<td className="break-all px-4 py-2 font-mono text-xs text-foreground">
|
||||
{value as string}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user