GHSA-vgrc-hq28-p3xp: Github.com/apernet/hysteria/core/v2 Hysteria Has An Authenticated UDP Vulnerability

HIGH
CVSS 7.4
CWE-284 CWE-918
Published
Advisory Summary

GHSA-vgrc-hq28-p3xp is a HIGH (CVSS 7.4) hysteria has an authenticated udp vulnerability (CWE-284: Improper Access Control) affecting Github.com/apernet/hysteria/core/v2 libraries in the Go ecosystem. The issue involves hysteria has an authenticated udp acl bypass that enables localhost and private-network udp ssrf. Remediation is available by upgrading affected packages to version 2.9.2.

GHSA-vgrc-hq28-p3xp at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-vgrc-hq28-p3xp
Severity HIGH
CVSS v3.1 Base Score 7.4
CWE Classification CWE-284: Improper Access Control
Affected Product Github.com/apernet/hysteria/core/v2
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 7.4

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Changed (S:C)
Confidentiality Low (C:L)
Integrity Low (I:L)
Availability Low (A:L)

How to Fix GHSA-vgrc-hq28-p3xp

To remediate this vulnerability, upgrade all affected Github.com/apernet/hysteria/core/v2 dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/apernet/hysteria/core/v2 fixed: 2.9.2
Upgrade github.com/apernet/hysteria/core/v2 to 2.9.2
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/apernet/hysteria/core/v2

How to Check if You Are Affected

To determine whether your application is affected by GHSA-vgrc-hq28-p3xp, inspect your project dependencies for Github.com/apernet/hysteria/core/v2:

inspect dependencies for github.com/apernet/hysteria/core/v2

Analyze the terminal output to locate any version of Github.com/apernet/hysteria/core/v2 in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.9.2), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-284: Improper Access Control, where the product does not restrict or incorrectly restricts access to a resource from an unauthorized actor. In Github.com/apernet/hysteria/core/v2, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary Hysteria's UDP relay treats the destination address as packet-scoped, but ACL and outbound policy are applied only once when a new UDP session is created. After an authenticated client opens a UDP session using an allowed first destination, later packets in the same `Session ID` can be sent to different destinations without re-running ACL evaluation. This allows an authenticated user to bypass server-side UDP ACL rules and reach localhost or RFC1918/private-network UDP services from the server's network perspective, even when those destinations are explicitly rejected by ACL. Verified on current HEAD at commit `64c396385631579598cc29d5561bff98c439772f`. ## Why this is a security issue This report is not based on the assumption that one UDP session must be bound to one destination. The protocol and official client both support per-packet destinations: - `PROTOCOL.md:93-107` defines each `UDPMessage` as carrying its own `Addr` field. - `core/client/udp.go:52-62` exposes `Send(data, addr)`, allowing the same UDP session to send to arbitrary addresses. The problem is that the security-relevant destination is packet-scoped, while ACL and outbound authorization are cached at session scope. This is also not a `RequestHook`-bypass claim. I understand `RequestHook` is first-packet-oriented. The broader issue is that operator-configured ACL policy intended to block UDP destinations is not enforced on later packets within the same session. Because the ACL documentation is presented as the mechanism for handling or blocking client requests, and includes examples of denying `udp/443` and private network CIDRs, operators can reasonably rely on ACL as a UDP egress security boundary. This boundary can currently be bypassed by reusing a previously authorized UDP session. ## Root cause The relevant flow appears to be: - `core/server/udp.go:280-299`: when a new session is created, the first destination is passed through `m.io.Hook(...)`, logged, and then `m.io.UDP(addr)` is called once to create the outbound UDP connection. - `core/server/server.go:397-398`: `m.io.UDP(addr)` delegates to `io.Outbound.UDP(reqAddr)`. - `app/cmd/server.go:1187-1190`: resolver, ACL, and actual outbounds are intentionally chained through the `Outbound` interface. - `core/server/udp.go:125`: the initial outbound connection is created only from the first packet via `DialFunc(firstMsg.Addr, firstMsg.Data)`. - `core/server/udp.go:92-111`: later packets in the same session take the current packet address and directly call `e.conn.WriteTo(dfMsg.Data, addr)` without re-running ACL or outbound policy evaluation. In other words, destination selection is packet-scoped, but authorization is session-scoped. ## Impact Any authenticated client that is allowed to use UDP relay can: - open one UDP session using an allowed first destination; - reuse the same session to send packets to destinations that ACL should reject; - reach UDP services on `127.0.0.1` or on RFC1918/private-network addresses from the server's network perspective. In real deployments, this can expose internal-only UDP services such as: - internal DNS resolvers; - service discovery endpoints; - telemetry or metrics listeners; - local administrative daemons; - application-specific UDP services intended to be reachable only from localhost or the internal network. This breaks the server's documented ACL-based UDP egress restrictions. ## Reproduction Two cases were reproduced with integration tests. ### Case 1: localhost bypass ACL: ```text direct(127.0.0.1, udp/<allowedPort>) reject(127.0.0.1/32) ``` Steps: 1. Start one UDP echo service on `127.0.0.1:<allowedPort>`. 2. Start another UDP echo service on `127.0.0.1:<blockedPort>`. 3. Connect an authenticated Hysteria client and create one UDP session. 4. Send a packet to the allowed loopback destination to establish the session. 5. Reuse the same UDP session and send a packet to the blocked loopback destination. Observed result: - The second packet is relayed successfully and the blocked loopback service replies. Expected result: - The second packet should be rejected because `127.0.0.1/32` is denied by ACL. ### Case 2: private-network bypass ACL: ```text direct(127.0.0.1, udp/<allowedPort>) reject(10.0.0.0/8) ``` or the corresponding local RFC1918 range, such as `192.168.0.0/16` or `172.16.0.0/12`. Steps: 1. Start one UDP echo service on `127.0.0.1:<allowedPort>`. 2. Start another UDP echo service on a real RFC1918 address of the server host. 3. Connect an authenticated Hysteria client and create one UDP session. 4. Send a packet to the allowed loopback destination first. 5. Reuse the same UDP session and send a packet to the RFC1918 destination. Observed result: - The private-address packet is relayed successfully and receives a reply. Expected result: - The packet should be rejected by ACL. ## PoC and local evidence A local integration test file was added during verification: - `core/internal/integration_tests/udp_private_acl_bypass_test.go` The two tests are: - `TestClientServerUDPACLBYPASSLoopback` - `TestClientServerUDPACLBYPASSPrivateIPv4` They can be executed with: ```bash go test ./core/internal/integration_tests -run 'TestClientServerUDPACLBYPASS(Loopback|PrivateIPv4)' -count=1 ``` The tests pass locally and demonstrate that a destination blocked by ACL becomes reachable after the session is established with an allowed first destination. ## Suggested fixes Any of the following would address the issue: 1. Re-evaluate ACL and outbound policy for every defragmented UDP packet before forwarding it with `WriteTo`. 2. Alternatively, enforce a single immutable destination per UDP session and reject destination changes after the first packet. 3. Ensure logging and policy hooks are aligned with the chosen model so that policy enforcement and observability reflect the real per-packet destination. ## Severity assessment Suggested CVSS v3.1 vector: `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L` This reflects a network-reachable issue with low attack complexity, requiring only an authenticated client, no victim interaction, and allowing impact beyond the proxy process by exposing localhost and internal-network UDP resources from the server's trust boundary.

GHSA-vgrc-hq28-p3xp Affected Github.com/apernet/hysteria/core/v2 Versions

1 component(s)
Fixed Versions:
2.9.2
Product / Package Ecosystem Affected Versions Fixed Version
github.com/apernet/hysteria/core/v2
Go
≥ 2.0.0
< 2.9.2
2.9.2

References

Frequently Asked Questions

What is GHSA-vgrc-hq28-p3xp?
GHSA-vgrc-hq28-p3xp is a security vulnerability in Github.com/apernet/hysteria/core/v2 involving Hysteria has an authenticated UDP ACL bypass that enables localhost and private-network UDP SSRF under CWE-284: Improper Access Control.
Which Github.com/apernet/hysteria/core/v2 versions are affected?
Affected versions include releases prior to 2.9.2 across affected packages: github.com/apernet/hysteria/core/v2.
What version fixes GHSA-vgrc-hq28-p3xp?
Upgrading to 2.9.2 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/apernet/hysteria/core/v2 affected?
Yes, github.com/apernet/hysteria/core/v2 is affected in releases prior to 2.9.2. Upgrade your dependency to 2.9.2 or later.
What is the CVSS score of GHSA-vgrc-hq28-p3xp?
GHSA-vgrc-hq28-p3xp has a CVSS v3.1 base score of 7.4 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L.
What CWE is associated with GHSA-vgrc-hq28-p3xp?
GHSA-vgrc-hq28-p3xp is classified under CWE-284: Improper Access Control (The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.).
How can I check Go dependencies for GHSA-vgrc-hq28-p3xp?
Run the dependency tree command: 'inspect dependencies for github.com/apernet/hysteria/core/v2'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-vgrc-hq28-p3xp",
  "modified" : "2026-06-26T19:48:50Z",
  "published" : "2026-06-26T19:48:50Z",
  "aliases" : [ ],
  "summary" : "Hysteria has an authenticated UDP ACL bypass that enables localhost and private-network UDP SSRF",
  "details" : "## Summary\n\nHysteria's UDP relay treats the destination address as packet-scoped, but ACL and outbound policy are applied only once when a new UDP session is created. After an authenticated client opens a UDP session using an allowed first destination, later packets in the same `Session ID` can be sent to different destinations without re-running ACL evaluation.\n\nThis allows an authenticated user to bypass server-side UDP ACL rules and reach localhost or RFC1918/private-network UDP services from the server's network perspective, even when those destinations are explicitly rejected by ACL.\n\nVerified on current HEAD at commit `64c396385631579598cc29d5561bff98c439772f`.\n\n## Why this is a security issue\n\nThis report is not based on the assumption that one UDP session must be bound to one destination. The protocol and official client both support per-packet destinations:\n\n- `PROTOCOL.md:93-107` defines each `UDPMessage` as carrying its own `Addr` field.\n- `core/client/udp.go:52-62` exposes `Send(data, addr)`, allowing the same UDP session to send to arbitrary addresses.\n\nThe problem is that the security-relevant destination is packet-scoped, while ACL and outbound authorization are cached at session scope.\n\nThis is also not a `RequestHook`-bypass claim. I understand `RequestHook` is first-packet-oriented. The broader issue is that operator-configured ACL policy intended to block UDP destinations is not enforced on later packets within the same session.\n\nBecause the ACL documentation is presented as the mechanism for handling or blocking client requests, and includes examples of denying `udp/443` and private network CIDRs, operators can reasonably rely on ACL as a UDP egress security boundary. This boundary can currently be bypassed by reusing a previously authorized UDP session.\n\n## Root cause\n\nThe relevant flow appears to be:\n\n- `core/server/udp.go:280-299`: when a new session is created, the first destination is passed through `m.io.Hook(...)`, logged, and then `m.io.UDP(addr)` is called once to create the outbound UDP connection.\n- `core/server/server.go:397-398`: `m.io.UDP(addr)` delegates to `io.Outbound.UDP(reqAddr)`.\n- `app/cmd/server.go:1187-1190`: resolver, ACL, and actual outbounds are intentionally chained through the `Outbound` interface.\n- `core/server/udp.go:125`: the initial outbound connection is created only from the first packet via `DialFunc(firstMsg.Addr, firstMsg.Data)`.\n- `core/server/udp.go:92-111`: later packets in the same session take the current packet address and directly call `e.conn.WriteTo(dfMsg.Data, addr)` without re-running ACL or outbound policy evaluation.\n\nIn other words, destination selection is packet-scoped, but authorization is session-scoped.\n\n## Impact\n\nAny authenticated client that is allowed to use UDP relay can:\n\n- open one UDP session using an allowed first destination;\n- reuse the same session to send packets to destinations that ACL should reject;\n- reach UDP services on `127.0.0.1` or on RFC1918/private-network addresses from the server's network perspective.\n\nIn real deployments, this can expose internal-only UDP services such as:\n\n- internal DNS resolvers;\n- service discovery endpoints;\n- telemetry or metrics listeners;\n- local administrative daemons;\n- application-specific UDP services intended to be reachable only from localhost or the internal network.\n\nThis breaks the server's documented ACL-based UDP egress restrictions.\n\n## Reproduction\n\nTwo cases were reproduced with integration tests.\n\n### Case 1: localhost bypass\n\nACL:\n\n```text\ndirect(127.0.0.1, udp/<allowedPort>)\nreject(127.0.0.1/32)\n```\n\nSteps:\n\n1. Start one UDP echo service on `127.0.0.1:<allowedPort>`.\n2. Start another UDP echo service on `127.0.0.1:<blockedPort>`.\n3. Connect an authenticated Hysteria client and create one UDP session.\n4. Send a packet to the allowed loopback destination to establish the session.\n5. Reuse the same UDP session and send a packet to the blocked loopback destination.\n\nObserved result:\n\n- The second packet is relayed successfully and the blocked loopback service replies.\n\nExpected result:\n\n- The second packet should be rejected because `127.0.0.1/32` is denied by ACL.\n\n### Case 2: private-network bypass\n\nACL:\n\n```text\ndirect(127.0.0.1, udp/<allowedPort>)\nreject(10.0.0.0/8)\n```\n\nor the corresponding local RFC1918 range, such as `192.168.0.0/16` or `172.16.0.0/12`.\n\nSteps:\n\n1. Start one UDP echo service on `127.0.0.1:<allowedPort>`.\n2. Start another UDP echo service on a real RFC1918 address of the server host.\n3. Connect an authenticated Hysteria client and create one UDP session.\n4. Send a packet to the allowed loopback destination first.\n5. Reuse the same UDP session and send a packet to the RFC1918 destination.\n\nObserved result:\n\n- The private-address packet is relayed successfully and receives a reply.\n\nExpected result:\n\n- The packet should be rejected by ACL.\n\n## PoC and local evidence\n\nA local integration test file was added during verification:\n\n- `core/internal/integration_tests/udp_private_acl_bypass_test.go`\n\nThe two tests are:\n\n- `TestClientServerUDPACLBYPASSLoopback`\n- `TestClientServerUDPACLBYPASSPrivateIPv4`\n\nThey can be executed with:\n\n```bash\ngo test ./core/internal/integration_tests -run 'TestClientServerUDPACLBYPASS(Loopback|PrivateIPv4)' -count=1\n```\n\nThe tests pass locally and demonstrate that a destination blocked by ACL becomes reachable after the session is established with an allowed first destination.\n\n## Suggested fixes\n\nAny of the following would address the issue:\n\n1. Re-evaluate ACL and outbound policy for every defragmented UDP packet before forwarding it with `WriteTo`.\n2. Alternatively, enforce a single immutable destination per UDP session and reject destination changes after the first packet.\n3. Ensure logging and policy hooks are aligned with the chosen model so that policy enforcement and observability reflect the real per-packet destination.\n\n## Severity assessment\n\nSuggested CVSS v3.1 vector: `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L`\n\nThis reflects a network-reachable issue with low attack complexity, requiring only an authenticated client, no victim interaction, and allowing impact beyond the proxy process by exposing localhost and internal-network UDP resources from the server's trust boundary.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/apernet/hysteria/core/v2"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "2.0.0"
      }, {
        "fixed" : "2.9.2"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 2.9.1"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/apernet/hysteria/security/advisories/GHSA-vgrc-hq28-p3xp"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/apernet/hysteria"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-284", "CWE-918" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-06-26T19:48:50Z",
    "nvd_published_at" : null
  }
}