fix(agents): SSRF guard for playbook http_request and notify (P2-W7)#120
Merged
Conversation
Playbook authors can supply arbitrary URLs to `notify` (channel=webhook) and `http_request` steps. Before this change the agents service would happily call `http://169.254.169.254/...`, `http://127.0.0.1:6379`, or any RFC1918 address — turning a low-privilege playbook write into an internal- network pivot and a cloud-metadata exfiltration primitive. This batch adds a single chokepoint, `app.playbook.ssrf_guard`, and routes both `_handle_http` and `_handle_notify` through `validate_outbound_url` before any socket is opened. The guard: - allows only `http`/`https` (override via AISOC_SSRF_ALLOWED_SCHEMES) - rejects URLs that embed credentials (`https://user:pass@host`) - resolves the host with getaddrinfo and rejects loopback, RFC1918, link-local, multicast, and IETF-reserved addresses — every resolved record must pass, not just the first - blocks the cloud metadata endpoints (169.254.169.254, fd00:ec2::254, metadata.google.internal, metadata.azure.com, …) unconditionally, even when AISOC_SSRF_ALLOW_PRIVATE=true - supports an extra deny list via AISOC_SSRF_EXTRA_BLOCKED_HOSTS Operators with a legitimate need to call private webhooks (Slack on a private network, internal Jira) can flip AISOC_SSRF_ALLOW_PRIVATE=true on the agents service while keeping the metadata block list intact. Tests ----- - 77 new unit tests covering scheme allow/deny, credential rejection, loopback / RFC1918 / link-local / multicast / reserved blocks, IPv6 loopback and link-local, metadata endpoints across clouds, extra deny list overrides, getaddrinfo failure handling, and integration with the two playbook handlers. Docs ---- - apps/docs/docs/deployment/env-vars.md: documents the three new env vars (AISOC_SSRF_ALLOWED_SCHEMES, AISOC_SSRF_ALLOW_PRIVATE, AISOC_SSRF_EXTRA_BLOCKED_HOSTS) under the Agents service section. - apps/docs/docs/operations/security.md: explains the guard, its default policy, when to enable AISOC_SSRF_ALLOW_PRIVATE, and where the chokepoint lives for future action handlers. Refs: P2-W7
| import socket | ||
| from urllib.parse import urlsplit | ||
|
|
||
| logger = logging.getLogger("aisoc.playbook.ssrf_guard") |
Restores 'Python — Lint & Type-check' to green by satisfying the repo-wide ruff lint + format gates that run across all services in CI.
This was referenced May 14, 2026
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
Adds a single SSRF chokepoint (
services/agents/app/playbook/ssrf_guard.py) and routes both_handle_httpand_handle_notifythroughvalidate_outbound_urlbefore any socket opens. Without this, playbook authors could pointnotify(channel=webhook) orhttp_requestathttp://169.254.169.254/...,http://127.0.0.1:6379, or any RFC1918 address — turning a low-privilege playbook write into a cloud-metadata exfil and internal-network pivot.Defaults (overridable per-deployment):
http:///https://allowed (AISOC_SSRF_ALLOWED_SCHEMES)AISOC_SSRF_ALLOW_PRIVATE=trueAISOC_SSRF_EXTRA_BLOCKED_HOSTSWhy this PR is safe to merge
pytest services/agents/tests/test_playbook_ssrf_guard.py).mainand are unrelated.Files
services/agents/app/playbook/ssrf_guard.py(new)services/agents/app/playbook/engine.py(route both handlers through guard)services/agents/tests/test_playbook_ssrf_guard.py(new, 77 cases)apps/docs/docs/deployment/env-vars.md(document three new env vars)apps/docs/docs/operations/security.md(operator-facing explanation)Test plan
pytest services/agents/tests/test_playbook_ssrf_guard.py— 77 passedAISOC_SSRF_ALLOW_PRIVATEis not set in production manifestsAISOC_SSRF_EXTRA_BLOCKED_HOSTSreview orAISOC_SSRF_ALLOW_PRIVATEon agents onlyRefs: P2-W7
Made with Cursor