CVE-2026-110314 (GCVE-0-2026-110314)
Vulnerability from cvelistv5
Published
2026-10-26 13:31
Modified
2026-10-26 13:31
Severity ?
VLAI Severity ?
EPSS score ?
CWE
- CWE-602 - Client-Side Enforcement of Server-Side Security
Summary
Client-Side Enforcement of Server-Side Security vulnerability in the WebSocket connection handling of mtrudel bandit allows a remote client to keep running WebSock handler code after the server has stopped its connection, via data frames sent after ignoring the server's Close frame.
When a WebSock callback returns {:stop, ...}, Bandit.WebSocket.Connection calls terminate/2, sends Close and shuts down the write side, but keeps the connection process running in a :closing state. handle_frame/3 and handle_info/3 never check that state, so later frames still reach handle_in/2 and later messages reach handle_info/2, with the state from the stop tuple, until the client sends Close, TCP drops or the idle timeout fires. A client refused in init/1 after a failed token check could still run an admin-only action.
This issue affects bandit: from 0.5.1 before 1.14.0.
References
Impacted products
{
"containers": {
"cna": {
"affected": [
{
"collectionURL": "https://repo.hex.pm",
"cpes": [
"cpe:2.3:a:mtrudel:bandit:*:*:*:*:*:*:*:*"
],
"defaultStatus": "unaffected",
"packageName": "bandit",
"packageURL": "pkg:hex/bandit",
"product": "bandit",
"repo": "https://github.com/mtrudel/bandit",
"vendor": "mtrudel",
"versions": [
{
"lessThan": "1.14.0",
"status": "affected",
"version": "0.5.1",
"versionType": "semver"
}
]
},
{
"collectionURL": "https://github.com",
"cpes": [
"cpe:2.3:a:mtrudel:bandit:*:*:*:*:*:*:*:*"
],
"defaultStatus": "unaffected",
"packageName": "mtrudel/bandit",
"packageURL": "pkg:github/mtrudel/bandit",
"product": "bandit",
"repo": "https://github.com/mtrudel/bandit",
"vendor": "mtrudel",
"versions": [
{
"lessThan": "220b9dfd27f9a21305be2afc75cbe585ddc24e7a",
"status": "affected",
"version": "bb407f2277dcb65a58dd8cc07913a0b58a80e266",
"versionType": "git"
}
]
}
],
"cpeApplicability": [
{
"nodes": [
{
"cpeMatch": [
{
"criteria": "cpe:2.3:a:mtrudel:bandit:*:*:*:*:*:*:*:*",
"versionEndExcluding": "1.14.0",
"versionStartIncluding": "0.5.1",
"vulnerable": true
}
],
"negate": false,
"operator": "OR"
}
],
"operator": "AND"
}
],
"credits": [
{
"lang": "en",
"type": "reporter",
"value": "Peter Ullrich"
},
{
"lang": "en",
"type": "remediation developer",
"value": "Mat Trudel"
},
{
"lang": "en",
"type": "analyst",
"value": "Jonatan M\u00e4nnchen / EEF"
}
],
"dateAssigned": "2026-10-23T12:47:59.000Z",
"descriptions": [
{
"lang": "en",
"supportingMedia": [
{
"base64": false,
"type": "text/html",
"value": "\u003cp\u003eClient-Side Enforcement of Server-Side Security vulnerability in the WebSocket connection handling of mtrudel bandit allows a remote client to keep running WebSock handler code after the server has stopped its connection, via data frames sent after ignoring the server\u0027s Close frame.\u003c/p\u003e\n\u003cp\u003eWhen a WebSock callback returns \u003ccode\u003e{:stop, ...}\u003c/code\u003e, \u003ccode\u003eBandit.WebSocket.Connection\u003c/code\u003e calls \u003ccode\u003eterminate/2\u003c/code\u003e, sends Close and shuts down the write side, but keeps the connection process running in a \u003ccode\u003e:closing\u003c/code\u003e state. \u003ccode\u003ehandle_frame/3\u003c/code\u003e and \u003ccode\u003ehandle_info/3\u003c/code\u003e never check that state, so later frames still reach \u003ccode\u003ehandle_in/2\u003c/code\u003e and later messages reach \u003ccode\u003ehandle_info/2\u003c/code\u003e, with the state from the stop tuple, until the client sends Close, TCP drops or the idle timeout fires. A client refused in \u003ccode\u003einit/1\u003c/code\u003e after a failed token check could still run an admin-only action.\u003c/p\u003e\n\u003cp\u003eThis issue affects bandit: from 0.5.1 before 1.14.0.\u003c/p\u003e"
},
{
"base64": false,
"type": "text/markdown",
"value": "Client-Side Enforcement of Server-Side Security vulnerability in the WebSocket connection handling of mtrudel bandit allows a remote client to keep running WebSock handler code after the server has stopped its connection, via data frames sent after ignoring the server\u0027s Close frame.\n\nWhen a WebSock callback returns `{:stop, ...}`, `Bandit.WebSocket.Connection` calls `terminate/2`, sends Close and shuts down the write side, but keeps the connection process running in a `:closing` state. `handle_frame/3` and `handle_info/3` never check that state, so later frames still reach `handle_in/2` and later messages reach `handle_info/2`, with the state from the stop tuple, until the client sends Close, TCP drops or the idle timeout fires. A client refused in `init/1` after a failed token check could still run an admin-only action.\n\nThis issue affects bandit: from 0.5.1 before 1.14.0."
}
],
"value": "Client-Side Enforcement of Server-Side Security vulnerability in the WebSocket connection handling of mtrudel bandit allows a remote client to keep running WebSock handler code after the server has stopped its connection, via data frames sent after ignoring the server\u0027s Close frame.\n\nWhen a WebSock callback returns {:stop, ...}, Bandit.WebSocket.Connection calls terminate/2, sends Close and shuts down the write side, but keeps the connection process running in a :closing state. handle_frame/3 and handle_info/3 never check that state, so later frames still reach handle_in/2 and later messages reach handle_info/2, with the state from the stop tuple, until the client sends Close, TCP drops or the idle timeout fires. A client refused in init/1 after a failed token check could still run an admin-only action.\n\nThis issue affects bandit: from 0.5.1 before 1.14.0."
}
],
"impacts": [
{
"capecId": "CAPEC-202",
"descriptions": [
{
"lang": "en",
"supportingMedia": [
{
"base64": false,
"type": "text/html",
"value": "\u003cp\u003eA remote client that ignores the Close frame can keep invoking a WebSock application\u0027s handlers after the application refused or ended its session, bypassing an authorization check in \u003ccode\u003einit/1\u003c/code\u003e or a kick or ban. This affects WebSock applications that rely on \u003ccode\u003e{:stop, ...}\u003c/code\u003e to end a session.\u003c/p\u003e"
},
{
"base64": false,
"type": "text/markdown",
"value": "A remote client that ignores the Close frame can keep invoking a WebSock application\u0027s handlers after the application refused or ended its session, bypassing an authorization check in `init/1` or a kick or ban. This affects WebSock applications that rely on `{:stop, ...}` to end a session."
}
],
"value": "A remote client that ignores the Close frame can keep invoking a WebSock application\u0027s handlers after the application refused or ended its session, bypassing an authorization check in init/1 or a kick or ban. This affects WebSock applications that rely on {:stop, ...} to end a session."
}
]
}
],
"metrics": [
{
"cvssV4_0": {
"Automatable": "NOT_DEFINED",
"Recovery": "NOT_DEFINED",
"Safety": "NOT_DEFINED",
"attackComplexity": "LOW",
"attackRequirements": "PRESENT",
"attackVector": "NETWORK",
"baseScore": 6.3,
"baseSeverity": "MEDIUM",
"privilegesRequired": "NONE",
"providerUrgency": "NOT_DEFINED",
"subAvailabilityImpact": "NONE",
"subConfidentialityImpact": "NONE",
"subIntegrityImpact": "NONE",
"userInteraction": "NONE",
"valueDensity": "NOT_DEFINED",
"vectorString": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N",
"version": "4.0",
"vulnAvailabilityImpact": "NONE",
"vulnConfidentialityImpact": "LOW",
"vulnIntegrityImpact": "LOW",
"vulnerabilityResponseEffort": "NOT_DEFINED"
},
"format": "CVSS",
"scenarios": [
{
"lang": "en",
"value": "GENERAL"
}
]
}
],
"problemTypes": [
{
"descriptions": [
{
"cweId": "CWE-602",
"description": "CWE-602 Client-Side Enforcement of Server-Side Security",
"lang": "en",
"type": "CWE"
}
]
}
],
"providerMetadata": {
"dateUpdated": "2026-10-26T13:31:18.257Z",
"orgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
"shortName": "EEF"
},
"references": [
{
"name": "WebSocket frames keep reaching the WebSock after a server-initiated close in Bandit",
"tags": [
"vendor-advisory",
"related"
],
"url": "https://github.com/mtrudel/bandit/security/advisories/GHSA-2v5v-8f9v-xh85"
},
{
"name": "EEF CNA record for CVE-2026-110314",
"tags": [
"related"
],
"url": "https://cna.erlef.org/cves/CVE-2026-110314.html"
},
{
"name": "OSV record EEF-CVE-2026-110314",
"tags": [
"related"
],
"url": "https://osv.dev/vulnerability/EEF-CVE-2026-110314"
},
{
"name": "Introducing commit bb407f2 in mtrudel/bandit",
"tags": [
"related"
],
"url": "https://github.com/mtrudel/bandit/commit/bb407f2277dcb65a58dd8cc07913a0b58a80e266"
},
{
"name": "Fix commit 220b9df in mtrudel/bandit",
"tags": [
"patch"
],
"url": "https://github.com/mtrudel/bandit/commit/220b9dfd27f9a21305be2afc75cbe585ddc24e7a"
}
],
"source": {
"discovery": "UNKNOWN"
},
"title": "WebSocket frames keep reaching the WebSock after a server-initiated close in Bandit",
"workarounds": [
{
"lang": "en",
"supportingMedia": [
{
"base64": false,
"type": "text/html",
"value": "\u003cp\u003eMark the WebSock state as stopped whenever a callback returns \u003ccode\u003e{:stop, ...}\u003c/code\u003e, and have \u003ccode\u003ehandle_in/2\u003c/code\u003e and \u003ccode\u003ehandle_info/2\u003c/code\u003e return \u003ccode\u003e{:ok, state}\u003c/code\u003e without acting while that mark is set.\u003c/p\u003e"
},
{
"base64": false,
"type": "text/markdown",
"value": "Mark the WebSock state as stopped whenever a callback returns `{:stop, ...}`, and have `handle_in/2` and `handle_info/2` return `{:ok, state}` without acting while that mark is set."
}
],
"value": "Mark the WebSock state as stopped whenever a callback returns {:stop, ...}, and have handle_in/2 and handle_info/2 return {:ok, state} without acting while that mark is set."
}
],
"x_proofOfConcept": [
{
"lang": "en",
"supportingMedia": [
{
"base64": false,
"type": "text/html",
"value": "\u003col\u003e\n\u003cli\u003eServe a WebSock whose \u003ccode\u003einit/1\u003c/code\u003e returns \u003ccode\u003e{:stop, :normal, {4401, \"unauthorized\"}, state}\u003c/code\u003e for a wrong token, and whose \u003ccode\u003ehandle_in/2\u003c/code\u003e runs an admin-only \u003ccode\u003edelete_all_users\u003c/code\u003e action when \u003ccode\u003euser_id\u003c/code\u003e is \u003ccode\u003eadmin\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eUpgrade with \u003ccode\u003e?user_id=admin\u0026amp;token=wrong\u003c/code\u003e. The server calls \u003ccode\u003eterminate/2\u003c/code\u003e and sends a Close frame with code 4401.\u003c/li\u003e\n\u003cli\u003eIgnore the Close frame and send a masked text frame \u003ccode\u003edelete_all_users\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ehandle_in/2\u003c/code\u003e runs \u003ccode\u003edelete_all_users\u003c/code\u003e as \u003ccode\u003eadmin\u003c/code\u003e.\u003c/li\u003e\n\u003c/ol\u003e"
},
{
"base64": false,
"type": "text/markdown",
"value": "1. Serve a WebSock whose `init/1` returns `{:stop, :normal, {4401, \"unauthorized\"}, state}` for a wrong token, and whose `handle_in/2` runs an admin-only `delete_all_users` action when `user_id` is `admin`.\n2. Upgrade with `?user_id=admin\u0026token=wrong`. The server calls `terminate/2` and sends a Close frame with code 4401.\n3. Ignore the Close frame and send a masked text frame `delete_all_users`.\n4. `handle_in/2` runs `delete_all_users` as `admin`."
}
],
"value": "* Serve a WebSock whose init/1 returns {:stop, :normal, {4401, \"unauthorized\"}, state} for a wrong token, and whose handle_in/2 runs an admin-only delete_all_users action when user_id is admin.\n* Upgrade with ?user_id=admin\u0026token=wrong. The server calls terminate/2 and sends a Close frame with code 4401.\n* Ignore the Close frame and send a masked text frame delete_all_users.\n* handle_in/2 runs delete_all_users as admin."
}
],
"x_technicalAnalysis": [
{
"lang": "en",
"supportingMedia": [
{
"base64": false,
"type": "text/html",
"value": "\u003cp\u003e\u003cstrong\u003e1. Stop without close.\u003c/strong\u003e In \u003ccode\u003eBandit.WebSocket.Connection\u003c/code\u003e, \u003ccode\u003edo_stop/4\u003c/code\u003e calls \u003ccode\u003eterminate/2\u003c/code\u003e, sends the Close frame and runs \u003ccode\u003eshutdown(:write)\u003c/code\u003e. It then returns \u003ccode\u003e{:continue, %{connection | state: :closing}}\u003c/code\u003e, so \u003ccode\u003eBandit.WebSocket.Handler\u003c/code\u003e keeps the process alive.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2. No state check.\u003c/strong\u003e \u003ccode\u003ehandle_frame/3\u003c/code\u003e and \u003ccode\u003ehandle_info/3\u003c/code\u003e never check \u003ccode\u003econnection.state\u003c/code\u003e. Every later data frame still reaches \u003ccode\u003ehandle_in/2\u003c/code\u003e, and every later message still reaches \u003ccode\u003ehandle_info/2\u003c/code\u003e, with the state from the stop tuple.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e3. Client-controlled lifetime.\u003c/strong\u003e The connection ends only when the client sends Close, TCP drops, or the idle timeout fires, and each client frame resets that timeout. All stops through \u003ccode\u003edo_stop\u003c/code\u003e are affected: \u003ccode\u003e{:stop, :normal, ...}\u003c/code\u003e, \u003ccode\u003e{:stop, {:shutdown, :disconnected}, ...}\u003c/code\u003e and \u003ccode\u003e{:stop, {:shutdown, :restart}, ...}\u003c/code\u003e, which is what Phoenix\u0027s \u003ccode\u003e:socket_drain\u003c/code\u003e returns. A banned user who was kicked kept posting for longer than the idle timeout.\u003c/p\u003e"
},
{
"base64": false,
"type": "text/markdown",
"value": "**1. Stop without close.** In `Bandit.WebSocket.Connection`, `do_stop/4` calls `terminate/2`, sends the Close frame and runs `shutdown(:write)`. It then returns `{:continue, %{connection | state: :closing}}`, so `Bandit.WebSocket.Handler` keeps the process alive.\n\n**2. No state check.** `handle_frame/3` and `handle_info/3` never check `connection.state`. Every later data frame still reaches `handle_in/2`, and every later message still reaches `handle_info/2`, with the state from the stop tuple.\n\n**3. Client-controlled lifetime.** The connection ends only when the client sends Close, TCP drops, or the idle timeout fires, and each client frame resets that timeout. All stops through `do_stop` are affected: `{:stop, :normal, ...}`, `{:stop, {:shutdown, :disconnected}, ...}` and `{:stop, {:shutdown, :restart}, ...}`, which is what Phoenix\u0027s `:socket_drain` returns. A banned user who was kicked kept posting for longer than the idle timeout."
}
],
"value": "1. Stop without close. In Bandit.WebSocket.Connection, do_stop/4 calls terminate/2, sends the Close frame and runs shutdown(:write). It then returns {:continue, %{connection | state: :closing}}, so Bandit.WebSocket.Handler keeps the process alive.\n\n2. No state check. handle_frame/3 and handle_info/3 never check connection.state. Every later data frame still reaches handle_in/2, and every later message still reaches handle_info/2, with the state from the stop tuple.\n\n3. Client-controlled lifetime. The connection ends only when the client sends Close, TCP drops, or the idle timeout fires, and each client frame resets that timeout. All stops through do_stop are affected: {:stop, :normal, ...}, {:stop, {:shutdown, :disconnected}, ...} and {:stop, {:shutdown, :restart}, ...}, which is what Phoenix\u0027s :socket_drain returns. A banned user who was kicked kept posting for longer than the idle timeout."
}
]
}
},
"cveMetadata": {
"assignerOrgId": "6b3ad84c-e1a6-4bf7-a703-f496b71e49db",
"assignerShortName": "EEF",
"cveId": "CVE-2026-110314",
"datePublished": "2026-10-26T13:31:18.257Z",
"dateReserved": "2026-10-23T12:45:01.107Z",
"dateUpdated": "2026-10-26T13:31:18.257Z",
"state": "PUBLISHED"
},
"dataType": "CVE_RECORD",
"dataVersion": "5.2"
}
Loading…
Loading…
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or seen somewhere by the user.
- Confirmed: The vulnerability is confirmed from an analyst perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: This vulnerability was exploited and seen by the user reporting the sighting.
- Patched: This vulnerability was successfully patched by the user reporting the sighting.
- Not exploited: This vulnerability was not exploited or seen by the user reporting the sighting.
- Not confirmed: The user expresses doubt about the veracity of the vulnerability.
- Not patched: This vulnerability was not successfully patched by the user reporting the sighting.
Loading…
Loading…