chore(deps): update dependency h3-next to v2.0.1-rc.17 [security]#1626
Open
renovate[bot] wants to merge 1 commit intomainfrom
Open
chore(deps): update dependency h3-next to v2.0.1-rc.17 [security]#1626renovate[bot] wants to merge 1 commit intomainfrom
renovate[bot] wants to merge 1 commit intomainfrom
Conversation
commit: |
OrbisK
reviewed
Mar 19, 2026
package.json
Outdated
| "get-port-please": "^3.2.0", | ||
| "h3": "^1.15.5", | ||
| "h3-next": "npm:h3@2.0.1-rc.14", | ||
| "h3-next": "npm:h3@2.0.1-rc.15", |
Member
There was a problem hiding this comment.
Suggested change
| "h3-next": "npm:h3@2.0.1-rc.15", | |
| "h3-next": "npm:h3@^2.0.1-rc.15", |
is it possible to have ^ so that it is not pinned?
cc @danielroe
da3bcb3 to
5745032
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
2.0.1-rc.14→2.0.1-rc.17GitHub Vulnerability Alerts
CVE-2026-33128
Summary
createEventStreamin h3 is vulnerable to Server-Sent Events (SSE) injection due to missing newline sanitization informatEventStreamMessage()andformatEventStreamComment(). 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: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:event:,data:,id:, orretry:directives\n\nto terminate the current event and start a new oneretry: 1to force aggressive reconnection (DoS)id:to manipulate which events are replayed on reconnectionInjection via the
eventfieldThe browser's
EventSourceAPI parses these as two separate events: onemessageevent and oneadminevent.Injection via the
datafieldBefore exploit:

PoC
Vulnerable server (
sse-server.ts)A realistic chat/notification server that broadcasts user input via SSE:
Exploit
Raw wire format proving injection
The browser's
EventSourcefires this as anadminevent with dataALL_USERS_COMPROMISED— entirely controlled by the attacker.Proof:
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:
admin,system)retry: 1to force all clients to reconnect every 1msThis 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.
GHSA-wr4h-v87w-p3r7
Summary
serveStatic()in h3 is vulnerable to path traversal via percent-encoded dot segments (%2e%2e), allowing an unauthenticated attacker to read arbitrary files outside the intended static directory on Node.js deployments.Details
The vulnerability exists in
src/utils/static.tsat line 86:On Node.js, h3 uses srvx's
FastURLclass to parse request URLs. Unlike the standard WHATWGURLparser,FastURLextracts the pathname via raw string slicing for performance — it does not normalize dot segments (./..) or resolve percent-encoded equivalents (%2e).This means a request to
/%2e%2e/will haveevent.url.pathnamereturn/%2e%2e/verbatim, whereas the standardURLparser would normalize it to/(resolving..upward).The
serveStatic()function then callsdecodeURI()on this raw pathname, which decodes%2eto., producing/../. The resulting path containing../traversal sequences is passed directly to the user-providedgetMeta()andgetContents()callbacks with no sanitization or traversal validation.When these callbacks perform filesystem operations (the intended and documented usage), the
../sequences resolve against the filesystem, escaping the static root directory.Before exploit:
Vulnerability chain
PoC
Vulnerable server (
server.ts)Exploit
Result
Proof:
Pwned by 0xkakashi
Impact
An unauthenticated remote attacker can read arbitrary files from the server's filesystem by sending a crafted HTTP request with
%2e%2e(percent-encoded..) path segments to any endpoint served byserveStatic().This affects any h3 v2.x application using
serveStatic()running on Node.js (where theFastURLfast path is used). Applications running on runtimes that provide a pre-parsedURLobject (e.g., Cloudflare Workers, Deno) may not be affected, asFastURL's raw string slicing is bypassed.Exploitable files include but are not limited to:
/etc/passwd,/etc/shadow(if readable).envfiles containing secrets, API keys, database credentialsCVE-2026-33131
H3 NodeRequestUrl bugs
Vulnerable pieces of code :
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.urlorevent.url.hostnameorevent.url._urlIt will lead to trigger one specials method
The
NodeRequestUrlis extends fromFastURLso when we just access.urlor trying to dump all data of this class . This function will be triggered !!And as debugging , the
this.#urlis null and will reach to this code :Where is the
this.hrefcomes from ?Because the
this.#urlis still null sothis.#hrefis built up by :Yeah and this is untrusted data go . An attacker can pollute the
Hostheader from requests lead overwrite theevent.url.Middleware bypass
What can be done with overwriting the
event.url?Audit the code we can easily realize that the
routeHanlderis found before running any middlewaresSo the handleRoute is fixed but when checking with middleware it check with the spoofed one lead to MIDDLEWARE BYPASS
We have this poc :
This is really dangerous if some one just try to dump all the
event.urlor something that trigger_url()from class FastURL and need a fix immediately.CVE-2026-33490
Summary
The
mount()method in h3 uses a simplestartsWith()check to determine whether incoming requests fall under a mounted sub-application's path prefix. Because this check does not verify a path segment boundary (i.e., that the next character after the base is/or end-of-string), middleware registered on a mount like/adminwill also execute for unrelated routes such as/admin-public,/administrator, or/adminstuff. This allows an attacker to trigger context-setting middleware on paths it was never intended to cover, potentially polluting request context with unintended privilege flags.Details
The root cause is in
src/h3.ts:127within themount()method:When a sub-app is mounted at
/admin, the checkoriginalPathname.startsWith("/admin")returnstruefor/admin,/admin/,/admin/dashboard, but also for/admin-public,/administrator,/adminFoo, etc. The mounted sub-app's entire middleware chain then executes for these unrelated paths.A secondary instance of the same flaw exists in
src/utils/internal/path.ts:40:The
withoutBase()utility will incorrectly strip the base from paths that merely share a string prefix, returning mangled paths (e.g.,withoutBase("/admin-public/info", "/admin")returns/-public/info).Exploitation flow:
/adminwith middleware that setsevent.context.isAdmin = true/admin-public/infoon the parent app that readsevent.context.isAdminGET /admin-public/info/adminmount'sstartsWithcheck passes → admin middleware executes → setsisAdmin = true/admin-public/infohandler seesevent.context.isAdmin === truePoC
Steps to reproduce:
Impact
isAdmin,isAuthenticated, role assignments) on requests to completely unrelated routes.withoutBase()utility produces incorrect paths (e.g.,/-public/infoinstead of/admin-public/info) when the input shares only a string prefix, potentially causing routing errors or further security issues in downstream path processing.mount()with a base path that is a string prefix of other routes is affected. The impact scales with how the application uses middleware-set context values.Recommended Fix
Add a segment boundary check after the
startsWithcall in both locations. The character immediately following the base prefix must be/,?,#, or the string must end exactly at the base:Fix for
src/h3.ts:127:mount(base: string, input: FetchHandler | FetchableObject | H3Type) { if ("handler" in input) { if (input["~middleware"].length > 0) { this["~middleware"].push((event, next) => { const originalPathname = event.url.pathname; - if (!originalPathname.startsWith(base)) { + if (!originalPathname.startsWith(base) || + (originalPathname.length > base.length && originalPathname[base.length] !== "/")) { return next(); }Fix for
src/utils/internal/path.ts:40:export function withoutBase(input: string = "", base: string = ""): string { if (!base || base === "/") { return input; } const _base = withoutTrailingSlash(base); - if (!input.startsWith(_base)) { + if (!input.startsWith(_base) || + (input.length > _base.length && input[_base.length] !== "/")) { return input; }This ensures that
/adminonly matches/admin,/admin/, and/admin/...— never/admin-public,/administrator, or other coincidental string-prefix matches.GHSA-4hxc-9384-m385
Summary
The
EventStreamclass in h3 fails to sanitize carriage return (\r) characters indataandcommentfields. Per the SSE specification,\ris a valid line terminator, so browsers interpret injected\ras line breaks. This allows an attacker to inject arbitrary SSE events, spoof event types, and split a singlepush()call into multiple distinct browser-parsed events. This is an incomplete fix bypass of commit7791538which addressed\ninjection but missed\r-only injection.Details
The prior fix in commit
7791538added_sanitizeSingleLine()to strip\nand\rfromidandeventfields, and changeddataformatting to split on\n. However, two code paths remain vulnerable:1.
datafield —formatEventStreamMessage()(src/utils/internal/event-stream.ts:190-193)String.prototype.split("\n")does not split on\r. A string like"legit\revent: evil"remains as a single "line" and is emitted as:Per the SSE specification §9.2.6,
\ralone is a valid line terminator. The browser parses this as two separate lines:2.
commentfield —formatEventStreamComment()(src/utils/internal/event-stream.ts:170-177)The same
split("\n")pattern means\rin comments is not handled. An input like"x\rdata: injected"produces:Which the browser parses as a comment line followed by actual data:
Why
_sanitizeSingleLinedoesn't helpThe
_sanitizeSingleLinefunction at line 198 correctly strips both\rand\n:But it is only applied to
idandeventfields (lines 182, 185), not todataorcomment.PoC
Setup
Create a minimal h3 application that reflects user input into an SSE stream:
Attack 1: Event type injection via
\rin dataExpected (safe) wire output:
Browser parses as:
The browser's
EventSourcefires a customevilevent instead of the defaultmessageevent, potentially routing data to unintended handlers.Attack 2: Message boundary injection (event splitting)
Browser parses as two separate events:
data: firstdata: injectedA single
push()call produces two distinct events in the browser — the attacker controls the second event's content entirely.Attack 3: Comment escape to data injection
Browser parses as:
Impact
event:types, causing browsers to dispatch events to differentEventSource.addEventListener()handlers than intended. In applications that use custom event types for control flow (e.g.,error,done,system), this enables UI manipulation.push()call can be split into multiple browser-side events. This breaks application-level framing assumptions — e.g., a chat message could appear as two messages, or an injected "system" message could appear in an AI chat interface.pushComment().7791538) explicitly intended to prevent SSE injection, demonstrating the project considers this a security issue. The incomplete fix creates a false sense of security.Recommended Fix
Both
formatEventStreamMessageandformatEventStreamCommentshould split on\r,\n, and\r\n— matching the SSE spec's line terminator definition.This ensures all three SSE-spec line terminators (
\r\n,\r,\n) are properly handled as line boundaries, preventing\rfrom being passed through to the browser where it would be interpreted as a line break.GHSA-72gr-qfp7-vwhw
Summary
The
serveStaticutility in h3 applies a redundantdecodeURI()call to the request pathname afterH3Eventhas already performed percent-decoding with%25preservation. This double decoding converts%252e%252einto%2e%2e, which bypassesresolveDotSegments()(since it checks for literal.characters, not percent-encoded equivalents). When the resulting asset ID is resolved by URL-based backends (CDN, S3, object storage),%2e%2eis interpreted as..per the URL Standard, enabling path traversal to read arbitrary files from the backend.Details
The vulnerability is a conflict between two decoding stages:
Stage 1 —
H3Eventconstructor (src/event.ts:65-69):This correctly preserves
%25sequences by escaping them before decoding. A request for/%252e%252e/etc/passwdproducesevent.url.pathname=/%2e%2e/etc/passwd— the%25was preserved so%252ebecame%2e(not.).Stage 2 —
serveStatic(src/utils/static.ts:86-88):This applies a second
decodeURI(), which decodes%2e→., producing/../../../etc/passwd. However, the decoding happens inside theresolveDotSegments()call argument —decodeURIruns first, thenresolveDotSegmentsprocesses the result.Wait — re-examining the flow more carefully:
/%2e%2e/%2e%2e/etc/passwddecodeURI()in static.ts converts%2e→., producing:/../../../etc/passwdresolveDotSegments("/../../../etc/passwd")does resolve..segments, clamping to/etc/passwdThe actual bypass is subtler.
decodeURI()does not decode%2e— it only decodes characters thatencodeURIwould encode. Since.is never encoded byencodeURI,%2eis not decoded bydecodeURI(). So the chain is:/%252e%252e/%252e%252e/etc/passwd/%2e%2e/%2e%2e/etc/passwddecodeURI()in static.ts:/%2e%2e/%2e%2e/etc/passwd(unchanged —decodeURIdoesn't decode%2e)resolveDotSegments()fast-returns at line 56 because%2econtains no literal.character:/%2e%2e/%2e%2e/etc/passwdis passed togetMeta()andgetContents()callbacks%2e%2eas..per RFC 3986 / URL StandardThe root cause is
resolveDotSegments()only checks for literal.characters and does not account for percent-encoded dot sequences (%2e). ThedecodeURI()in static.ts is redundant (event.ts already decodes) but is not the direct cause — the real gap is that%2e%2esurvives as a traversal payload through both decoding stages andresolveDotSegments.PoC
1. Create a minimal h3 server with a URL-based static backend:
2. Send the double-encoded traversal request:
curl -v 'http://localhost:3000/%252e%252e/%252e%252e/etc/passwd'3. Observe server logs:
The
%2e%2esequences in the asset ID are resolved as..by theURLconstructor, causing the backend URL to traverse from/static/to/etc/passwd.Impact
serveStaticwith callbacks that resolve asset IDs via URL construction (new URL(id, baseUrl)or equivalent). This is a common pattern for CDN proxying and cloud object storage backends. Filesystem-based backends usingpath.join()are not affected since%2e%2eis not resolved as a traversal sequence by filesystem APIs.Recommended Fix
The
resolveDotSegments()function must account for percent-encoded dot sequences. Additionally, the redundantdecodeURI()inserveStaticshould be removed sinceH3Eventalready handles decoding.Fix 1 — Remove redundant
decodeURIinsrc/utils/static.ts:86-88:Fix 2 — Harden
resolveDotSegmentsinsrc/utils/internal/path.ts:55-73to handle percent-encoded dots:export function resolveDotSegments(path: string): string { - if (!path.includes(".")) { + if (!path.includes(".") && !path.toLowerCase().includes("%2e")) { return path; } // Normalize backslashes to forward slashes to prevent traversal via `\` - const segments = path.replaceAll("\\", "/").split("/"); + const segments = path.replaceAll("\\", "/") + .replaceAll(/%2e/gi, ".") + .split("/"); const resolved: string[] = [];Both fixes should be applied. Fix 1 removes the unnecessary double-decode. Fix 2 provides defense-in-depth by ensuring
resolveDotSegmentscannot be bypassed with percent-encoded dots regardless of the caller.Release Notes
h3js/h3 (h3-next)
v2.0.1-rc.17Compare Source
compare changes
🚀 Enhancements
removeRoute(#1331)🩹 Fixes
Allowheader in 405 response (#1314)requestWithBaseURL(0295f90)startsWithcheck (7ccc9e2)📖 Documentation
🏡 Chore
✅ Tests
❤️ Contributors
v2.0.1-rc.16Compare Source
compare changes
🏡 Chore
❤️ Contributors
v2.0.1-rc.15Compare Source
compare changes
🚀 Enhancements
defineJsonRpcHandleranddefineJsonRpcWebSocketHandler(#1180)🔥 Performance
🩹 Fixes
📖 Documentation
unjwtcommunity library entry (#1309)📦 Build
h3 docs(#1311)🏡 Chore
ESNextto tsconfig'slib(#1297)❤️ Contributors
Configuration
📅 Schedule: Branch creation - "" (UTC), Automerge - At any time (no schedule defined).
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.