Forty-Eight Defects in Eleven STUN and TURN Implementations

STUN has one mechanism binding a response to the request that asked for it. Three of the eleven implementations we tested broke it. In total we uncovered 48 defects, and none of the eleven came back clean.

Every video call, every peer-to-peer connection and every VoIP handset that works from behind a home router depends on STUN, and often on TURN behind it. It is the layer that discovers what a NAT has done to your address and, when that fails, relays the media through a server instead.

The binding request is unauthenticated by design. It has to be, since it is how a device finds out who it is before any session exists.

We tested eleven independent STUN and TURN implementations and uncovered 48 defects. None of the eleven came back clean.

The only thing binding a response to a request

A STUN message carries a 96-bit transaction identifier. It is the sole mechanism tying a response to the request that asked for it, and on an unauthenticated binding exchange it is the only thing an attacker has to guess to forge an answer.

Three implementations weaken it, in three different ways.

Product I generates the same transaction identifiers in every process. Not merely predictable within a run: identical across installations.

Product C builds one that carries 32 bits of entropy rather than 96, and leaks stack contents in the remainder.

Product D publishes a value from which the transaction identifier can be derived.

None of these is a memory-safety bug and none will crash anything. They convert a value that is supposed to be unguessable into one an off-path attacker can supply, which is the entire basis on which an unauthenticated response is trusted.

Attribute lengths, and the bytes after them

A STUN message is a header followed by type-length-value attributes, padded to four-byte boundaries. Five implementations read past what an attribute actually carries.

Product J has three in one parser: an error code attribute shorter than the four bytes its fixed fields occupy, value parsers that read four bytes regardless of what the length declares, and a header read before any length check at all.

The same product has a fourth that arrives from a different direction entirely. When the STUN server is configured as a domain rather than an address, the name is resolved and the resulting service record target is copied into a 48-byte stack buffer, bounded by the distance to a separator rather than by the size of the destination. There is an assertion above the copy that looks like a bound and is not one, because it compares that distance against the length of the input. So the attacker on this path is whatever answers the name lookup, not the peer on the other end of the STUN exchange.

Product I reads a string attribute past its stack buffer, and the bytes it read go back out on the wire in the response. Product C reads out of bounds while fetching an error code. Product H leaks memory on duplicate relay data attributes.

Product F has the one worth pausing on. Its fingerprint check computes a checksum over whatever is sitting in the buffer rather than over the message that was actually decoded. An integrity mechanism that validates the wrong bytes reports success on a message nobody verified.

Tables that grow for anyone who asks

TURN maintains per-client state, and the state is created before the client has authenticated. Six implementations never bound it.

Product B has four in the same server: a nonce map that grows once per unauthenticated datagram and is never pruned, no allocation quota so one credential yields one relay socket per source port, an uncapped permission map, and channel bindings in a flat list so creating them costs quadratic time.

Product H keeps an unbounded cache of pre-authentication responses. Product E builds per-session state for unauthenticated peers on its TCP path and leaves its packet queue uncapped with no drop policy. Product C lets idle peers hold connection slots.

The pattern is consistent. The work and the memory are committed when the first packet arrives, and the credential check happens afterwards.

Answers larger than the questions

STUN sits on UDP and answers strangers, which makes reply size a security property.

Product K measures at seven times unauthenticated amplification and answers indications it should ignore, and separately turns 4 KB of input into 160 KB of log output. Product A pads responses out to the path MTU rather than to the size of the request that prompted them.

Logs are the quieter version of the same problem. Product K puts attacker bytes into them unfiltered, and Product A logs a wrong relay thread three times for each unauthenticated byte that selects it.

Very small messages

Product K is killed entirely by a one-byte datagram whenever relaying is disabled. Product I aborts on a 24-byte datagram. Product G has a 20-byte TCP payload that pins a worker forever, loses its listener permanently on a single accept error, and can be ended by 8.2 MB of traffic that is fully authenticated.

Product A has the architectural version: its framing layer and its dispatcher disagree about what the buffer in front of them is.

Things that report success

Several findings here are not crashes but false assurance, which is harder to notice in production and worse to rely on.

Product F reports a successful connection for a secure transport that can never complete a handshake, and does one read with no framing, so a peer decides how many messages the caller sees. Product K hands a client a second allocation through a stale record. Product A has an allocation that survives its own deletion. Product C frees its authentication provider while it is in use, as a consequence of turning authentication on.

Product E has a different kind of surprise: a default-configured agent listens on multicast DNS, which is a protocol the developer did not ask for and may not know is exposed.

Finding these with Penzzer

Select INET4 or INET6 and choose STUN for a client under test or STUN Server for a server, then point the run at the target. The binding surface needs no credentials, which is what makes it reachable before anything else in a media stack.

Protocol-aware fuzzing walks the message as fields: the type and length in the header, the magic cookie, the transaction identifier, and each attribute as a type, a declared length and the padding that follows it, with the declared length driven against the bytes actually present. Seven attack modules run alongside:

| Module | The shape it provokes | | --- | --- | | Attribute length and padding abuse | A declared attribute length against what follows it | | Attribute count amplification | Many attributes against a per-message budget | | Message integrity and fingerprint abuse | An integrity check over the wrong bytes | | Unauthenticated state table growth | State committed before the credential is checked | | TURN allocation and permission abuse | Allocations and permissions created without limit | | Channel data framing abuse | A framing length against the data that arrived | | Log and reply amplification | Output disproportionate to the input that caused it |

‍

What this article is, and is not

The 48 defects above are a sample of what this kind of testing surfaces, not a description of Penzzer's STUN coverage. They are what eleven specific implementations happened to get wrong.

The attack library spans defect classes well beyond what this article shows: memory safety, information disclosure, resource exhaustion, availability, hangs and algorithmic complexity, input validation, protocol state logic, type and format handling, and error handling. Across the 107 protocols with a protocol-aware target, a run also generates its test cases from the structure of the protocol itself, walking each field through the boundaries where parsers break.

The same limit applies in reverse. What a test looks for is a property of the test, not a claim about your device.

If your product makes calls or connects peers

It speaks STUN, and probably TURN, and the stack is almost certainly a dependency rather than something you wrote.

Three questions. Is the transaction identifier produced by a cryptographic source, with its full ninety-six bits, and does it differ between two installations of your product? Does any per-client table grow before authentication, and if so what bounds it? And does every attribute read check the declared length against the bytes that are actually present, including the degenerate case of an attribute shorter than its own fixed fields?

That none of the eleven came back clean is the number to sit with. This is a protocol that runs unauthenticated, on UDP, on a port reachable from the internet, in software almost nobody writes themselves.

Other Post
Uncover Hidden Vulnerabilities

Identify security flaws before attackers do, automatically and at scale with Penzzer's intelligent fuzzing engine.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.