GitHub Advisoryadvisory · npm

GHSA-q8hw-4fvp-9rwv Nuxt OG Image has unauthenticated SSRF via `fonts[].path` URL parameter

MEDIUM Published 2026-09-17 Modified 2026-09-17 Official page ↗
Aliases
CVE-2026-61793
Ecosystem
npm
Packages
nuxt-og-image
Severity
Moderate
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:L/SI:N/SA:N
Weaknesses
CWE-1188, CWE-20, CWE-441, CWE-749, CWE-918
Reviewed
2026-09-17

Affected packages

• npm nuxt-og-image: >= 6.0.2, < 6.7.0 → fixed in 6.7.0

Description

Summary `nuxt-og-image` exposes an unauthenticated HTTP endpoint at `/_og/d/` that base64url-decodes and `JSON.parse`s a `fonts` URL segment, then passes each `fonts[i].path` value directly into `fetch()` server-side without any URL validation (no scheme allowlist, no loopback/RFC1918 block, no host allowlist, no DNS rebinding mitigation). Under the module's documented default configuration (`security.strict = false`, `security.secret = ""`, `restrictRuntimeImagesToOrigin = false`), any caller able to reach the deployed Nuxt site can force the Nuxt server to issue arbitrary outbound `GET` requests to any host reachable from the server - including loopback, RFC1918 LAN, and cloud metadata services (AWS IMDS, GCE/Azure metadata, Kubernetes `kubelet`, internal admin panels, Redis/etcd/Consul/Vault HTTP APIs). The chain is blind (Satori consumes the response as font bytes and silently discards non-fonts) but a robust side-channel exists: the outer HTTP status is `500` when the SSRF target returns `2xx`, and `200` when it fails or returns non-`2xx`. This is sufficient to (a) enumerate live internal services and open ports, (b) confirm IMDSv1 reachability, and (c) detect credential issuance on environments still allowing IMDSv1. Demonstrated end-to-end on a stock `npm create nuxt@latest` install with the module's documented default usage. Detail Endpoint registration (unauthenticated) The module registers `/_og/d/` and `/_og/s/` with no authentication / Origin check / Sec‑Fetch‑Site validation: ```js // dist/shared/nuxt-og-image.DdbTs-xp.mjs : 5113-5133 addServerHandler({ route: "/_og/d/", handler: resolve("./runtime/server/routes/image") }) addServerHandler({ route: "/_og/s/", handler: resolve("./runtime/server/routes/image") }) ``` Default security config (permissive) ```js // dist/shared/nuxt-og-image.DdbTs-xp.mjs : 5618-5640 security: { strict: config.security?.strict ?? false, // entire block skipped. } ``` Attacker-controlled deserialization of `fonts` `fonts` is enumerated as a complex parameter: its value is base64url-decoded and then `JSON.parse`d straight into `options`: ```js // dist/runtime/shared/urlEncoding.js : 65 const COMPLEX_PARAMS = new Set(["satori","resvg","sharp","screenshot","takumi","fonts","_query","_path"]) // dist/runtime/shared/urlEncoding.js : 184-231 export function decodeOgImageParams(encoded) { ... for (const part of parts) { const idx = part.search(RE_SINGLE_UNDERSCORE) if (idx === -1) continue const alias = part.slice(0, idx) let value = part.slice(idx + 1) const paramName = PARAM_ALIASES[alias] || alias if (COMPLEX_PARAMS.has(paramName)) { try { const json = b64Decode(value) options[paramName] = JSON.parse(json) // options.fonts = [{ name: "X", path: "", ... }] ``` From `options.fonts` to the unfettered `fetch()` ```js // dist/runtime/server/og-image/satori/renderer.js : 36-42 const fonts = await loadFontsForRenderer(event, { ...options, fontDefs: options.fonts, // null) ... } } ``` The production binding (selected for every non-dev / non-prerender preset - `dist/shared/nuxt-og-image.DdbTs-xp.mjs:5445-5452`): ```js // dist/runtime/server/og-image/bindings/font-assets/node.js : 6-21 null) // -> fetch(attacker-URL) ... } ``` `ufo.withBase("http://target/", "/")` returns `"http://target/"` unchanged when the input is already an absolute URL; `new URL(abs, origin)` then yields the absolute URL. No `URL.protocol` check, no IP-literal block, no DNS-resolution-aware allowlist, no redirect cap. Side-channel for blind exfiltration Although the response body is consumed as font bytes and Satori discards non-font payloads, the outer HTTP status code differs deterministically** based on the SSRF target's response: | Target returns | Satori behavior | Outer response | |----------------|-----------------|----------------| | `2xx` with non-font body | `parseFont(bytes)` throws | `HTTP 500` | | Connection refused / timeout / non-`2xx` | `fetch().catch(() => null)` -> fallback fonts used | `HTTP 200` (a PNG is returned) | The boolean oracle (target alive & answered 2xx vs. not) is sufficient to: • enumerate open ports on `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16` • detect cloud metadata reachability (and on legacy AWS IMDSv1, trigger credential issuance - even without read-back, the *act* of issuing credentials creates audit-trail and timing observables) • distinguish health-check responses, vault-init status, k8s `kubelet` `/pods` reachability, etc. Steps To Reproduce ```bash 1. Create a stock Nuxt 4 app and add the module npm create nuxt@latest lab-test --yes # accept defaults cd lab-test npm install nuxt-og-image # -> installs v6.6.0 (current latest) ``` `nuxt.config.ts` - the only change is enabling the module: ```ts export default defineNuxtConfig({ compatibilityDate: '2025-07-15', modules: ['nuxt-og-image'], // NO `ogImage.security` overrides - accept module defaults. }) ``` The module requires at least one OG image component to be registered (its documented Hello‑World; otherwise the endpoint returns `500 No OG Image components found`). Add the minimal one: ```bash mkdir -p app/components/OgImage cat > app/components/OgImage/Default.satori.vue defineProps<{ title?: …

Related

Alias · CVE (1)

  • CVE-2026-61793 Nuxt OG Image generates OG Images with Vue templates in Nuxt. vulnerability

Weakness · CWE (5)

  • CWE-1188 Initialization of a Resource with an Insecure Default weakness
  • CWE-20 Improper Input Validation weakness
  • CWE-441 Unintended Proxy or Intermediary weakness
  • CWE-749 Exposed Dangerous Method or Function weakness
  • CWE-918 Server-Side Request Forgery weakness

References