SSRF Protections for Outbound Webhook Calls in Buzz: Security Architecture and Implementation

Buzz prevents Server-Side Request Forgery (SSRF) attacks on outbound webhooks by resolving hostnames to IP addresses before establishing connections, blocking private and reserved network ranges, pinning validated IPs to the HTTP client, and disabling redirects and system proxies.

Buzz is an open-source workflow automation platform that executes arbitrary third-party HTTP requests via the call_webhook action. Because these outbound webhook calls target user-supplied URLs, the platform implements layered SSRF protections to prevent malicious workflows from scanning internal networks, accessing cloud metadata services, or exfiltrating data through server-side request forgery. These security controls are implemented primarily in crates/buzz-workflow/src/executor.rs and enforced at the network boundary before any TCP connection opens.

Pre-Connection Host Validation

Resolving Hostnames Before Network Access

The first line of defense resides in the check_ssrf function within crates/buzz-workflow/src/executor.rs. Before any HTTP request initiates, the system extracts the host component from the target URL and resolves it to concrete IP addresses using the OS resolver. This resolution runs in a dedicated blocking thread pool and completes before any network socket opens, capturing the results at lines 18-25. If DNS resolution yields no addresses, the function aborts immediately with an error, preventing attacks against empty or malformed hosts (lines 38-42).

Rejecting Private and Reserved IP Ranges

Once resolved, each IP address is validated against buzz_core::network::is_private_ip. Located at lines 46-51 in crates/buzz-workflow/src/executor.rs, this guard inspects every resolved address and rejects those belonging to RFC 1918 private networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), link-local ranges (169.254.0.0/16), loopback interfaces (127.0.0.0/8), and other reserved spaces. If any address in the resolution set is private, the entire request aborts with a WebhookError, ensuring workflows cannot target internal infrastructure like Kubernetes API servers or cloud metadata endpoints.

Hardened HTTP Client Configuration

DNS Pinning to Prevent Rebinding Attacks

After validating the IP address, Buzz eliminates the time-of-check to time-of-use (TOCTOU) window through DNS pinning. Inside call_webhook_impl (lines 84-95 of crates/buzz-workflow/src/executor.rs), the system constructs a reqwest::Client using the .resolve(host, SocketAddr) method. This forces the HTTP client to connect exclusively to the pre-validated IP address, preventing DNS rebinding attacks where a malicious server might change its DNS record to an internal address after the initial safety check.

Disabling System Proxies and Redirects

The client builder explicitly disables system proxies with .no_proxy() and sets a redirect policy of Policy::none() (lines 89-94). These configurations serve critical security functions:

  • No Proxy: Prevents a malicious proxy configuration from resolving hostnames independently and bypassing the IP validation step.
  • No Redirects: Blocks HTTP 3xx redirects to internal addresses that would otherwise circumvent the private IP checks performed on the original URL.

Resource Constraints and Secure Error Handling

Response Size Limits and Timeouts

Buzz enforces strict resource boundaries to prevent denial-of-service attacks. The system streams response bodies and truncates them at 1 MiB using the WEBHOOK_MAX_RESPONSE_BYTES constant defined at lines 57-60 in crates/buzz-workflow/src/executor.rs. Additionally, every outbound request has a hard 10-second timeout configured via Client::builder().timeout, limiting the duration a malicious server can hold connections open or attempt slowloris-style attacks.

Error Handling Without Information Leakage

When SSRF violations or network errors occur, Buzz returns WorkflowError::WebhookError as defined in crates/buzz-workflow/src/error.rs (lines 45-48). This error variant ensures that sensitive data, including webhook secrets managed in crates/buzz-relay/src/webhook_secret.rs, never leaks into error messages returned to workflow users or logged output.

Configuring Outbound Webhooks in Practice

Defining a Webhook Workflow

Workflow schemas are defined in crates/buzz-workflow/src/schema.rs. The following YAML configures a workflow triggered by an incoming webhook that then calls an external API:

name: Notify external system
trigger:
  on: webhook                     # workflow is started via an incoming webhook

steps:
  - id: call
    action: call_webhook
    url: https://hooks.example.com/notify
    method: POST
    headers:
      Content-Type: application/json
    body: '{"event":"new_message"}'

Executing via CLI

Deploy and trigger the workflow using the Buzz CLI:

buzz workflows create --file webhook_workflow.yaml

# The CLI prints the webhook secret; include it when invoking the webhook:

curl -X POST http://localhost:3000/api/workflows/webhook \
     -H "X-Webhook-Secret: <secret>" \
     -d '{}'

Runtime SSRF Enforcement Flow

The following Rust pseudocode illustrates how the runtime applies these protections during execution:

// 1️⃣ Resolve and validate the host/IP
let safe_ip = check_ssrf(host, port).await?;

// 2️⃣ Build a client that can only talk to that IP
let client = reqwest::Client::builder()
    .no_proxy()
    .redirect(reqwest::redirect::Policy::none())
    .resolve(host, std::net::SocketAddr::new(safe_ip, port))
    .timeout(Duration::from_secs(10))
    .build()?;

// 3️⃣ Perform the request with size limits enforced during streaming
let resp = client.request(method, url).send().await?;

Summary

  • Pre-resolution: Buzz resolves hostnames to IPs in check_ssrf before opening network connections, preventing TOCTOU vulnerabilities.
  • Private IP blocking: The system rejects RFC 1918, loopback, and link-local addresses using buzz_core::network::is_private_ip.
  • DNS pinning: Validated IPs are pinned to the reqwest::Client via .resolve() to prevent rebinding attacks.
  • Client hardening: System proxies and HTTP redirects are explicitly disabled to prevent bypass techniques.
  • Resource limits: Responses are capped at 1 MiB and requests time out after 10 seconds to prevent resource exhaustion.
  • Secure errors: WorkflowError::WebhookError ensures webhook secrets and internal details never leak through error messages.

Frequently Asked Questions

What happens if a webhook URL resolves to a private IP address?

Buzz aborts the request immediately. In crates/buzz-workflow/src/executor.rs (lines 46-51), the check_ssrf function validates every resolved IP against buzz_core::network::is_private_ip. If any address belongs to a private, reserved, or loopback range, the system returns a WorkflowError::WebhookError and prevents the connection from opening.

How does Buzz prevent DNS rebinding attacks?

The platform implements DNS pinning in call_webhook_impl (lines 84-95). After validating the IP address, Buzz configures the reqwest::Client with .resolve(host, SocketAddr), forcing the client to connect only to the pre-validated IP. Even if an attacker changes the DNS record to point to an internal address after validation, the HTTP client ignores the updated resolution and connects only to the original, safe IP.

What are the response size and timeout limits for webhook calls?

Buzz enforces a 1 MiB maximum response size via the WEBHOOK_MAX_RESPONSE_BYTES constant and streams responses to prevent memory exhaustion. Additionally, all webhook requests have a 10-second timeout configured in the reqwest::Client builder at lines 86-89 of crates/buzz-workflow/src/executor.rs.

Are HTTP redirects allowed in outbound webhook calls?

No. Buzz explicitly disables redirects using reqwest::redirect::Policy::none() in the client builder (lines 92-94 of crates/buzz-workflow/src/executor.rs). This prevents attackers from redirecting requests to internal addresses after passing the initial URL validation checks.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →