Skip to content

fix(plugins): guard integration plugins against SSRF - #3407

Merged
jiparis merged 4 commits into
chainloop-dev:mainfrom
jiparis:ssrf-plugin-network-policy
Sep 8, 2026
Merged

fix(plugins): guard integration plugins against SSRF#3407
jiparis merged 4 commits into
chainloop-dev:mainfrom
jiparis:ssrf-plugin-network-policy

Conversation

@jiparis

@jiparis jiparis commented Sep 7, 2026

Copy link
Copy Markdown
Member

Hardens the integration plugins that make outbound HTTP requests to a URL supplied at registration time.

> cldev integration registered add slack-webhook --name clslack --opt webhook=http://localhost/my/webhook
DBG using config file path="/Users/jiparis/Library/Application Support/chainloop/config.devel.toml"
WRN API contacted in insecure mode
ERR validation error: error validating a webhook: error making request: Post "http://localhost/my/webhook": blocked outbound request: "localhost" resolves to non-public address ::1

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 as controlplane.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.

Review in cubic

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
@chainloop-platform

chainloop-platform Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

AI Session Checks — 🟡 87% · ⚠️ 1 failing

Avg score Sessions Failing policies Attribution Files Lines Total Duration
🟡 87% 1 ⚠️ 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)

View session details ↗

Change Summary

  • Adds an SSRF-safe SDK HTTP client that blocks non-public targets for public-only plugins.
  • Threads plugins_network_policy.block_private_targets through 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

Status Attribution File Lines
modified ai app/controlplane/plugins/sdk/v1/httpclient_test.go +300 / -10
modified ai app/controlplane/plugins/sdk/v1/httpclient.go +230 / -22
created ai app/controlplane/plugins/core/webhook/v1/webhook_test.go +103 / -0
modified ai app/controlplane/plugins/core/dependency-track/v1/extension_test.go +62 / -4
modified ai app/controlplane/plugins/core/dependency-track/v1/client/sbom.go +21 / -17
modified ai app/controlplane/plugins/core/discord-webhook/v1/discord_test.go +36 / -1
modified ai app/controlplane/plugins/core/slack-webhook/v1/slack_webhook_test.go +36 / -1
modified ai app/controlplane/plugins/core/dependency-track/v1/extension.go +21 / -9
modified ai app/controlplane/plugins/core/discord-webhook/v1/discord.go +21 / -9
modified ai app/controlplane/plugins/core/slack-webhook/v1/slack_webhook.go +15 / -8
modified ai app/controlplane/plugins/core/dependency-track/v1/client/sbom_test.go +10 / -7
modified ai app/controlplane/internal/conf/controlplane/config/v1/conf.proto +16 / -0
modified ai app/controlplane/plugins/core/webhook/v1/webhook.go +10 / -3
modified ai app/controlplane/plugins/core/dependency-track/v1/cmd/main.go +9 / -2
modified ai app/controlplane/plugins/plugins.go +7 / -3
modified ai app/controlplane/cmd/main.go +5 / -1
modified ai deployment/chainloop/values.yaml +5 / -0
modified ai devel/integrations.md +2 / -2
modified ai app/controlplane/plugins/sdk/readme-generator/main.go +1 / -1
modified ai deployment/chainloop/Chart.yaml +1 / -1
modified ai deployment/chainloop/README.md +2 / -0
modified ai deployment/chainloop/templates/controlplane/configmap.yaml +2 / -0

Policies (4, 1 failing)

Status Policy Material Messages
✅ Passed ai-config-ai-agents-allowed ai-coding-session-481e7c -
✅ Passed ai-config-no-dangerous-commands ai-coding-session-481e7c -
⚠️ Failed ai-config-no-secrets ai-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-allowed ai-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
⚠️ Failed iac-misconfiguration Base64 High Entropy String in "deployment/chainloop/values.yaml" (error)

PR info

Status Policy Messages
✅ Passed pr-description-required -
⚠️ Failed 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

View attestation ↗


Powered by Chainloop and Chainloop Trace

@jiparis
jiparis requested a review from a team September 7, 2026 09:45

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 23 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread app/controlplane/plugins/sdk/v1/httpclient.go Outdated
Comment thread app/controlplane/plugins/sdk/v1/httpclient_test.go Outdated
Comment thread app/controlplane/plugins/sdk/v1/httpclient.go
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

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread app/controlplane/plugins/sdk/v1/httpclient.go
…-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 javirln left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We will have to update the other chart. Thanks!

@jiparis

jiparis commented Sep 8, 2026

Copy link
Copy Markdown
Member Author

We will have to update the other chart. Thanks!

Yes, although it's backwards compatible with current deployments (deactivated by default).

@jiparis
jiparis merged commit 12468d6 into chainloop-dev:main Sep 8, 2026
15 of 17 checks passed
@jiparis
jiparis deleted the ssrf-plugin-network-policy branch September 8, 2026 15:35
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.

2 participants