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

> Learn how Buzz secures outbound webhook calls against SSRF attacks. Discover protections like IP resolution, private network blocking, and disabling redirects.

- Repository: [Block Open Source/buzz](https://github.com/block/buzz)
- Tags: architecture
- Published: 2026-08-29

---

**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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/crates/buzz-workflow/src/schema.rs). The following YAML configures a workflow triggered by an incoming webhook that then calls an external API:

```yaml
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:

```bash
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:

```rust
// 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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/crates/buzz-workflow/src/executor.rs)). This prevents attackers from redirecting requests to internal addresses after passing the initial URL validation checks.