Skip to content

SSRF via Webhook and Integration Plugin Registration (Discord, Slack, Generic Webhook, Dependency-Track)

Moderate
jiparis published GHSA-223g-m8gr-wcch Sep 11, 2026

Package

gomod github.com/chainloop-dev/chainloop (Go)

Affected versions

<= v1.108.6

Patched versions

v1.108.6

Description

SSRF via Webhook and Integration Plugin Registration (Discord, Slack, Generic Webhook, Dependency-Track)

Summary

Four integration plugins in the Chainloop control plane make outbound HTTP requests to user-supplied URLs without any IP address or network-level validation. An authenticated organization administrator can register a malicious webhook/integration URL pointing at internal services, cloud metadata endpoints (e.g. http://169.254.169.254/latest/meta-data/), or other non-public hosts. The Discord webhook plugin provides full-read SSRF because the response body is decoded and persisted in the registration configuration that is returned to the caller.

Affected Components

Plugin File Sink Type
Discord webhook app/controlplane/plugins/core/discord-webhook/v1/discord.go L92 http.Get(request.WebhookURL) Full-read (response body decoded as JSON, stored in config)
Discord webhook same file L219 http.Post(webhookURL, ...) Blind POST
Slack webhook app/controlplane/plugins/core/slack-webhook/v1/slack_webhook.go L142 http.Post(webhookURL, ...) Blind POST
Generic webhook app/controlplane/plugins/core/webhook/v1/webhook.go L245-255 http.NewRequestWithContext + client.Do(req) Blind POST
Dependency-Track app/controlplane/plugins/core/dependency-track/v1/client/sbom.go L207-214, L268-278, L307-314 http.NewRequest + http.DefaultClient.Do(req) Full-read GET + Blind POST

Root Cause

None of the four plugins validate the resolved IP address of the target URL before connecting. There is no SSRF-safe HTTP client, no DNS-pinning connector, and no private/reserved/loopback address check anywhere in the plugin SDK or the individual plugins.

The validateURL helper in the generic webhook plugin checks only the URL scheme (http/https) and syntactic validity; the Discord and Slack plugins perform no validation at all.

Impact

  1. Internal network scanning: the server can be directed to probe arbitrary internal hosts and ports. HTTP status codes and error messages reveal reachability.

  2. Cloud metadata exfiltration: on cloud-hosted deployments the server can reach http://169.254.169.254/ (AWS IMDS), http://metadata.google.internal/, or http://169.254.169.254/metadata/instance (Azure IMDS) to retrieve instance credentials, IAM role tokens, and other secrets.

  3. Full-read data theft (Discord + Dependency-Track): the Discord plugin decodes the HTTP response body as JSON and stores the name and user.username fields in the registration config object, which is returned to the caller in the Register response and via the DescribeRegistration endpoint. Internal services that return JSON with these field names leak their response content to the attacker. The Dependency-Track plugin similarly reads and parses response bodies from the configured host.

  4. Blind request forgery (all four): the server can be used as a proxy to send crafted POST payloads to internal services, potentially triggering state-changing operations on services that trust the server's network address.

Reproduction

  1. Deploy a Chainloop control plane instance with default settings.
  2. Authenticate as an organization admin/owner.
  3. Register a Discord webhook integration with webhook set to http://169.254.169.254/latest/meta-data/:
chainloop integration registered add \
  --name ssrf-test \
  --driver discord-webhook \
  --opt webhook=http://169.254.169.254/latest/meta-data/
  1. On a cloud instance (AWS/GCP/Azure), the server makes http.Get("http://169.254.169.254/latest/meta-data/") and attempts to decode the response as JSON. The HTTP request reaches the metadata service.

  2. For the generic webhook plugin, the server sends a POST with an attestation-shaped JSON body to the target URL:

chainloop integration registered add \
  --name ssrf-webhook \
  --driver webhook \
  --opt url=http://internal-service:8080/admin/shutdown

Suggested Fix

Introduce a shared SSRF-safe HTTP transport for all plugins that:

  1. Resolves the target hostname via DNS.
  2. Checks the resolved IP address against a deny-list of non-public ranges (loopback, RFC 1918, link-local, IPv6 transition forms including ::ffff:, 2002::/16, 64:ff9b::/96, Teredo 2001::/32).
  3. Connects to the validated IP (DNS pinning) to prevent TOCTOU/DNS-rebinding bypasses.
  4. Re-validates the resolved address on every redirect hop.

Go example using a custom net.Dialer with address validation in DialContext:

func ssrfSafeTransport() *http.Transport {
    return &http.Transport{
        DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
            host, port, _ := net.SplitHostPort(addr)
            ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
            if err != nil {
                return nil, err
            }
            for _, ip := range ips {
                if !ip.IP.IsGlobalUnicast() || ip.IP.IsPrivate() || ip.IP.IsLoopback() || ip.IP.IsLinkLocalUnicast() {
                    return nil, fmt.Errorf("SSRF blocked: %s resolves to non-public address %s", host, ip.IP)
                }
            }
            dialer := &net.Dialer{}
            return dialer.DialContext(ctx, network, net.JoinHostPort(ips[0].IP.String(), port))
        },
    }
}

Credit

Discovered by tonghuaroot.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
Required
Scope
Changed
Confidentiality
Low
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:N/A:N

CVE ID

No known CVE

Weaknesses

No CWEs

Credits