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.

4625 vulnerabilities reference this CWE, most recent first.

GHSA-W44W-3R72-GMR4

Vulnerability from github – Published: 2025-01-04 00:33 – Updated: 2025-01-04 00:33
VLAI
Details

The Photo Gallery Slideshow & Masonry Tiled Gallery plugin for WordPress is vulnerable to Server-Side Request Forgery in all versions up to, and including, 1.0.15 via the rjg_get_youtube_info_justified_gallery_callback function. This makes it possible for authenticated attackers, with Subscriber-level access and above, to make web requests to arbitrary locations originating from the web application and can be used to retrieve limited information from internal services.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2024-12237"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-01-03T23:15:06Z",
    "severity": "MODERATE"
  },
  "details": "The Photo Gallery Slideshow \u0026 Masonry Tiled Gallery plugin for WordPress is vulnerable to Server-Side Request Forgery in all versions up to, and including, 1.0.15 via the rjg_get_youtube_info_justified_gallery_callback function. This makes it possible for authenticated attackers, with Subscriber-level access and above, to make web requests to arbitrary locations originating from the web application and can be used to retrieve limited information from internal services.",
  "id": "GHSA-w44w-3r72-gmr4",
  "modified": "2025-01-04T00:33:41Z",
  "published": "2025-01-04T00:33:41Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-12237"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/browser/wp-responsive-photo-gallery/tags/1.0.15/wp-responsive-photo-gallery.php#L3023"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/browser/wp-responsive-photo-gallery/tags/1.0.15/wp-responsive-photo-gallery.php#L3044"
    },
    {
      "type": "WEB",
      "url": "https://www.wordfence.com/threat-intel/vulnerabilities/id/08e44434-8908-4c63-9e5b-9a8b387255d9?source=cve"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-W45G-5746-X9FP

Vulnerability from github – Published: 2026-02-20 21:13 – Updated: 2026-02-23 22:29
VLAI
Summary
OpenClaw hardened cron webhook delivery against SSRF
Details

Affected Packages / Versions

  • openclaw npm package versions <= 2026.2.17.

Vulnerability

Cron webhook delivery in src/gateway/server-cron.ts used fetch() directly, so webhook targets could reach private/metadata/internal endpoints without SSRF policy checks.

Fix Commit(s)

  • 99db4d13e
  • 35851cdaf

Thanks @Adam55A-code for reporting.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2026.2.17"
      },
      "package": {
        "ecosystem": "npm",
        "name": "openclaw"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2026.2.19"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-27488"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-02-20T21:13:03Z",
    "nvd_published_at": "2026-02-21T10:16:13Z",
    "severity": "MODERATE"
  },
  "details": "## Affected Packages / Versions\n\n- `openclaw` npm package versions `\u003c= 2026.2.17`.\n\n## Vulnerability\nCron webhook delivery in `src/gateway/server-cron.ts` used `fetch()` directly, so webhook targets could reach private/metadata/internal endpoints without SSRF policy checks.\n\n## Fix Commit(s)\n- `99db4d13e`\n- `35851cdaf`\n\nThanks @Adam55A-code for reporting.",
  "id": "GHSA-w45g-5746-x9fp",
  "modified": "2026-02-23T22:29:47Z",
  "published": "2026-02-20T21:13:03Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/openclaw/openclaw/security/advisories/GHSA-w45g-5746-x9fp"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-27488"
    },
    {
      "type": "WEB",
      "url": "https://github.com/openclaw/openclaw/commit/99db4d13e5c139883ef0def9ff963e9273179655"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/openclaw/openclaw"
    },
    {
      "type": "WEB",
      "url": "https://github.com/openclaw/openclaw/releases/tag/v2026.2.19"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:L/SA:L",
      "type": "CVSS_V4"
    }
  ],
  "summary": "OpenClaw hardened cron webhook delivery against SSRF"
}

GHSA-W4G9-MXGG-J532

Vulnerability from github – Published: 2026-05-23 00:08 – Updated: 2026-06-26 21:28
VLAI
Summary
Nezha Monitoring: RoleMember-reachable SSRF with full response-body reflection via POST /api/v1/notification
Details

Summary

nezha's dashboard supports two user roles: RoleAdmin (Role==0) and RoleMember (Role==1). The notification routes POST /api/v1/notification and PATCH /api/v1/notification/:id are wired through commonHandler rather than adminHandler — so a RoleMember user can call them. These handlers synchronously Send() an HTTP request to a user-controlled URL and reflect the entire response body (no size limit) back to the caller on any non-2xx response.

Net effect: a low-privilege RoleMember can read intranet HTTP response bodies via the dashboard's hub.

Affected versions

Commit 50dc8e660326b9f22990898142c58b7a5312b42a and earlier on master.

Reachability chain

cmd/dashboard/controller/controller.go:121-122
    auth.GET("/notification", listHandler(listNotification))
    auth.POST("/notification", commonHandler(createNotification))   // <-- commonHandler, not adminHandler

For comparison, /user routes ARE gated by adminHandler:

auth.GET("/user", adminHandler(listUser))
auth.POST("/user", adminHandler(createUser))
auth.POST("/batch-delete/user", adminHandler(batchDeleteUser))

adminHandler (controller.go:220-236) explicitly enforces user.Role.IsAdmin(). commonHandler (controller.go:214-218) does not.

The vulnerable handler

// cmd/dashboard/controller/notification.go:46-83
func createNotification(c *gin.Context) (uint64, error) {
    var nf model.NotificationForm
    if err := c.ShouldBindJSON(&nf); err != nil { return 0, err }
    var n model.Notification
    n.UserID = getUid(c)
    n.Name = nf.Name
    n.RequestMethod = nf.RequestMethod
    n.RequestType = nf.RequestType
    n.RequestHeader = nf.RequestHeader
    n.RequestBody = nf.RequestBody
    n.URL = nf.URL
    ...
    ns := model.NotificationServerBundle{Notification: &n, Server: nil, Loc: singleton.Loc}
    if !nf.SkipCheck {
        if err := ns.Send(singleton.Localizer.T("a test message")); err != nil {
            return 0, err   // <-- err.Error() reflects up to caller via newErrorResponse
        }
    }
    ...
}

Identical pattern in updateNotification (PATCH /notification/:id) at lines 97-146.

The reflection sink

// model/notification.go:113-159
func (ns *NotificationServerBundle) Send(message string) error {
    var client *http.Client
    n := ns.Notification
    if n.VerifyTLS != nil && *n.VerifyTLS {
        client = utils.HttpClient
    } else {
        client = utils.HttpClientSkipTlsVerify
    }
    reqBody, err := ns.reqBody(message)
    if err != nil { return err }
    reqMethod, err := n.reqMethod()
    if err != nil { return err }
    req, err := http.NewRequest(reqMethod, ns.reqURL(message), strings.NewReader(reqBody))
    if err != nil { return err }
    n.setContentType(req)
    if err := n.setRequestHeader(req); err != nil { return err }
    resp, err := client.Do(req)
    if err != nil { return err }
    defer func() { _ = resp.Body.Close() }()
    if resp.StatusCode < 200 || resp.StatusCode > 299 {
        body, _ := io.ReadAll(resp.Body)   // <-- NO io.LimitReader
        return fmt.Errorf("%d@%s %s", resp.StatusCode, resp.Status, string(body))
    } else {
        _, _ = io.Copy(io.Discard, resp.Body)
    }
    return nil
}

The full body (no size limit) is concatenated into an error string. That error flows through commonHandler → handle() → newErrorResponse(err) → c.JSON(http.StatusOK, ...). The intranet response body is JSON-encoded back to the RoleMember caller.

Additional wrinkle: client = utils.HttpClientSkipTlsVerify when VerifyTLS is false — attacker-controlled. So the SSRF works against TLS endpoints too, ignoring cert validation.

PoC

A. Read intranet admin-panel response body

curl -X POST -H "Authorization: Bearer <member-jwt>" \
  -H "Content-Type: application/json" \
  -d '{"name":"x","url":"http://192.168.1.1/admin/index.html","request_method":1,"request_type":1,"verify_tls":false,"skip_check":false}' \
  http://nezha-dashboard.example.com/api/v1/notification

Response:

{"success":false,"error":"401@Unauthorized <full HTML body of the admin login page, no size limit>"}

B. AWS IMDSv2 reachability + body leak

curl -X POST -H "Authorization: Bearer <member-jwt>" \
  -H "Content-Type: application/json" \
  -d '{"name":"x","url":"http://169.254.169.254/latest/meta-data/iam/security-credentials/","request_method":1,"request_type":1,"verify_tls":false,"skip_check":false}' \
  http://nezha-dashboard.example.com/api/v1/notification

IMDSv2 returns 401 with a body explaining the missing token; that body is reflected.

C. DoS via large internal file

Because the body is read via unbounded io.ReadAll, a RoleMember pointing at any internal large-file URL (logs, package mirrors, video) blows up dashboard memory.

Suggested fix

  1. Switch /notification routes to adminHandler. Same fix for /alert-rule, /cron, /ddns if they also issue user-URL requests synchronously. Compare with how /user is already guarded.

go auth.POST("/notification", adminHandler(createNotification)) auth.PATCH("/notification/:id", adminHandler(updateNotification))

  1. SSRF-harden NotificationServerBundle.Send():
  2. Resolve URL host once via net.LookupIP; refuse private/loopback/link-local/CGNAT.
  3. Pin http.Transport.DialContext to the resolved IP — closes DNS-rebinding TOCTOU.
  4. Refuse non-http(s) schemes.

  5. Cap response body: io.LimitReader(resp.Body, 4096). 4 KB is plenty for surfacing webhook errors.

  6. Reconsider VerifyTLS=false toggle on RoleMember-reachable paths — if the route remains member-reachable, at minimum cert validation should be enforced.

Severity

  • CVSS 3.1: Medium — AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:L ≈ 6.4. PR:L because attacker needs a RoleMember account (admin-issued). C:L because intranet response bodies can be read but typically not full credentials. A:L because of the unbounded body-read DoS.
  • Auth: authenticated RoleMember (Role == 1).

Reproduction environment

  • Tested against: nezhahq/nezha:v0.x (commit 50dc8e660326b9f22990898142c58b7a5312b42a).
  • Code locations:
  • Handler: cmd/dashboard/controller/notification.go:46-83, 97-146
  • Sink: model/notification.go:113-159
  • Auth gate: cmd/dashboard/controller/controller.go:121-122 (commonHandler), 214-236 (handler defs)

Reporter

Eddie Ran. Filed via reporter API (PVR enabled). nezha's SECURITY.md mentions email hi@nai.ba for vulnerability reports — happy to also send via email if the maintainer prefers.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/nezhahq/nezha"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.4.0"
            },
            {
              "fixed": "1.14.15-0.20260517022419-d06d539d34c1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-46717"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-863",
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-23T00:08:04Z",
    "nvd_published_at": "2026-06-12T22:16:50Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\nnezha\u0027s dashboard supports two user roles: `RoleAdmin` (Role==0) and `RoleMember` (Role==1). The notification routes `POST /api/v1/notification` and `PATCH /api/v1/notification/:id` are wired through `commonHandler` rather than `adminHandler` \u2014 so a `RoleMember` user can call them. These handlers synchronously `Send()` an HTTP request to a user-controlled URL and reflect the *entire* response body (no size limit) back to the caller on any non-2xx response.\n\nNet effect: a low-privilege `RoleMember` can read intranet HTTP response bodies via the dashboard\u0027s hub.\n\n## Affected versions\n\nCommit `50dc8e660326b9f22990898142c58b7a5312b42a` and earlier on `master`.\n\n## Reachability chain\n\n```\ncmd/dashboard/controller/controller.go:121-122\n    auth.GET(\"/notification\", listHandler(listNotification))\n    auth.POST(\"/notification\", commonHandler(createNotification))   // \u003c-- commonHandler, not adminHandler\n```\n\nFor comparison, `/user` routes ARE gated by `adminHandler`:\n\n```\nauth.GET(\"/user\", adminHandler(listUser))\nauth.POST(\"/user\", adminHandler(createUser))\nauth.POST(\"/batch-delete/user\", adminHandler(batchDeleteUser))\n```\n\n`adminHandler` (controller.go:220-236) explicitly enforces `user.Role.IsAdmin()`. `commonHandler` (controller.go:214-218) does not.\n\n## The vulnerable handler\n\n```go\n// cmd/dashboard/controller/notification.go:46-83\nfunc createNotification(c *gin.Context) (uint64, error) {\n    var nf model.NotificationForm\n    if err := c.ShouldBindJSON(\u0026nf); err != nil { return 0, err }\n    var n model.Notification\n    n.UserID = getUid(c)\n    n.Name = nf.Name\n    n.RequestMethod = nf.RequestMethod\n    n.RequestType = nf.RequestType\n    n.RequestHeader = nf.RequestHeader\n    n.RequestBody = nf.RequestBody\n    n.URL = nf.URL\n    ...\n    ns := model.NotificationServerBundle{Notification: \u0026n, Server: nil, Loc: singleton.Loc}\n    if !nf.SkipCheck {\n        if err := ns.Send(singleton.Localizer.T(\"a test message\")); err != nil {\n            return 0, err   // \u003c-- err.Error() reflects up to caller via newErrorResponse\n        }\n    }\n    ...\n}\n```\n\nIdentical pattern in `updateNotification` (PATCH /notification/:id) at lines 97-146.\n\n## The reflection sink\n\n```go\n// model/notification.go:113-159\nfunc (ns *NotificationServerBundle) Send(message string) error {\n    var client *http.Client\n    n := ns.Notification\n    if n.VerifyTLS != nil \u0026\u0026 *n.VerifyTLS {\n        client = utils.HttpClient\n    } else {\n        client = utils.HttpClientSkipTlsVerify\n    }\n    reqBody, err := ns.reqBody(message)\n    if err != nil { return err }\n    reqMethod, err := n.reqMethod()\n    if err != nil { return err }\n    req, err := http.NewRequest(reqMethod, ns.reqURL(message), strings.NewReader(reqBody))\n    if err != nil { return err }\n    n.setContentType(req)\n    if err := n.setRequestHeader(req); err != nil { return err }\n    resp, err := client.Do(req)\n    if err != nil { return err }\n    defer func() { _ = resp.Body.Close() }()\n    if resp.StatusCode \u003c 200 || resp.StatusCode \u003e 299 {\n        body, _ := io.ReadAll(resp.Body)   // \u003c-- NO io.LimitReader\n        return fmt.Errorf(\"%d@%s %s\", resp.StatusCode, resp.Status, string(body))\n    } else {\n        _, _ = io.Copy(io.Discard, resp.Body)\n    }\n    return nil\n}\n```\n\nThe full body (no size limit) is concatenated into an error string. That error flows through `commonHandler \u2192 handle() \u2192 newErrorResponse(err) \u2192 c.JSON(http.StatusOK, ...)`. The intranet response body is JSON-encoded back to the `RoleMember` caller.\n\nAdditional wrinkle: `client = utils.HttpClientSkipTlsVerify` when `VerifyTLS` is false \u2014 attacker-controlled. So the SSRF works against TLS endpoints too, ignoring cert validation.\n\n## PoC\n\n### A. Read intranet admin-panel response body\n\n```bash\ncurl -X POST -H \"Authorization: Bearer \u003cmember-jwt\u003e\" \\\n  -H \"Content-Type: application/json\" \\\n  -d \u0027{\"name\":\"x\",\"url\":\"http://192.168.1.1/admin/index.html\",\"request_method\":1,\"request_type\":1,\"verify_tls\":false,\"skip_check\":false}\u0027 \\\n  http://nezha-dashboard.example.com/api/v1/notification\n```\n\nResponse:\n```json\n{\"success\":false,\"error\":\"401@Unauthorized \u003cfull HTML body of the admin login page, no size limit\u003e\"}\n```\n\n### B. AWS IMDSv2 reachability + body leak\n\n```bash\ncurl -X POST -H \"Authorization: Bearer \u003cmember-jwt\u003e\" \\\n  -H \"Content-Type: application/json\" \\\n  -d \u0027{\"name\":\"x\",\"url\":\"http://169.254.169.254/latest/meta-data/iam/security-credentials/\",\"request_method\":1,\"request_type\":1,\"verify_tls\":false,\"skip_check\":false}\u0027 \\\n  http://nezha-dashboard.example.com/api/v1/notification\n```\n\nIMDSv2 returns 401 with a body explaining the missing token; that body is reflected.\n\n### C. DoS via large internal file\n\nBecause the body is read via unbounded `io.ReadAll`, a `RoleMember` pointing at any internal large-file URL (logs, package mirrors, video) blows up dashboard memory.\n\n## Suggested fix\n\n1. **Switch /notification routes to `adminHandler`.** Same fix for `/alert-rule`, `/cron`, `/ddns` if they also issue user-URL requests synchronously. Compare with how `/user` is already guarded.\n\n   ```go\n   auth.POST(\"/notification\", adminHandler(createNotification))\n   auth.PATCH(\"/notification/:id\", adminHandler(updateNotification))\n   ```\n\n2. **SSRF-harden `NotificationServerBundle.Send()`:**\n   - Resolve URL host once via `net.LookupIP`; refuse private/loopback/link-local/CGNAT.\n   - Pin `http.Transport.DialContext` to the resolved IP \u2014 closes DNS-rebinding TOCTOU.\n   - Refuse non-http(s) schemes.\n\n3. **Cap response body**: `io.LimitReader(resp.Body, 4096)`. 4 KB is plenty for surfacing webhook errors.\n\n4. **Reconsider `VerifyTLS=false` toggle on RoleMember-reachable paths** \u2014 if the route remains member-reachable, at minimum cert validation should be enforced.\n\n## Severity\n\n- **CVSS 3.1:** Medium \u2014 `AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:L` \u2248 6.4. PR:L because attacker needs a `RoleMember` account (admin-issued). C:L because intranet response bodies can be read but typically not full credentials. A:L because of the unbounded body-read DoS.\n- **Auth:** authenticated `RoleMember` (Role == 1).\n\n## Reproduction environment\n\n- Tested against: `nezhahq/nezha:v0.x` (commit `50dc8e660326b9f22990898142c58b7a5312b42a`).\n- Code locations:\n  - Handler: `cmd/dashboard/controller/notification.go:46-83, 97-146`\n  - Sink: `model/notification.go:113-159`\n  - Auth gate: `cmd/dashboard/controller/controller.go:121-122` (commonHandler), 214-236 (handler defs)\n\n## Reporter\n\nEddie Ran. Filed via reporter API (PVR enabled). nezha\u0027s `SECURITY.md` mentions email `hi@nai.ba` for vulnerability reports \u2014 happy to also send via email if the maintainer prefers.",
  "id": "GHSA-w4g9-mxgg-j532",
  "modified": "2026-06-26T21:28:27Z",
  "published": "2026-05-23T00:08:04Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/nezhahq/nezha/security/advisories/GHSA-w4g9-mxgg-j532"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-46717"
    },
    {
      "type": "WEB",
      "url": "https://github.com/nezhahq/nezha/commit/d06d539d34c143d842b91e2a64326e8c8f9bc405"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/nezhahq/nezha"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Nezha Monitoring: RoleMember-reachable SSRF with full response-body reflection via POST /api/v1/notification"
}

GHSA-W4HM-RRXG-PXCF

Vulnerability from github – Published: 2026-06-23 15:32 – Updated: 2026-06-25 21:31
VLAI
Details

Flowise before 3.1.0 contains a server-side request forgery vulnerability in the Execute Flow node that allows attackers to bypass security validation by providing intranet addresses through the base URL field. Attackers can initiate HTTP requests to internal network addresses, access cloud metadata, and enumerate internal services by exploiting the missing secureFetch verification in httpSecurity.ts.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-56275"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-06-23T13:16:45Z",
    "severity": "MODERATE"
  },
  "details": "Flowise before 3.1.0 contains a server-side request forgery vulnerability in the Execute Flow node that allows attackers to bypass security validation by providing intranet addresses through the base URL field. Attackers can initiate HTTP requests to internal network addresses, access cloud metadata, and enumerate internal services by exploiting the missing secureFetch verification in httpSecurity.ts.",
  "id": "GHSA-w4hm-rrxg-pxcf",
  "modified": "2026-06-25T21:31:22Z",
  "published": "2026-06-23T15:32:36Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/FlowiseAI/Flowise/security/advisories/GHSA-9hrv-gvrv-6gf2"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-56275"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/flowise-server-side-request-forgery-via-execute-flow-base-url"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:H/AT:N/PR:L/UI:N/VC:H/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-W4MC-HHC6-XP28

Vulnerability from github – Published: 2026-06-19 21:16 – Updated: 2026-06-19 21:16
VLAI
Summary
Mailpit: Incomplete SSRF protection in Link Check API via IPv6 transition mechanisms
Details

Summary

The remediation shipped in mailpit v1.29.2 for GHSA-mpf7-p9x7-96r3 (CVE-2026-27808) is incomplete. The tools.IsInternalIP deny-list relies on Go's stdlib classification helpers (IsLoopback, IsPrivate, IsLinkLocalUnicast, IsLinkLocalMulticast, IsUnspecified, IsMulticast) plus an inline CGNAT range, but those helpers do not match two classes of IPv6 address that should be blocked for SSRF purposes:

  1. IPv6 forms that embed an IPv4 destination via documented translation mechanisms — 6to4, NAT64, IPv4-compatible IPv6, ISATAP, or (in older Go versions) IPv4-mapped IPv6. These let an attacker reach internal IPv4 destinations by supplying an IPv6 literal that encodes the desired IPv4.

  2. IPv6 prefixes that fall outside the narrow private/loopback/link-local ranges Go's stdlib classifies — specifically the deprecated site-local prefix fec0::/10 (RFC 3879/4291) and the documentation prefix 2001:db8::/32 (RFC 3849). The first is still routable on dual-stack hosts and is cited as a bypass form in CVE-2026-44430; the second should never appear in real network traffic and is safe to block as fail-safe behavior.

Together these gaps let the Link Check API be coerced into dialing internal destinations that the v1.29.2 fix was intended to block.

This is the same bug class as GHSA-56c3-vfp2-5qqj / CVE-2026-44430 (MCP Registry) and GHSA-86m8-88fq-xfxp / CVE-2026-45741 (Gotenberg) — projects that, like mailpit, built their SSRF deny-list around Go's stdlib Is* family and discovered the resulting bypass post-disclosure.

The underlying ecosystem-wide issue is tracked upstream at golang/go#79925, which proposes extending net.IP.IsPrivate to handle these IPv6 transition forms. Until that lands, every Go project that wants comprehensive SSRF protection has to implement the decoding itself — which is exactly the gap that produced this advisory and the three CVEs in adjacent projects cited above.

Affected versions

  • mailpit v1.29.2 and later HEAD — the GHSA-mpf7-p9x7-96r3 fix is in place but tools.IsInternalIP does not cover the IPv6 forms enumerated below.
  • Pre-v1.29.2 versions remain vulnerable to the original advisory.

Vulnerable code

internal/tools/net.go L25-L34IsInternalIP:

func IsInternalIP(ip net.IP) bool {
    return ip.IsLoopback() ||
        ip.IsPrivate() ||
        ip.IsLinkLocalUnicast() ||
        ip.IsLinkLocalMulticast() ||
        ip.IsUnspecified() ||
        ip.IsMulticast() ||
        cgnatRange.Contains(ip)
}

internal/linkcheck/status.go L140-L163safeDialContext calls IsInternalIP on resolved IPs before dialing, but only blocks when one of the seven predicates above fires.

For each of the following bypass forms, net.IP.IsLoopback, IsPrivate, IsLinkLocalUnicast, IsLinkLocalMulticast, IsUnspecified, IsMulticast, and the CGNAT range check all return false — so the dial proceeds:

IPv4-embedded-in-IPv6 forms (each carries an IPv4 destination via a documented translation prefix):

Bypass IPv6 literal Decoded IPv4 destination RFC
64:ff9b::a9fe:a9fe 169.254.169.254 (AWS / GCP / Azure metadata) RFC 6052 — NAT64 well-known prefix
64:ff9b:1::a9fe:a9fe 169.254.169.254 RFC 8215 — NAT64 local-use
2002:a9fe:a9fe:: 169.254.169.254 RFC 3056 — 6to4
::a9fe:a9fe 169.254.169.254 RFC 4291 §2.5.5.1 — IPv4-compatible IPv6
64:ff9b::7f00:1 127.0.0.1 RFC 6052 (loopback via NAT64)
2002:0a00:0001:: 10.0.0.1 RFC 3056 (RFC 1918 via 6to4)
<any-prefix>:5efe:<ipv4> <ipv4> (e.g. 2001:db8::5efe:7f00:1127.0.0.1) RFC 5214 — ISATAP

Direct IPv6 prefixes not classified by the stdlib Is* family:

Bypass IPv6 literal What it is RFC
fec0::1 (any address in fec0::/10) Deprecated site-local — still routable on dual-stack hosts RFC 3879 (deprecation) / RFC 4291 §2.5.7
2001:db8::1 (any address in 2001:db8::/32) Documentation prefix — should never appear on the wire RFC 3849

IsInternalIP returns false for every entry in both tables.

The original advisory's stated mitigations do hold against the embedded-IPv4 forms in the narrow case where the IPv6 literal is ::ffff:<ipv4> (IPv4-mapped), because Go's net.IP.To4() normalizes that form and the stdlib Is* methods then check the embedded IPv4. This was the partial fix shipped in Go 1.22.4 / CVE-2024-24790. But it does not extend to 6to4, NAT64, IPv4-compatible, or ISATAP forms — those require explicit decoding that neither Go's stdlib nor IsInternalIP performs. The direct prefixes (fec0::/10, 2001:db8::/32) likewise are simply outside the scope of any Go stdlib Is* method.

Proof of Concept

The repro depends on environment-specific routing for the embedded IPv4 destination. The forms below all pass the safeDialContext check on a stock mailpit v1.29.2 — they will not be blocked by the SSRF deny-list. Whether they connect successfully depends on whether the host's network has NAT64 / 6to4 routing to reach the embedded IPv4.

Unit-test repro (no network dependency)

The most defensible PoC is a unit test against IsInternalIP itself — it demonstrates the deny-list gap directly without depending on the test environment routing the bypass IPs:

// internal/tools/net_ssrf_test.go
package tools

import (
    "net"
    "testing"
)

func TestIsInternalIP_UncoveredIPv6Forms(t *testing.T) {
    cases := map[string]net.IP{
        // IPv4-embedded-in-IPv6 forms.
        "NAT64 well-known wrapping AWS IMDS (RFC 6052)":     net.ParseIP("64:ff9b::a9fe:a9fe"),
        "NAT64 local-use wrapping AWS IMDS (RFC 8215)":      net.ParseIP("64:ff9b:1::a9fe:a9fe"),
        "6to4 wrapping AWS IMDS (RFC 3056)":                 net.ParseIP("2002:a9fe:a9fe::"),
        "IPv4-compatible IPv6 wrapping AWS IMDS (RFC 4291)": net.ParseIP("::a9fe:a9fe"),
        "NAT64 wrapping loopback (RFC 6052)":                net.ParseIP("64:ff9b::7f00:1"),
        "6to4 wrapping RFC 1918 (RFC 3056)":                 net.ParseIP("2002:0a00:0001::"),
        "ISATAP wrapping AWS IMDS (RFC 5214)":               net.ParseIP("2001:db8::5efe:a9fe:a9fe"),

        // Direct IPv6 prefixes outside the stdlib Is* family.
        "Deprecated site-local fec0::/10 (RFC 3879/4291)":   net.ParseIP("fec0::1"),
        "Documentation prefix 2001:db8::/32 (RFC 3849)":     net.ParseIP("2001:db8::1"),
    }

    for name, ip := range cases {
        t.Run(name, func(t *testing.T) {
            if !IsInternalIP(ip) {
                t.Errorf("IsInternalIP(%s) = false — SSRF deny-list bypass", ip)
            }
        })
    }
}

Run with:

go test ./internal/tools/ -run TestIsInternalIP_UncoveredIPv6Forms

On v1.29.2 every subtest fails. Each failure is a documented bypass.

End-to-end repro

In an environment where the embedded IPv4 destination is reachable (e.g. a host whose network provides NAT64 to RFC 1918 / link-local):

  1. Send a crafted email to mailpit's SMTP listener containing an <a href> with a bypass URL: html <a href="http://[64:ff9b::a9fe:a9fe]/latest/meta-data/iam/security-credentials/">link</a>
  2. POST /api/v1/message/{ID}/link-check.
  3. Observe the doHead HTTP HEAD response status — non-zero status (success or specific error) confirms the dial reached the destination rather than being blocked by IsInternalIP.

In environments without NAT64 / 6to4 routing the connection will time out, but the absence of a private/reserved address blocked response confirms the deny-list bypass logically; the unit test above is the canonical PoC.

Impact

Identical scope and severity model to the original GHSA-mpf7-p9x7-96r3:

  • The link-check API is reachable in mailpit's default deploy without authentication (no --ui-auth, no --smtp-auth required).
  • An attacker who can deliver email to the mailpit SMTP listener (often unauthenticated in default config) and invoke the link-check API can probe internal services using any of the uncovered IPv6 forms above — either via the embedded-IPv4 mechanisms to reach IPv4 destinations like cloud metadata endpoints (169.254.169.254, 168.63.129.16), or by addressing a routable IPv6 service via fec0::/10 directly.
  • The status-code-and-error feedback exposed by the link-check API leaks reachability information per probe.
  • Damage ceiling is bounded by the mailpit response shape (status code, status text, 451 Blocked private/reserved address sentinel) — no response body is exposed — but reachability + status-code mapping is sufficient for service discovery and for confirming cloud-metadata service identity.
  • Scope note: tools.IsInternalIP is also used by the screenshot-proxy and HTML-Check-API endpoints (per maintainer disclosure). The same deny-list bypass applies to dialer decisions in those paths, but they include additional checks that mute the impact. The Link Check API remains the most revealing because its response includes the HTTP status code from the dialed destination; the other two are less directly leaky.

Severity: Moderate, mirroring the original advisory (CVSS 5.8).

Suggested remediation

The fix has two parts:

  1. For the IPv4-embedded-in-IPv6 forms: decode the embedded IPv4 and re-check it. This is the same pattern Python's ipaddress.is_private implemented in 3.13, what code.dny.dev/ssrf (IANA Special Purpose Registry-driven, auto-synced) implements out-of-the-box, and the behavior change being proposed for Go's stdlib at golang/go#79925.

  2. For the direct IPv6 prefixes: add them to the first range check alongside cgnatRange.Contains.

Reference implementation (extends the existing helper, keeps the call-site contract identical):

// internal/tools/net.go
package tools

import (
    "encoding/binary"
    "net"
)

var (
    cgnatRange          = mustCIDR("100.64.0.0/10")    // RFC 6598
    deprecatedSiteLocal = mustCIDR("fec0::/10")         // RFC 3879 / 4291
    documentationPrefix = mustCIDR("2001:db8::/32")     // RFC 3849
    nat64WellKnown      = mustCIDR("64:ff9b::/96")      // RFC 6052
    nat64LocalUse       = mustCIDR("64:ff9b:1::/48")    // RFC 8215
    sixToFour           = mustCIDR("2002::/16")         // RFC 3056
    teredo              = mustCIDR("2001::/32")         // RFC 4380
    ipv4Compatible      = mustCIDR("::/96")             // RFC 4291 §2.5.5.1
    ipv4Mapped          = mustCIDR("::ffff:0:0/96")     // RFC 4291 §2.5.5.2
)

func mustCIDR(s string) *net.IPNet {
    _, n, err := net.ParseCIDR(s)
    if err != nil {
        panic(err)
    }
    return n
}

// IsInternalIP reports whether ip should be blocked as a connection target.
// Covers the stdlib Is* checks plus CGNAT, plus IPv6 forms outside the
// stdlib's scope (deprecated site-local, documentation prefix, and the
// IPv6 transition mechanisms whose embedded IPv4 is itself internal).
func IsInternalIP(ip net.IP) bool {
    if ip.IsLoopback() ||
        ip.IsPrivate() ||
        ip.IsLinkLocalUnicast() ||
        ip.IsLinkLocalMulticast() ||
        ip.IsUnspecified() ||
        ip.IsMulticast() ||
        cgnatRange.Contains(ip) ||
        deprecatedSiteLocal.Contains(ip) ||
        documentationPrefix.Contains(ip) {
        return true
    }
    if embedded, ok := embeddedIPv4(ip); ok {
        return IsInternalIP(embedded)
    }
    return false
}

// embeddedIPv4 returns the IPv4 destination encoded in ip, if ip is an
// IPv6 form documented to carry an embedded IPv4 destination.
func embeddedIPv4(ip net.IP) (net.IP, bool) {
    // Skip IPv4 / IPv4-mapped IPv6 — covered by the stdlib Is* checks via To4.
    if ip.To4() != nil {
        return nil, false
    }
    ip16 := ip.To16()
    if ip16 == nil || len(ip16) != net.IPv6len {
        return nil, false
    }
    switch {
    case nat64WellKnown.Contains(ip16), nat64LocalUse.Contains(ip16),
        ipv4Compatible.Contains(ip16):
        // Last 32 bits are the embedded IPv4.
        return net.IPv4(ip16[12], ip16[13], ip16[14], ip16[15]).To4(), true
    case sixToFour.Contains(ip16):
        // Bits 16..47 are the embedded IPv4.
        return net.IPv4(ip16[2], ip16[3], ip16[4], ip16[5]).To4(), true
    case teredo.Contains(ip16):
        // Bits 96..127 are the embedded IPv4 XOR'd with 0xFFFFFFFF.
        x := binary.BigEndian.Uint32(ip16[12:16]) ^ 0xFFFFFFFF
        b := make([]byte, 4)
        binary.BigEndian.PutUint32(b, x)
        return net.IPv4(b[0], b[1], b[2], b[3]).To4(), true
    case ip16[10] == 0x5e && ip16[11] == 0xfe:
        // ISATAP (RFC 5214) — interface identifier ends with :5efe:<ipv4>.
        // Match structurally on bytes 10-11; the /64 prefix is not fixed.
        // Must run after the fixed-prefix cases above (Teredo can legitimately
        // have 5efe in bytes 10-11; its embedding takes precedence).
        return net.IPv4(ip16[12], ip16[13], ip16[14], ip16[15]).To4(), true
    }
    return nil, false
}

This covers every bypass in the two tables above. The direct-prefix additions (deprecatedSiteLocal, documentationPrefix) are two lines in the first if-block; the embedded-IPv4 decoder is the substantive new function.

Alternative — adopt a comprehensive library: Replace the hand-rolled deny-list with code.dny.dev/ssrf, which generates its IPv4 and IPv6 prefix lists from the IANA Special Purpose Registries via a bi-monthly auto-sync. This protects against future RFCs adding new transition forms without requiring further mailpit maintenance.

References

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 1.30.1"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/axllent/mailpit"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.30.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-55187"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-19T21:16:21Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nThe remediation shipped in mailpit v1.29.2 for [GHSA-mpf7-p9x7-96r3](https://github.com/axllent/mailpit/security/advisories/GHSA-mpf7-p9x7-96r3) (CVE-2026-27808) is incomplete. The `tools.IsInternalIP` deny-list relies on Go\u0027s stdlib classification helpers (`IsLoopback`, `IsPrivate`, `IsLinkLocalUnicast`, `IsLinkLocalMulticast`, `IsUnspecified`, `IsMulticast`) plus an inline CGNAT range, but those helpers do **not** match two classes of IPv6 address that should be blocked for SSRF purposes:\n\n1. **IPv6 forms that embed an IPv4 destination via documented translation mechanisms** \u2014 6to4, NAT64, IPv4-compatible IPv6, ISATAP, or (in older Go versions) IPv4-mapped IPv6. These let an attacker reach internal IPv4 destinations by supplying an IPv6 literal that encodes the desired IPv4.\n\n2. **IPv6 prefixes that fall outside the narrow private/loopback/link-local ranges Go\u0027s stdlib classifies** \u2014 specifically the deprecated site-local prefix `fec0::/10` (RFC 3879/4291) and the documentation prefix `2001:db8::/32` (RFC 3849). The first is still routable on dual-stack hosts and is cited as a bypass form in [CVE-2026-44430](https://advisories.gitlab.com/golang/github.com/modelcontextprotocol/registry/CVE-2026-44430/); the second should never appear in real network traffic and is safe to block as fail-safe behavior.\n\nTogether these gaps let the Link Check API be coerced into dialing internal destinations that the v1.29.2 fix was intended to block.\n\nThis is the same bug class as [GHSA-56c3-vfp2-5qqj / CVE-2026-44430 (MCP Registry)](https://advisories.gitlab.com/golang/github.com/modelcontextprotocol/registry/CVE-2026-44430/) and [GHSA-86m8-88fq-xfxp / CVE-2026-45741 (Gotenberg)](https://advisories.gitlab.com/golang/github.com/gotenberg/gotenberg/v8/CVE-2026-45741/) \u2014 projects that, like mailpit, built their SSRF deny-list around Go\u0027s stdlib `Is*` family and discovered the resulting bypass post-disclosure.\n\nThe underlying ecosystem-wide issue is tracked upstream at [**golang/go#79925**](https://github.com/golang/go/issues/79925), which proposes extending `net.IP.IsPrivate` to handle these IPv6 transition forms. Until that lands, every Go project that wants comprehensive SSRF protection has to implement the decoding itself \u2014 which is exactly the gap that produced this advisory and the three CVEs in adjacent projects cited above.\n\n## Affected versions\n\n- mailpit `v1.29.2` and later HEAD \u2014 the GHSA-mpf7-p9x7-96r3 fix is in place but [`tools.IsInternalIP`](https://github.com/axllent/mailpit/blob/a68499fa4e8874d414921fbd520e181dc92a39d7/internal/tools/net.go#L25-L34) does not cover the IPv6 forms enumerated below.\n- Pre-`v1.29.2` versions remain vulnerable to the original advisory.\n\n## Vulnerable code\n\n[`internal/tools/net.go` L25-L34](https://github.com/axllent/mailpit/blob/a68499fa4e8874d414921fbd520e181dc92a39d7/internal/tools/net.go#L25-L34) \u2014 `IsInternalIP`:\n\n```go\nfunc IsInternalIP(ip net.IP) bool {\n    return ip.IsLoopback() ||\n        ip.IsPrivate() ||\n        ip.IsLinkLocalUnicast() ||\n        ip.IsLinkLocalMulticast() ||\n        ip.IsUnspecified() ||\n        ip.IsMulticast() ||\n        cgnatRange.Contains(ip)\n}\n```\n\n[`internal/linkcheck/status.go` L140-L163](https://github.com/axllent/mailpit/blob/a68499fa4e8874d414921fbd520e181dc92a39d7/internal/linkcheck/status.go#L140-L163) \u2014 `safeDialContext` calls `IsInternalIP` on resolved IPs before dialing, but only blocks when one of the seven predicates above fires.\n\nFor each of the following bypass forms, `net.IP.IsLoopback`, `IsPrivate`, `IsLinkLocalUnicast`, `IsLinkLocalMulticast`, `IsUnspecified`, `IsMulticast`, and the CGNAT range check all return `false` \u2014 so the dial proceeds:\n\n**IPv4-embedded-in-IPv6 forms** (each carries an IPv4 destination via a documented translation prefix):\n\n| Bypass IPv6 literal | Decoded IPv4 destination | RFC |\n|---|---|---|\n| `64:ff9b::a9fe:a9fe` | `169.254.169.254` (AWS / GCP / Azure metadata) | RFC 6052 \u2014 NAT64 well-known prefix |\n| `64:ff9b:1::a9fe:a9fe` | `169.254.169.254` | RFC 8215 \u2014 NAT64 local-use |\n| `2002:a9fe:a9fe::` | `169.254.169.254` | RFC 3056 \u2014 6to4 |\n| `::a9fe:a9fe` | `169.254.169.254` | RFC 4291 \u00a72.5.5.1 \u2014 IPv4-compatible IPv6 |\n| `64:ff9b::7f00:1` | `127.0.0.1` | RFC 6052 (loopback via NAT64) |\n| `2002:0a00:0001::` | `10.0.0.1` | RFC 3056 (RFC 1918 via 6to4) |\n| `\u003cany-prefix\u003e:5efe:\u003cipv4\u003e` | `\u003cipv4\u003e` (e.g. `2001:db8::5efe:7f00:1` \u2192 `127.0.0.1`) | RFC 5214 \u2014 ISATAP |\n\n**Direct IPv6 prefixes not classified by the stdlib `Is*` family:**\n\n| Bypass IPv6 literal | What it is | RFC |\n|---|---|---|\n| `fec0::1` (any address in `fec0::/10`) | Deprecated site-local \u2014 still routable on dual-stack hosts | RFC 3879 (deprecation) / RFC 4291 \u00a72.5.7 |\n| `2001:db8::1` (any address in `2001:db8::/32`) | Documentation prefix \u2014 should never appear on the wire | RFC 3849 |\n\n`IsInternalIP` returns `false` for every entry in both tables.\n\nThe original advisory\u0027s stated mitigations *do* hold against the embedded-IPv4 forms in the narrow case where the IPv6 literal is `::ffff:\u003cipv4\u003e` (IPv4-mapped), because Go\u0027s `net.IP.To4()` normalizes that form and the stdlib `Is*` methods then check the embedded IPv4. This was the partial fix shipped in [Go 1.22.4 / CVE-2024-24790](https://pkg.go.dev/vuln/GO-2024-2887). But it does not extend to 6to4, NAT64, IPv4-compatible, or ISATAP forms \u2014 those require explicit decoding that neither Go\u0027s stdlib nor `IsInternalIP` performs. The direct prefixes (`fec0::/10`, `2001:db8::/32`) likewise are simply outside the scope of any Go stdlib `Is*` method.\n\n## Proof of Concept\n\nThe repro depends on environment-specific routing for the embedded IPv4 destination. The forms below all *pass* the `safeDialContext` check on a stock mailpit v1.29.2 \u2014 they will not be blocked by the SSRF deny-list. Whether they connect successfully depends on whether the host\u0027s network has NAT64 / 6to4 routing to reach the embedded IPv4.\n\n### Unit-test repro (no network dependency)\n\nThe most defensible PoC is a unit test against `IsInternalIP` itself \u2014 it demonstrates the deny-list gap directly without depending on the test environment routing the bypass IPs:\n\n```go\n// internal/tools/net_ssrf_test.go\npackage tools\n\nimport (\n    \"net\"\n    \"testing\"\n)\n\nfunc TestIsInternalIP_UncoveredIPv6Forms(t *testing.T) {\n    cases := map[string]net.IP{\n        // IPv4-embedded-in-IPv6 forms.\n        \"NAT64 well-known wrapping AWS IMDS (RFC 6052)\":     net.ParseIP(\"64:ff9b::a9fe:a9fe\"),\n        \"NAT64 local-use wrapping AWS IMDS (RFC 8215)\":      net.ParseIP(\"64:ff9b:1::a9fe:a9fe\"),\n        \"6to4 wrapping AWS IMDS (RFC 3056)\":                 net.ParseIP(\"2002:a9fe:a9fe::\"),\n        \"IPv4-compatible IPv6 wrapping AWS IMDS (RFC 4291)\": net.ParseIP(\"::a9fe:a9fe\"),\n        \"NAT64 wrapping loopback (RFC 6052)\":                net.ParseIP(\"64:ff9b::7f00:1\"),\n        \"6to4 wrapping RFC 1918 (RFC 3056)\":                 net.ParseIP(\"2002:0a00:0001::\"),\n        \"ISATAP wrapping AWS IMDS (RFC 5214)\":               net.ParseIP(\"2001:db8::5efe:a9fe:a9fe\"),\n\n        // Direct IPv6 prefixes outside the stdlib Is* family.\n        \"Deprecated site-local fec0::/10 (RFC 3879/4291)\":   net.ParseIP(\"fec0::1\"),\n        \"Documentation prefix 2001:db8::/32 (RFC 3849)\":     net.ParseIP(\"2001:db8::1\"),\n    }\n\n    for name, ip := range cases {\n        t.Run(name, func(t *testing.T) {\n            if !IsInternalIP(ip) {\n                t.Errorf(\"IsInternalIP(%s) = false \u2014 SSRF deny-list bypass\", ip)\n            }\n        })\n    }\n}\n```\n\nRun with:\n```\ngo test ./internal/tools/ -run TestIsInternalIP_UncoveredIPv6Forms\n```\n\nOn v1.29.2 every subtest fails. Each failure is a documented bypass.\n\n### End-to-end repro\n\nIn an environment where the embedded IPv4 destination is reachable (e.g. a host whose network provides NAT64 to RFC 1918 / link-local):\n\n1. Send a crafted email to mailpit\u0027s SMTP listener containing an `\u003ca href\u003e` with a bypass URL:\n   ```html\n   \u003ca href=\"http://[64:ff9b::a9fe:a9fe]/latest/meta-data/iam/security-credentials/\"\u003elink\u003c/a\u003e\n   ```\n2. `POST /api/v1/message/{ID}/link-check`.\n3. Observe the `doHead` HTTP HEAD response status \u2014 non-zero status (success or specific error) confirms the dial reached the destination rather than being blocked by `IsInternalIP`.\n\nIn environments without NAT64 / 6to4 routing the connection will time out, but the absence of a `private/reserved address` blocked response confirms the deny-list bypass logically; the unit test above is the canonical PoC.\n\n## Impact\n\nIdentical scope and severity model to the original GHSA-mpf7-p9x7-96r3:\n\n- The link-check API is reachable in mailpit\u0027s default deploy without authentication (no `--ui-auth`, no `--smtp-auth` required).\n- An attacker who can deliver email to the mailpit SMTP listener (often unauthenticated in default config) and invoke the link-check API can probe internal services using any of the uncovered IPv6 forms above \u2014 either via the embedded-IPv4 mechanisms to reach IPv4 destinations like cloud metadata endpoints (`169.254.169.254`, `168.63.129.16`), or by addressing a routable IPv6 service via `fec0::/10` directly.\n- The status-code-and-error feedback exposed by the link-check API leaks reachability information per probe.\n- Damage ceiling is bounded by the mailpit response shape (status code, status text, `451 Blocked private/reserved address` sentinel) \u2014 no response body is exposed \u2014 but reachability + status-code mapping is sufficient for service discovery and for confirming cloud-metadata service identity.\n- **Scope note:** `tools.IsInternalIP` is also used by the screenshot-proxy and HTML-Check-API endpoints (per maintainer disclosure). The same deny-list bypass applies to dialer decisions in those paths, but they include additional checks that mute the impact. The Link Check API remains the most revealing because its response includes the HTTP status code from the dialed destination; the other two are less directly leaky.\n\n**Severity:** Moderate, mirroring the original advisory (CVSS 5.8).\n\n## Suggested remediation\n\nThe fix has two parts:\n\n1. **For the IPv4-embedded-in-IPv6 forms:** decode the embedded IPv4 and re-check it. This is the same pattern [Python\u0027s `ipaddress.is_private` implemented in 3.13](https://docs.python.org/3/library/ipaddress.html), what [`code.dny.dev/ssrf`](https://pkg.go.dev/code.dny.dev/ssrf) (IANA Special Purpose Registry-driven, auto-synced) implements out-of-the-box, and the behavior change being proposed for Go\u0027s stdlib at [golang/go#79925](https://github.com/golang/go/issues/79925).\n\n2. **For the direct IPv6 prefixes:** add them to the first range check alongside `cgnatRange.Contains`.\n\nReference implementation (extends the existing helper, keeps the call-site contract identical):\n\n```go\n// internal/tools/net.go\npackage tools\n\nimport (\n    \"encoding/binary\"\n    \"net\"\n)\n\nvar (\n    cgnatRange          = mustCIDR(\"100.64.0.0/10\")    // RFC 6598\n    deprecatedSiteLocal = mustCIDR(\"fec0::/10\")         // RFC 3879 / 4291\n    documentationPrefix = mustCIDR(\"2001:db8::/32\")     // RFC 3849\n    nat64WellKnown      = mustCIDR(\"64:ff9b::/96\")      // RFC 6052\n    nat64LocalUse       = mustCIDR(\"64:ff9b:1::/48\")    // RFC 8215\n    sixToFour           = mustCIDR(\"2002::/16\")         // RFC 3056\n    teredo              = mustCIDR(\"2001::/32\")         // RFC 4380\n    ipv4Compatible      = mustCIDR(\"::/96\")             // RFC 4291 \u00a72.5.5.1\n    ipv4Mapped          = mustCIDR(\"::ffff:0:0/96\")     // RFC 4291 \u00a72.5.5.2\n)\n\nfunc mustCIDR(s string) *net.IPNet {\n    _, n, err := net.ParseCIDR(s)\n    if err != nil {\n        panic(err)\n    }\n    return n\n}\n\n// IsInternalIP reports whether ip should be blocked as a connection target.\n// Covers the stdlib Is* checks plus CGNAT, plus IPv6 forms outside the\n// stdlib\u0027s scope (deprecated site-local, documentation prefix, and the\n// IPv6 transition mechanisms whose embedded IPv4 is itself internal).\nfunc IsInternalIP(ip net.IP) bool {\n    if ip.IsLoopback() ||\n        ip.IsPrivate() ||\n        ip.IsLinkLocalUnicast() ||\n        ip.IsLinkLocalMulticast() ||\n        ip.IsUnspecified() ||\n        ip.IsMulticast() ||\n        cgnatRange.Contains(ip) ||\n        deprecatedSiteLocal.Contains(ip) ||\n        documentationPrefix.Contains(ip) {\n        return true\n    }\n    if embedded, ok := embeddedIPv4(ip); ok {\n        return IsInternalIP(embedded)\n    }\n    return false\n}\n\n// embeddedIPv4 returns the IPv4 destination encoded in ip, if ip is an\n// IPv6 form documented to carry an embedded IPv4 destination.\nfunc embeddedIPv4(ip net.IP) (net.IP, bool) {\n    // Skip IPv4 / IPv4-mapped IPv6 \u2014 covered by the stdlib Is* checks via To4.\n    if ip.To4() != nil {\n        return nil, false\n    }\n    ip16 := ip.To16()\n    if ip16 == nil || len(ip16) != net.IPv6len {\n        return nil, false\n    }\n    switch {\n    case nat64WellKnown.Contains(ip16), nat64LocalUse.Contains(ip16),\n        ipv4Compatible.Contains(ip16):\n        // Last 32 bits are the embedded IPv4.\n        return net.IPv4(ip16[12], ip16[13], ip16[14], ip16[15]).To4(), true\n    case sixToFour.Contains(ip16):\n        // Bits 16..47 are the embedded IPv4.\n        return net.IPv4(ip16[2], ip16[3], ip16[4], ip16[5]).To4(), true\n    case teredo.Contains(ip16):\n        // Bits 96..127 are the embedded IPv4 XOR\u0027d with 0xFFFFFFFF.\n        x := binary.BigEndian.Uint32(ip16[12:16]) ^ 0xFFFFFFFF\n        b := make([]byte, 4)\n        binary.BigEndian.PutUint32(b, x)\n        return net.IPv4(b[0], b[1], b[2], b[3]).To4(), true\n    case ip16[10] == 0x5e \u0026\u0026 ip16[11] == 0xfe:\n        // ISATAP (RFC 5214) \u2014 interface identifier ends with :5efe:\u003cipv4\u003e.\n        // Match structurally on bytes 10-11; the /64 prefix is not fixed.\n        // Must run after the fixed-prefix cases above (Teredo can legitimately\n        // have 5efe in bytes 10-11; its embedding takes precedence).\n        return net.IPv4(ip16[12], ip16[13], ip16[14], ip16[15]).To4(), true\n    }\n    return nil, false\n}\n```\n\nThis covers every bypass in the two tables above. The direct-prefix additions (`deprecatedSiteLocal`, `documentationPrefix`) are two lines in the first if-block; the embedded-IPv4 decoder is the substantive new function.\n\n**Alternative \u2014 adopt a comprehensive library:** Replace the hand-rolled deny-list with [`code.dny.dev/ssrf`](https://pkg.go.dev/code.dny.dev/ssrf), which generates its IPv4 and IPv6 prefix lists from the IANA Special Purpose Registries via a bi-monthly auto-sync. This protects against future RFCs adding new transition forms without requiring further mailpit maintenance.\n\n## References\n\n- Original advisory: [GHSA-mpf7-p9x7-96r3 / CVE-2026-27808](https://github.com/axllent/mailpit/security/advisories/GHSA-mpf7-p9x7-96r3)\n- Vulnerable function: [`internal/tools/net.go#L25-L34` \u2014 `IsInternalIP`](https://github.com/axllent/mailpit/blob/a68499fa4e8874d414921fbd520e181dc92a39d7/internal/tools/net.go#L25-L34)\n- Caller: [`internal/linkcheck/status.go#L140-L163` \u2014 `safeDialContext`](https://github.com/axllent/mailpit/blob/a68499fa4e8874d414921fbd520e181dc92a39d7/internal/linkcheck/status.go#L140-L163)\n- Upstream Go-stdlib issue tracking the root cause: [**golang/go#79925**](https://github.com/golang/go/issues/79925) \u2014 proposal to extend `net.IP.IsPrivate` semantics and improve documentation\n- Related: same bypass class in other Go projects \u2014 [GHSA-56c3-vfp2-5qqj / CVE-2026-44430](https://advisories.gitlab.com/golang/github.com/modelcontextprotocol/registry/CVE-2026-44430/), [GHSA-86m8-88fq-xfxp / CVE-2026-45741](https://advisories.gitlab.com/golang/github.com/gotenberg/gotenberg/v8/CVE-2026-45741/)\n- Go stdlib design context: [Damien Neil\u0027s comment](https://github.com/golang/go/issues/76067#issuecomment-3506705688) (\"`IsPrivate` was a mistake. Just about every use of it that I\u0027ve seen seems to misuse it.\")\n- Python stdlib reference: [`ipaddress.is_private` 3.13 docs](https://docs.python.org/3/library/ipaddress.html) \u2014 covers 6to4, NAT64 explicitly\n- Comprehensive Go library: [`code.dny.dev/ssrf`](https://pkg.go.dev/code.dny.dev/ssrf) \u2014 IANA-registry-driven\n- RFCs: 3056 (6to4), 4380 (Teredo), 6052 (NAT64), 8215 (NAT64 local-use), 4291 (IPv6 addressing including IPv4-mapped/compatible), 5214 (ISATAP), 3879 (site-local deprecation), 3849 (documentation prefix)",
  "id": "GHSA-w4mc-hhc6-xp28",
  "modified": "2026-06-19T21:16:21Z",
  "published": "2026-06-19T21:16:21Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/axllent/mailpit/security/advisories/GHSA-w4mc-hhc6-xp28"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/axllent/mailpit"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Mailpit: Incomplete SSRF protection in Link Check API via IPv6 transition mechanisms"
}

GHSA-W54W-JFQH-PQHC

Vulnerability from github – Published: 2022-06-10 00:00 – Updated: 2022-06-16 00:00
VLAI
Details

MonstaFTP v2.10.3 was discovered to contain a Server-Side Request Forgery (SSRF) via the function performFetchRequest at HTTPFetcher.php.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2022-31827"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2022-06-09T14:15:00Z",
    "severity": "CRITICAL"
  },
  "details": "MonstaFTP v2.10.3 was discovered to contain a Server-Side Request Forgery (SSRF) via the function performFetchRequest at HTTPFetcher.php.",
  "id": "GHSA-w54w-jfqh-pqhc",
  "modified": "2022-06-16T00:00:23Z",
  "published": "2022-06-10T00:00:57Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-31827"
    },
    {
      "type": "WEB",
      "url": "https://github.com/zer0yu/CVE_Request/blob/master/MonstaFTP/MonstaFTP_v2_10_3_SSRF.md"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-W55J-PF39-M7CJ

Vulnerability from github – Published: 2025-01-23 03:30 – Updated: 2025-01-23 03:30
VLAI
Details

BigFix Patch Download Plug-ins are affected by Server-Side Request Forgery (SSRF) vulnerability. It may allow the application to download files from an internally hosted server on localhost.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2024-42182"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-01-23T01:15:26Z",
    "severity": "LOW"
  },
  "details": "BigFix Patch Download Plug-ins are affected by Server-Side Request Forgery (SSRF) vulnerability.  It may allow the application to download files from an internally hosted server on localhost.",
  "id": "GHSA-w55j-pf39-m7cj",
  "modified": "2025-01-23T03:30:53Z",
  "published": "2025-01-23T03:30:53Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-42182"
    },
    {
      "type": "WEB",
      "url": "https://support.hcl-software.com/csm?id=kb_article\u0026sysparm_article=KB0118565"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:C/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-W5H5-P497-87G9

Vulnerability from github – Published: 2023-11-13 03:30 – Updated: 2026-04-28 21:33
VLAI
Details

Server-Side Request Forgery (SSRF) vulnerability in Blubrry PowerPress Podcasting plugin by Blubrry.This issue affects PowerPress Podcasting plugin by Blubrry: from n/a through 11.0.6.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2023-41239"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2023-11-13T03:15:09Z",
    "severity": "MODERATE"
  },
  "details": "Server-Side Request Forgery (SSRF) vulnerability in Blubrry PowerPress Podcasting plugin by Blubrry.This issue affects PowerPress Podcasting plugin by Blubrry: from n/a through 11.0.6.",
  "id": "GHSA-w5h5-p497-87g9",
  "modified": "2026-04-28T21:33:07Z",
  "published": "2023-11-13T03:30:37Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-41239"
    },
    {
      "type": "WEB",
      "url": "https://patchstack.com/database/vulnerability/powerpress/wordpress-powerpress-podcasting-plugin-by-blubrry-plugin-11-0-6-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-W5X4-9GR8-6473

Vulnerability from github – Published: 2024-01-26 21:30 – Updated: 2024-01-26 21:30
VLAI
Details

A vulnerability classified as critical was found in 60IndexPage up to 1.8.5. This vulnerability affects unknown code of the file /apply/index.php of the component Parameter Handler. The manipulation of the argument url leads to server-side request forgery. The attack can be initiated remotely. The exploit has been disclosed to the public and may be used. VDB-252190 is the identifier assigned to this vulnerability. NOTE: The vendor was contacted early about this disclosure but did not respond in any way.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2024-0946"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2024-01-26T21:15:08Z",
    "severity": "HIGH"
  },
  "details": "A vulnerability classified as critical was found in 60IndexPage up to 1.8.5. This vulnerability affects unknown code of the file /apply/index.php of the component Parameter Handler. The manipulation of the argument url leads to server-side request forgery. The attack can be initiated remotely. The exploit has been disclosed to the public and may be used. VDB-252190 is the identifier assigned to this vulnerability. NOTE: The vendor was contacted early about this disclosure but did not respond in any way.",
  "id": "GHSA-w5x4-9gr8-6473",
  "modified": "2024-01-26T21:30:22Z",
  "published": "2024-01-26T21:30:22Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-0946"
    },
    {
      "type": "WEB",
      "url": "https://note.zhaoj.in/share/iNSyaClT0hGi"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/?ctiid.252190"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/?id.252190"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-W62C-5624-MCPG

Vulnerability from github – Published: 2022-05-14 02:59 – Updated: 2022-05-14 02:59
VLAI
Details

Adobe Experience Manager versions 6.4 and earlier have a Server-Side Request Forgery vulnerability. Successful exploitation could lead to sensitive information disclosure.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2018-5006"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2018-07-20T19:29:00Z",
    "severity": "HIGH"
  },
  "details": "Adobe Experience Manager versions 6.4 and earlier have a Server-Side Request Forgery vulnerability. Successful exploitation could lead to sensitive information disclosure.",
  "id": "GHSA-w62c-5624-mcpg",
  "modified": "2022-05-14T02:59:17Z",
  "published": "2022-05-14T02:59:17Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2018-5006"
    },
    {
      "type": "WEB",
      "url": "https://helpx.adobe.com/security/products/experience-manager/apsb18-23.html"
    },
    {
      "type": "WEB",
      "url": "http://www.securityfocus.com/bid/104702"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

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.