GitHub Advisoryadvisory · npm
GHSA-q8hw-4fvp-9rwv Nuxt OG Image has unauthenticated SSRF via `fonts[].path` URL parameter
MEDIUM
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
References
- https://github.com/nuxt-modules/og-image/security/advisories/GHSA-q8hw-4fvp-9rwv web
- https://github.com/nuxt-modules/og-image/pull/637 web
- https://github.com/nuxt-modules/og-image/commit/243cac2228671d3711c2bd65e300c278fcdf5a4e web
- https://github.com/nuxt-modules/og-image package
- https://github.com/nuxt-modules/og-image/releases/tag/v6.7.0 web