GHSA-85RG-P3FR-XC2F
Vulnerability from github – Published: 2026-07-28 16:36 – Updated: 2026-07-28 16:36Impact
The relay's reconnect handler forwards every RECONNECT_REQUEST to the host without deduplication or a size cap on the pendingReconnects map, unlike the connect flow which guards against this with maxPendingConnections. An unauthenticated attacker who knows a valid session ID can send RECONNECT_REQUEST packets from many spoofed source addresses; each packet that passes the session lookup is forwarded to the host as a new reconnect attempt. Because the per-source rate limiter assigns a fresh token bucket to each spoofed IP, it provides no protection. The host receives one forwarded packet per spoofed source per cleanup cycle, making the relay an amplification vector for denial-of-service against the host. The host's real address is never exposed to clients by design, so this is the primary viable DoS path against it. A secondary issue: once more than maxRateLimiters spoofed IPs are seen, performCleanup calls rateLimiters.clear(), resetting rate limit state for all sources including legitimate ones.
Affected: NeonRelay in all three implementations (Java, Python, TypeScript).
Patches
Not yet patched. Fix consists of three changes to handleReconnectRequest in each implementation:
- Reject the request if
pendingReconnects.size() >= maxPendingConnections(mirrors the existing connect-flow guard) - Only forward to the host if the
sessionId:clientIdkey is not already present inpendingReconnects— the map entry can still be updated with the new source address, but the host only needs to validate once per slot - In
performCleanup, evict throttled entries before falling back torateLimiters.clear()to avoid resetting legitimate sources' rate limit state
Workarounds
Operators can partially mitigate by placing the relay behind a network-level filter that drops packets with spoofed source addresses (BCP38/uRPF). This does not address the missing pendingReconnects size cap or the rateLimiters.clear() issue but eliminates the amplification path in most deployment environments.
References
- PROTOCOL.md — reconnect flow
- ARCHITECTURE.md — reconnect handshake detail (step 4 describes the unbounded
put)
{
"affected": [
{
"package": {
"ecosystem": "Maven",
"name": "com.quietterminal:qti-neon"
},
"versions": [
"1.0.0"
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "qti-neon"
},
"versions": [
"1.0.0"
]
},
{
"package": {
"ecosystem": "npm",
"name": "qti-neon"
},
"versions": [
"1.0.0"
]
}
],
"aliases": [
"CVE-2026-54609"
],
"database_specific": {
"cwe_ids": [
"CWE-400",
"CWE-406",
"CWE-770"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-28T16:36:47Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Impact\nThe relay\u0027s reconnect handler forwards every `RECONNECT_REQUEST` to the host without deduplication or a size cap on the `pendingReconnects` map, unlike the connect flow which guards against this with `maxPendingConnections`. An unauthenticated attacker who knows a valid session ID can send `RECONNECT_REQUEST` packets from many spoofed source addresses; each packet that passes the session lookup is forwarded to the host as a new reconnect attempt. Because the per-source rate limiter assigns a fresh token bucket to each spoofed IP, it provides no protection. The host receives one forwarded packet per spoofed source per cleanup cycle, making the relay an amplification vector for denial-of-service against the host. The host\u0027s real address is never exposed to clients by design, so this is the primary viable DoS path against it. A secondary issue: once more than `maxRateLimiters` spoofed IPs are seen, `performCleanup` calls `rateLimiters.clear()`, resetting rate limit state for all sources including legitimate ones.\n\nAffected: `NeonRelay` in all three implementations (Java, Python, TypeScript).\n\n### Patches\nNot yet patched. Fix consists of three changes to handleReconnectRequest in each implementation:\n\n1. Reject the request if `pendingReconnects.size() \u003e= maxPendingConnections` (mirrors the existing connect-flow guard)\n2. Only forward to the host if the `sessionId:clientId` key is not already present in `pendingReconnects` \u2014 the map entry can still be updated with the new source address, but the host only needs to validate once per slot\n3. In `performCleanup`, evict throttled entries before falling back to `rateLimiters.clear()` to avoid resetting legitimate sources\u0027 rate limit state\n\n### Workarounds\nOperators can partially mitigate by placing the relay behind a network-level filter that drops packets with spoofed source addresses (BCP38/uRPF). This does not address the missing `pendingReconnects` size cap or the `rateLimiters.clear()` issue but eliminates the amplification path in most deployment environments.\n\n### References\n- PROTOCOL.md \u2014 reconnect flow\n- ARCHITECTURE.md \u2014 reconnect handshake detail (step 4 describes the unbounded `put`)",
"id": "GHSA-85rg-p3fr-xc2f",
"modified": "2026-07-28T16:36:47Z",
"published": "2026-07-28T16:36:47Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Quiet-Terminal-Interactive/QTINeon/security/advisories/GHSA-85rg-p3fr-xc2f"
},
{
"type": "PACKAGE",
"url": "https://github.com/Quiet-Terminal-Interactive/QTINeon"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": "QTINeon has unauthenticated relay-to-host amplification via unbounded RECONNECT_REQUEST forwarding"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.