fix(security): prevent SSRF via webhook URL validation#454
Merged
Conversation
Webhook URLs (execution webhooks and observability webhooks) were only validated for HTTP/HTTPS scheme, allowing users to target internal services, cloud metadata endpoints (169.254.169.254), and RFC-1918 private networks through the server acting as an open proxy. This adds two layers of defense: - Registration-time validation: ValidateWebhookURL rejects URLs pointing to private/loopback/link-local IPs before they are stored. - Transport-level enforcement: NewSSRFSafeClient uses a custom DialContext that resolves DNS and rejects private IPs before the TCP connection is established, preventing DNS rebinding attacks. Closes #418 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Contributor
📊 Coverage gateThresholds from
✅ Gate passedNo surface regressed past the allowed threshold and the aggregate stayed above the floor. |
Contributor
📐 Patch coverage gateThreshold: 80% on lines this PR touches vs
✅ Patch gate passedEvery surface whose lines were touched by this PR has patch coverage at or above the threshold. |
The strict SSRF filter rejected legitimate RFC-1918 callbacks inside Docker/K8s clusters, breaking the functional test that webhooks back to the test-runner container at an internal bridge IP. Add an allowlist (hosts, wildcards, CIDRs) that bypasses the private-IP check for explicitly trusted targets, mirroring the existing `serverless_discovery_allowed_hosts` pattern: - `AGENTFIELD_WEBHOOK_ALLOWED_HOSTS` env var / `webhook_allowed_hosts` YAML field feeds services.SetWebhookAllowedHosts at server startup. - Both `ValidateWebhookURL` and `NewSSRFSafeClient`'s DialContext honor the allowlist before applying private-IP rejection. - Functional test docker-compose files set the env to "test-runner" so the existing webhook contract test passes without weakening the gate. Added tests for allowlist parsing, hostname/wildcard/CIDR matching, bypass behavior in both validators, dialer error paths, and an end-to-end test that loopback traffic flows when 127.0.0.0/8 is allowlisted. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #418 — SSRF via unvalidated webhook URLs allows internal network access and cloud credential exfiltration.
ValidateWebhookURLrejects URLs targeting private/loopback/link-local/RFC-1918 IPs andlocalhostbefore they are storedNewSSRFSafeClientresolves DNS and rejects private IPs at dial time, preventing DNS rebinding attacksnormalizeWebhookRequest) and observability webhooks (SetWebhookHandler)Blocked IP ranges
127.0.0.0/8,::1169.254.0.0/16,fe80::/1010.0.0.0/8,172.16.0.0/12,192.168.0.0/16fc00::/70.0.0.0,::localhost,*.localhostTest plan
TestIsPrivateIP— all private/public IP classificationsTestIsPrivateHost— localhost hostname detectionTestValidateWebhookURL— URL-level SSRF rejection (12 cases)TestNewSSRFSafeClient_BlocksPrivateIPs— transport blocks loopback httptest serverTestNormalizeWebhookRequest/ssrf_*— 8 SSRF cases added to existing handler testsHTTPClientfor test servers)🤖 Generated with Claude Code