fix(security): SSRF protection for approval callback_url (#435) - #790
Conversation
05c685e to
afcd0e9
Compare
santoshkumarradha
left a comment
There was a problem hiding this comment.
Thanks for the focused security fix. I checked this against the existing SSRF helper surface, and the defense-in-depth shape here is right: reject unsafe callback_url values up front, then enforce the same policy again at dial time.
I also ran the targeted handler tests from the PR locally on the branch head:
cd control-plane && go test ./internal/handlers/ -run 'TestRequestApprovalHandler_Rejects|TestRequestApprovalHandler_PublicCallback|TestNotifyApprovalCallback_UsesSSRFSafeClient|TestApprovalWebhook_CallbackNotification|TestApprovalFlow' -count=1
That passed cleanly. I’m leaving this approved and waiting on the remaining required CI checks before merge.
…#435) The approval callback_url field was accepted without SSRF validation and dispatched via a plain http.Client, allowing an attacker to use the control plane as a proxy to internal services (cloud metadata, RFC-1918, loopback). Changes: - Validate callback_url at registration time using services.ValidateWebhookURL() to reject private/internal targets (localhost, 169.254.x, 10.x, 172.16.x, 192.168.x, ::1) before the URL is persisted. - Replace the plain http.Client in notifyApprovalCallback with services.NewSSRFSafeClient() which enforces DNS-rebinding-safe private-IP blocking at dial time. - Add comprehensive test coverage for both registration rejection and runtime transport enforcement. Fixes Agent-Field#435
Head branch was pushed to by a user without write access
094d0b6 to
2efee0c
Compare
|
All required checks are green ✅ the only failures are the pre-existing timeout (same deadlock on ). Ready to merge when you get a chance. Thanks for the review! |
|
Quick maintainer update: I reran the required checks on Saturday, July 18, 2026, and the same required Full documentation is at https://coverage.readthedocs.io/en/7.13.5 workflow is still failing on the PR head, specifically followed by the aggregate job. The approval still stands, but we still cannot merge until that required check goes green on the contributor branch. |
|
Quick maintainer update: I reran the required checks on Saturday, July 18, 2026, and the same required Coverage Summary workflow is still failing on the PR head, specifically coverage (control-plane) followed by the aggregate coverage-summary job. The approval still stands, but we still cannot merge until that required check goes green on the contributor branch. |
…ient The existing TestExecuteReasonerAndWebhookHelpersCoverage test calls notifyApprovalCallback against a httptest server (127.0.0.1). After switching to NewSSRFSafeClient (Agent-Field#435), the SSRF transport rejects loopback as a private IP, causing the test to hang waiting for callbacks that never arrive. Fix: set services.SetWebhookAllowedHosts([]string{"127.0.0.1"}) before the callback calls so the httptest server is reachable.
📊 Coverage gateThresholds from
✅ Gate passedNo surface regressed past the allowed threshold and the aggregate stayed above the floor. |
📐 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. |
|
@santoshkumarradha review pls |
|
The CI failed because the existing |
santoshkumarradha
left a comment
There was a problem hiding this comment.
Thanks for chasing the test follow-up quickly. I reran the full control-plane handler test package locally on the current head, and the SSRF fix plus the loopback test adjustment both look good from the maintainer side.
The
approval callback_urlfield was accepted without SSRF validation and dispatched via a plainhttp.Client, allowing an attacker to use the control plane as a proxy to internal services (cloud metadata, RFC-1918, loopback).Changes
callback_urlat registration time usingservices.ValidateWebhookURL()to reject private/internal targets (localhost,169.254.x,10.x,172.16.x,192.168.x,::1) before the URL is persisted.http.ClientinnotifyApprovalCallbackwithservices.NewSSRFSafeClient(), which enforces DNS-rebinding-safe private-IP blocking at dial time.Fixes #435
Summary
Closes an SSRF vulnerability (CWE-918) in the approval callback flow. Agents could register arbitrary
callback_urlvalues (including cloud metadata endpoints and RFC-1918 addresses) that the control plane would POST to upon approval resolution — effectively turning the CP into an open proxy. This PR adds input validation at registration time and switches the outbound HTTP client to the existing SSRF-safe transport as defense in depth.Type of change
Test plan
cd control-plane && go test ./internal/handlers/ -run "TestRequestApprovalHandler_Rejects" -v -count=1cd control-plane && go test ./internal/handlers/ -run "TestRequestApprovalHandler_PublicCallback" -v -count=1cd control-plane && go test ./internal/handlers/ -run "TestNotifyApprovalCallback_UsesSSRFSafeClient" -v -count=1cd control-plane && go test ./internal/handlers/ -run "TestApprovalWebhook_CallbackNotification" -v -count=1cd control-plane && go test ./internal/handlers/ -run "TestApprovalFlow" -v -count=1Test coverage
coverage-baseline.jsonin this PR only if the removal caused a legitimate regression and I called it out in the summary above.Checklist
Related issues / PRs
Fixes #435