GitHub Advisoryadvisory · npm
GHSA-c2rm-g55x-8hr5 nuxt-og-image SSRF — bypass of GHSA-pqhr-mp3f-hrpp / v6.2.5 fix (IPv6 + redirect)
LOW
Official page ↗
- Aliases
- CVE-2026-44589
- Ecosystem
- npm
- Packages
- nuxt-og-image
- Severity
- Low
- CVSS vector
- CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N
- Weaknesses
- CWE-918
- Reviewed
- 2026-05-07
Affected packages
• npm nuxt-og-image: >= 6.2.5, < 6.4.9 → fixed in 6.4.9
Description
Summary
The `isBlockedUrl()` denylist introduced in `[email protected]` to remediate GHSA-pqhr-mp3f-hrpp (Dmitry Prokhorov / Positive Technologies, March 2026) is incomplete. The patch advisory states "Decimal/hexadecimal IP encoding bypasses are also handled" — that part is true (Node's WHATWG URL parser canonicalizes those forms before validation), but the v6.2.5 implementation misses two independent surfaces in the latest release `6.4.8`:
1. IPv6 prefix list is incomplete. The IPv6 branch checks only `bare === "::1" || startsWith("fc") || startsWith("fd") || startsWith("fe80")`. It misses:
• `[::ffff:7f00:1]` — IPv6-mapped IPv4 loopback in pure-hex form (RE_MAPPED_V4 regex requires dotted-quad). Reaches 127.0.0.1 on a single-stack-IPv4 host with no other primitive needed.
• `[fec0::/10]` (RFC 3879 site-local — deprecated but still routable on legacy networks)
• `[5f00::/16]` (RFC 9602 SRv6 SIDs)
• `[3fff::/20]` (RFC 9637 IPv6 documentation v2)
• `[64:ff9b:1::/48]` (RFC 8215 NAT64 local-use, including embedded IPv4 loopback `[64:ff9b:1::7f00:1]`)
2. No redirect re-validation. `isBlockedUrl` runs once on the initial ``. The subsequent `$fetch(decodedSrc, ...)` (ofetch, default redirect-follow) follows 30x responses with no second-pass validation. Any allowed origin that returns a 302 to an internal IP — S3 redirect rules, GCS, Azure, CloudFront, any user-content CDN where the attacker can place a single redirect — completes the SSRF.
The net result is that the v6.2.5 SSRF advisory is bypassable in two distinct ways. The same root family as #29 / #38 (ipx) but in a different code path with different gaps — `nuxt-og-image` does not delegate to `ipx`, it ships its own validator, and that validator has fresh issues that survived the prior fix.
Affected
| Package | Version | Role |
|------------------|-------------------|-----------------------------------------------------|
| `nuxt-og-image` | `6.4.8` (latest) | default OG-image generator for Nuxt apps |
| `@nuxtjs/og-image` (alias) | same | re-export, same code path |
The vulnerable code lives in `dist/runtime/server/og-image/core/plugins/imageSrc.js` and is enforced for every `` (and `style="background-image: url(...)"`) inside an OG image component, on production builds (`!import.meta.dev`).
Vulnerable code (`imageSrc.js`, verbatim)
```js
function isPrivateIPv4(a, b) {
if (a === 127) return true;
if (a === 10) return true;
if (a === 172 && b >= 16 && b RE_DIGIT_ONLY.test(p))) {
/* dotted-decimal IPv4 path */
}
if (RE_INT_IP.test(ip)) {
/* single-integer IPv4 path */
}
if (bare === "::1" || bare.startsWith("fc") || bare.startsWith("fd") || bare.startsWith("fe80"))
return true; // ← gap: only 4 IPv6 prefixes
return false; // ← everything else is "public"
}
// Then:
async function doResolveSrcToBuffer(src, kind, ctx) {
...
if (!import.meta.dev && isBlockedUrl(decodedSrc)) {
return { blocked: true };
}
const buffer = await $fetch(decodedSrc, { // ← follows 30x by default
responseType: "arrayBuffer",
timeout: fetchTimeout,
});
...
}
```
Two distinct issues:
• The IPv6 prefix list is hand-rolled (`fc`, `fd`, `fe80`, `::1`) and inherits no taxonomy from `ipaddr.js` or any RFC table.
• `$fetch` is `ofetch`, which wraps Node `fetch()` with default `redirect: "follow"`. The validator does not run on the redirect target.
Reproducer (verbatim, no host privilege)
End-to-end test of `isBlockedUrl` on a corpus of internal-IP forms, paired with empirical `fetch()` confirming which forms actually reach loopback. Verbatim output:
```
isBlockedUrl? fetch reaches loopback? url
------------- ----------------------- ---
✓ blocked YES http://127.0.0.1:8765/ (control: dotted-decimal loopback)
✓ blocked YES http://localhost:8765/ (control)
✓ blocked no(ECONNREFUSED) http://[::1]:8765/ (control: IPv6 loopback)
✓ blocked no(EHOSTUNREACH) http://169.254.169.254:8765/ (control: AWS IMDS)
✓ blocked YES http://2130706433:8765/ (control: decimal-int IPv4)
✓ blocked YES http://0x7f000001:8765/ (control: hex-int IPv4)
✓ blocked YES http://0177.0.0.1:8765/ (control: octal — URL parser canonicalizes)
✓ blocked YES http://127.1:8765/ (control: shorthand — URL parser canonicalizes)
✗ NOT blocked YES http://[::ffff:7f00:1]:8765/ (BYPASS: IPv6-mapped, hex form)
✗ NOT blocked no(unreachable) http://[fec0::1]:8765/ (BYPASS: RFC 3879 site-local)
✗ NOT blocked no(unreachable) http://[5f00::1]:8765/ (BYPASS: RFC 9602 SRv6)
✗ NOT blocked no(unreachable) http://[3fff::1]:8765/ (BYPASS: RFC 9637 docs)
✗ NOT blocked no(unreachable) http://[64:ff9b:1::1]:8765/ (BYPASS: RFC 8215 NAT64)
✗ NOT blocked no(unreachable) http://[64:ff9b:1::7f00:1]:8765/ (BYPASS: NAT64 + embedded loopback)
```
The first six bypass rows say "✗ NOT blocked" — that is `isBlockedUrl` returning `false` (i.e., "this URL is fine to fetch") for each of those addresses. The "fetch reaches loopback" column shows that `[::ffff:7f00:1]` actually round-trips to 127.0.0.1 on a single-stack-IPv4 dev box; the four cluster ranges are unreachable on the dev box but succeed on dual-stack / k8s / NAT64 / SRv6 networks where any of these prefixes is internally bound.
The "control" rows confirm the bypass set is minimal — the validator catches the obvious cases. The bypasses are the cases the prefix list forgot.
Class 2: redirect amplifier
`$fetch(url, { responseType: "arrayBuffer", timeout })` follows 30x by default. Confirmed empirically — `ofetch('http://lab.menna.website/test/redirect-to-loopback')` (where `lab.menna.website` returns `302 Location: http://127.0.0.1/`) ends with ` fetch failed` after the connect attempt to `127.0.0.1:80`, proving the redirect was followed. On a target where the redirect destination has a service bound, the bytes round-trip back through the OG renderer.
Same primitive as #29 / #38 (ipx redirect bypass), in a different validator. The fix recommendations for #29 also apply here, with the same trade-offs.
Impact
A Nuxt application that uses `nuxt-og-image` (the official-recommended OG generator) and includes any user-influenced URL in an OG component is vulnerable to SSRF that returns the bytes of the internal response as part of the rendered OG image:
• Class 1 directly: `` reaches 127.0.0.1 on the OG worker. If the dev's deployment has anything bound to loopback (admin dashboards, internal HTTP-RPC, Redis HTTP UI, anything running alongside the function on the same machine in self-hosted setups), it leaks.
• Class 1 cluster: the IPv6 cluster ranges tr…
Related
Alias · CVE (1)
- CVE-2026-44589 Nuxt OG Image generates OG Images with Vue templates in Nuxt. vulnerability
Weakness · CWE (1)
- CWE-918 Server-Side Request Forgery weakness