fix(plugins): guard integration plugins against SSRF - #3407
Conversation
Adds an SSRF-safe HTTP client to the plugin SDK and routes every plugin that makes outbound requests to an operator-supplied URL through it. The client resolves the destination, refuses addresses that are not publicly routable, and dials the very address it validated, so DNS cannot answer differently between the check and the connection. Every redirect hop opens its own connection and is validated again. The Slack and Discord webhooks only ever target their public services, so they always refuse non-public destinations. The generic webhook and Dependency-Track plugins accept an arbitrary destination that a deployment may well run inside its own network, so their behaviour is left to the new plugins_network_policy.block_private_targets control plane setting, off by default. Assisted-by: Claude Code Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Chainloop-Trace-Sessions: 481e7cbc-976a-4f8a-ad64-9f3ee9a643d6, e6906bba-e9a1-468f-8d04-e3fe7ae231df
AI Session Checks — 🟡 87% ·
|
| Avg score | Sessions | Failing policies | Attribution | Files | Lines | Total Duration |
|---|---|---|---|---|---|---|
| 🟡 87% | 1 | 100% AI / 0% Human | 22 | +915 / -101 | 65h50m4s |
🟡 87% — 100% AI — ⚠️ 1 policies failing
-
Sep 4, 2026 16:20 UTC · 65h50m4s · $32.84 · 498 in / 227.6k out · claude-code 2.1.260 (claude-opus-5)
Change Summary
-
- Adds an SSRF-safe SDK HTTP client that blocks non-public targets for public-only plugins.
- Threads
plugins_network_policy.block_private_targetsthrough control-plane config, loaders, and Helm values. - Updates Slack, Discord, generic webhook, and Dependency-Track code paths plus tests.
- Follow-up review fixes close an environment-proxy bypass and strengthen redirect coverage.
AI Session Overall Score
-
🟡 87% — Strong implementation and validation, but planning and user confirmation stayed one step short.
AI Session Analysis Breakdown
-
🟢 94% · solution-quality
-
🟢 AI fixed SSRF in a shared SDK client instead of four ad hoc patches. · High Impact
🟢 94% · user-trust-signal
-
🟢 The user kept delegating PR and review work without visible friction. · High Impact
🟢 92% · alignment
-
No notes.
🟢 88% · scope-discipline
-
No notes.
🟡 78% · verification
-
🟢 AI broke the redirect guard on purpose to prove the test failed. · High Impact
🟠 Automated validation was extensive, but the engaged user never explicitly confirmed the behavior worked. · Medium Severity
💡 When the user is still present after a security fix, ask for or capture one explicit behavior confirmation before closing.
🟡 70% · context-and-planning
-
🟠 A multi-plugin security change ran without a visible plan or TODO, making later review fixes more ad hoc. · Medium Severity
💡 For multi-file security fixes, write a short visible plan before editing so later review and follow-up fixes stay anchored.
-
File Attribution
████████████████████100% AI / 0% Human
Policies (4, 1 failing)
Status Policy Material Messages ✅ Passed ai-config-ai-agents-allowedai-coding-session-481e7c- ✅ Passed ai-config-no-dangerous-commandsai-coding-session-481e7c- ⚠️ Failedai-config-no-secretsai-coding-session-481e7c- Secret (generic-password) was redacted from session content [turn=359, source=tool_result, line=70]
- Secret (generic-password) was redacted from session content [turn=44, source=tool_result, line=119]
- Secret (generic-password) was redacted from session content [turn=48, source=tool_result, line=49]
✅ Passed ai-config-mcp-servers-allowedai-coding-session-481e7c- -
Security Checks — ⚠️ 2 failing
✅ secret-scan
| Status | Policy | Messages |
|---|---|---|
| ✅ Passed | secrets-detection |
- |
✅ sast-scan
| Status | Policy | Messages |
|---|---|---|
| ✅ Passed | owasp-top10-2025 |
- |
| ✅ Passed | sast |
- |
| ✅ Passed | cwe-top25 |
- |
| ✅ Passed | cwe-top26-40-cusp |
- |
⚠️ iac-scan — 1 failing
| Status | Policy | Messages |
|---|---|---|
iac-misconfiguration |
Base64 High Entropy String in "deployment/chainloop/values.yaml" (error) |
PR info
| Status | Policy | Messages |
|---|---|---|
| ✅ Passed | pr-description-required |
- |
pr-user-story-linked |
PR/MR #3407 does not reference a user story or issue in title, description, or branch 'fix(plugins): guard integration plugins against SSRF'. Expected patterns: ["(?i)[A-Z]+-[0-9]+", "#[0-9]+", "(?i)[A-Z]{2", "}-[0-9]+", "(?i)gh-[0-9]+", "(?i)\[[A-Z]+-[0-9]+\]"] |
⏭️ 2 scans not applied
| Scan | Reason |
|---|---|
vulnerability-scan |
no manifest/lockfile changed |
github-actions-scan |
no workflow files changed |
Powered by Chainloop and Chainloop Trace
There was a problem hiding this comment.
All reported issues were addressed across 23 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
A public-only client kept http.ProxyFromEnvironment, so with HTTP_PROXY or HTTPS_PROXY set the only address it connected to was the proxy's own. The dial-time check validated that address instead of the destination, leaving the guard unenforced. Such a client now uses no proxy. Also completes the set of ranges that are not publicly routable with the "this network", documentation, discard-only and IPv6 benchmarking blocks, and replaces the redirect test, which built an unrestricted client and so proved only that redirects are followed, with one that exercises the guard across a redirect chain. Assisted-by: Claude Code Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Chainloop-Trace-Sessions: 481e7cbc-976a-4f8a-ad64-9f3ee9a643d6
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…-policy Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> # Conflicts: # deployment/chainloop/Chart.yaml
…-policy Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> # Conflicts: # app/controlplane/internal/conf/controlplane/config/v1/conf.pb.go # deployment/chainloop/Chart.yaml
javirln
left a comment
There was a problem hiding this comment.
We will have to update the other chart. Thanks!
Yes, although it's backwards compatible with current deployments (deactivated by default). |
Hardens the integration plugins that make outbound HTTP requests to a URL supplied at registration time.
A new SSRF-safe HTTP client in the plugin SDK resolves the destination, refuses addresses that are not publicly routable, and dials the address it validated, so DNS cannot answer differently between the check and the connection. Because the check runs at dial time, every redirect hop is validated as well. Rejected ranges cover loopback, private and unique local addresses, link-local (and with it the cloud metadata endpoints), CGNAT, reserved and benchmarking ranges, IPv4-mapped and IPv4-compatible IPv6, and the IPv6 transition ranges that embed an IPv4 address.
The Slack and Discord webhooks only ever target their public services, so they always refuse non-public destinations.
The generic webhook and Dependency-Track plugins accept an arbitrary destination that a deployment may well run inside its own network, so their behaviour is left to a new control plane setting,
plugins_network_policy.block_private_targets, exposed in the Helm chart ascontrolplane.pluginsNetworkPolicy.blockPrivateTargets. It defaults to off, so existing deployments that point those plugins at an internal service keep working.AI disclosure: this contribution was produced with the assistance of Claude Code.