Skip to content

Block webhook SSRF via hostnames that resolve to internal IPs - #230

Merged
getappz merged 1 commit into
masterfrom
devin/1784314734-webhook-ssrf-dns
Jul 18, 2026
Merged

Block webhook SSRF via hostnames that resolve to internal IPs#230
getappz merged 1 commit into
masterfrom
devin/1784314734-webhook-ssrf-dns

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

validate_webhook_url (crates/agentflare-backend/src/webhook.rs) blocked SSRF only when the webhook host was an IP literal in a private/link-local/loopback range. A domain host was let through after only a literal == "localhost" check, so a hostname that resolves to an internal address bypassed the guard entirely:

create webhook url=http://metadata.internal/    # resolves to 169.254.169.254 → passed

An agent registering webhooks over MCP could thus point delivery at cloud metadata endpoints or other loopback/RFC1918 services and receive the response body back (it is logged to webhook_logs).

This PR resolves domain hosts and applies the same is_blocked_ip check to every resolved address:

Some(url::Host::Domain(d)) =>
    d.eq_ignore_ascii_case("localhost") || domain_resolves_to_blocked_ip(d, port)

domain_resolves_to_blocked_ip uses ToSocketAddrs and rejects if any resolved IP is blocked. A resolution failure is treated as "not provably internal" and allowed through, so a transient DNS error can't take down legitimate hooks (a genuinely unresolvable host fails at send anyway).

deliver() now re-validates the stored URL on every send, not just at creation — this narrows the DNS-rebinding window where a host validated as external at create time is later repointed at an internal IP. A blocked send is recorded as a blocked_ssrf delivery-log row instead of hitting the network.

This complements the existing hardening already in this file (redirects disabled, response body capped, HMAC-signed payloads).

Test plan

  • cargo test -p agentflare-backend (69 passed)
  • cargo clippy -p agentflare-backend --all-targets --all-features -- -D warnings -A unsafe_code -A clippy::pedantic
  • cargo fmt --check

New test domain_resolution_flags_names_pointing_at_loopback pins the resolution path using localhost (deterministic offline via /etc/hosts).

Notes for reviewers

  • Risk areas / edge cases: validate_webhook_url now performs DNS resolution, so create/update/deliver can incur a lookup. Resolution errors intentionally do not block (availability over strictness); the check only fires on a successful resolution to an internal IP. Residual TOCTOU between resolve and connect remains (ureq connects by hostname) but the per-send re-validation shrinks it substantially.
  • Backwards compatibility: no API/schema change. Only previously-exploitable URLs (external-looking names resolving to internal ranges) are newly rejected; a new blocked_ssrf value can appear in webhook_logs.response_status.

Link to Devin session: https://app.devin.ai/sessions/f86e3441ccb34626ad93fb5023a26134
Requested by: @getappz

Summary by CodeRabbit

  • Bug Fixes
    • Strengthened webhook URL validation to block destinations resolving to private, loopback, link-local, or otherwise restricted addresses.
    • Re-checks webhook destinations before each delivery to help prevent DNS rebinding attacks.
    • Records blocked delivery attempts without sending the request.
    • Added coverage for hostnames resolving to loopback addresses.

@getappz getappz self-assigned this Jul 17, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@getappz, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 17 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6fd43579-2d12-45ee-9182-4ebe4c46f863

📥 Commits

Reviewing files that changed from the base of the PR and between bbea569 and 530e862.

📒 Files selected for processing (1)
  • crates/agentflare-backend/src/webhook.rs
📝 Walkthrough

Walkthrough

Changes

Webhook SSRF validation

Layer / File(s) Summary
Domain resolution and URL validation
crates/agentflare-backend/src/webhook.rs
Domain hosts are resolved and rejected when any address is blocked; localhost handling and a loopback-resolution test are included.
Delivery-time SSRF blocking
crates/agentflare-backend/src/webhook.rs
Stored webhook URLs are revalidated before delivery, with rejected attempts logged as blocked_ssrf without issuing HTTP requests.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: getappz

Sequence Diagram(s)

sequenceDiagram
  participant deliver
  participant validate_webhook_url
  participant log_delivery
  participant HTTPClient
  deliver->>validate_webhook_url: validate stored webhook.url
  validate_webhook_url-->>deliver: validation result
  deliver->>log_delivery: record blocked_ssrf when rejected
  deliver->>HTTPClient: send request when allowed
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: hardening webhook SSRF checks for hostnames that resolve to internal IPs.
Description check ✅ Passed The description matches the template and includes summary, test plan, and reviewer notes with relevant implementation details.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1784314734-webhook-ssrf-dns

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/agentflare-backend/src/webhook.rs`:
- Around line 166-167: Update the host validation logic around the IPv4 and IPv6
branches to normalize IPv4-mapped IPv6 addresses with to_ipv4_mapped() and run
them through the existing IPv4 blocking checks before applying IPv6 checks.
Ensure mapped localhost literals such as ::ffff:127.0.0.1 are rejected, and add
a regression test covering that URL.
- Around line 147-151: Update validate_webhook_url and deliver so DNS resolution
and blocked-IP validation are tied to the same outbound connection, preventing
hostname rebinding between preflight and connect; connect using the validated
resolved address or otherwise revalidate the address immediately before sending.
Extend is_blocked_ip to normalize IPv4-mapped IPv6 addresses and apply the IPv4
blocked-range checks to the mapped address.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 64ee0b2b-18ea-4f98-bef8-631093319b85

📥 Commits

Reviewing files that changed from the base of the PR and between a9d3095 and bbea569.

📒 Files selected for processing (1)
  • crates/agentflare-backend/src/webhook.rs

Comment thread crates/agentflare-backend/src/webhook.rs
Comment on lines 166 to 167
Some(url::Host::Ipv4(v4)) => is_blocked_ip(std::net::IpAddr::V4(v4)),
Some(url::Host::Ipv6(v6)) => is_blocked_ip(std::net::IpAddr::V6(v6)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant file section with line numbers.
sed -n '130,230p' crates/agentflare-backend/src/webhook.rs

# Look for related IP checks and tests.
rg -n "to_ipv4_mapped|is_blocked_ip|loopback|localhost|private|unique_local|link_local|multicast" crates/agentflare-backend/src -S

Repository: getappz/agentflare

Length of output: 6157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the IP predicate and nearby tests.
sed -n '110,210p' crates/agentflare-backend/src/webhook.rs
printf '\n--- TESTS ---\n'
sed -n '420,510p' crates/agentflare-backend/src/webhook.rs

Repository: getappz/agentflare

Length of output: 7270


Reject IPv4-mapped IPv6 literals.

http://[::ffff:127.0.0.1]/ bypasses the IPv6 branch because is_loopback() only catches ::1. Normalize mapped addresses with to_ipv4_mapped() and apply the IPv4 checks first; add a regression test for a mapped localhost URL.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/agentflare-backend/src/webhook.rs` around lines 166 - 167, Update the
host validation logic around the IPv4 and IPv6 branches to normalize IPv4-mapped
IPv6 addresses with to_ipv4_mapped() and run them through the existing IPv4
blocking checks before applying IPv6 checks. Ensure mapped localhost literals
such as ::ffff:127.0.0.1 are rejected, and add a regression test covering that
URL.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@getappz
getappz force-pushed the devin/1784314734-webhook-ssrf-dns branch from b9abaff to 530e862 Compare July 18, 2026 05:04
@getappz
getappz self-requested a review as a code owner July 18, 2026 05:04
@getappz
getappz merged commit dbe7f2f into master Jul 18, 2026
16 checks passed
@getappz
getappz deleted the devin/1784314734-webhook-ssrf-dns branch July 18, 2026 05:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant