Update dependency h3 to v2.0.1-rc.17 [SECURITY]#921
Open
renovate[bot] wants to merge 1 commit intomainfrom
Open
Update dependency h3 to v2.0.1-rc.17 [SECURITY]#921renovate[bot] wants to merge 1 commit intomainfrom
renovate[bot] wants to merge 1 commit intomainfrom
Conversation
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.16→2.0.1-rc.17GitHub Vulnerability Alerts
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.Release Notes
h3js/h3 (h3)
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
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.