chore(deps): update dependency h3 to v1.15.9 [security]#12370
Open
renovate[bot] wants to merge 1 commit intodevelopfrom
Open
chore(deps): update dependency h3 to v1.15.9 [security]#12370renovate[bot] wants to merge 1 commit intodevelopfrom
renovate[bot] wants to merge 1 commit intodevelopfrom
Conversation
|
Oops! Looks like you forgot to update the changelog. When updating CHANGELOG.md, please consider the following:
|
|
ℹ️ Coverage metrics explained: |
📊 commons test coverage |
📊 events test coverage |
|
This PR has been marked with label stale Since it has been inactive for 20 days. It will automatically be closed in 10 days if no further activity occurs. |
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:
1.15.6→1.15.9h3: SSE Event Injection via Unsanitized Carriage Return (
\r) in EventStream Data and Comment Fields (Bypass of CVE Fix)GHSA-4hxc-9384-m385
More information
Details
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.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
h3: Double Decoding in
serveStaticBypassesresolveDotSegmentsPath Traversal Protection via%252e%252eGHSA-72gr-qfp7-vwhw
More information
Details
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.Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
Release Notes
h3js/h3 (h3)
v1.15.9Compare Source
compare changes
🩹 Fixes
%25in pathname (1103df6)%252e%252e) (c56683d)v1.15.8Compare Source
compare changes
🩹 Fixes
%25in pathname (1103df6)v1.15.7Compare Source
compare changes
🩹 Fixes
..as a path segment only (c049dc0)💅 Refactors
❤️ Contributors
Configuration
📅 Schedule: (in timezone Europe/London)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Never, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about these updates again.
This PR was generated by Mend Renovate. View the repository job log.