coturn is the TURN/STUN relay most WebRTC deployments run. Its job is to accept traffic from a client and forward it somewhere else, which makes the list of places it will forward to a security control rather than a convenience. That list is denied-peer-ip, and operators use it to keep the relay away from loopback, link-local, cloud metadata, and internal ranges. The check behind it recognized one way of writing an IPv4 address inside an IPv6 address, the mapped form ::ffff:10.0.0.5. The same address written any of the other legal ways, such as the NAT64 form 64:ff9b::a00:5, did not match the operator’s IPv4 range and was allowed through, on a path that then makes a real outbound TCP connection. Reported to coturn and fixed in 4.13.1 (CVE-2026-73212, CWE-284 and CWE-918, Moderate, CVSS 5.8).
IMPORTANT
Who’s exposed. You run coturn 4.13.0 or earlier, TCP relaying is enabled (it is on by default), and you rely on denied-peer-ip to keep the relay out of somewhere. Exploiting it needs one valid TURN credential, not an admin one, which in a normal WebRTC deployment is something the service hands to visitors. What the bypass is worth then depends entirely on your topology: if nothing sensitive is reachable from the coturn host, an attacker gets a relay to somewhere they could already reach. If something sensitive is reachable, they get a relay to it. If you have no denied-peer-ip rules and no internal services near the relay, this changes nothing for you.
One spelling out of many
Every peer address coturn will relay to passes through a single gate, good_peer_addr() in src/server/ns_turn_server.c. The gate runs the built-in checks for multicast, loopback, and the zero address, then walks the operator’s denied-peer-ip ranges. All of those comparisons live in helpers in src/client/ns_turn_ioaddr.c, and each of them had learned to recognize exactly one IPv6 encoding of an IPv4 address: the mapped form, tested with IN6_IS_ADDR_V4MAPPED.
That single case was added by an earlier round of hardening, for CVE-2026-27624, which found that ::ffff:127.0.0.1 slipped past a denylist written in IPv4. The fix was right for the address it was about. The difficulty is that an IPv4 address can be carried inside an IPv6 address in several legal ways, and the check only knew the one:
| How the destination is written | Recognized as IPv4? |
|---|---|
::ffff:10.0.0.5 (IPv4-mapped) | yes |
::10.0.0.5 (IPv4-compatible) | no |
64:ff9b::a00:5 (NAT64 well-known prefix) | no |
2002:a00:5::1 (6to4) | no |
fc00:1ab::99 (native IPv6, no IPv4 inside it) | not applicable, and no IPv6 range to match against |
When the address is not in the mapped form, the range comparison runs over the full sixteen bytes of the IPv6 address against an IPv4 range. It never matches. The rule the operator wrote is still there and still correct; it simply has nothing to say about the address in front of it, so the gate falls through to allow.
The last row is a separate gap. denied-peer-ip rules are commonly written in IPv4 because that is what the documentation examples show, and an internal service on a native IPv6 address, the ordinary dual-stack case, is not expressible in those rules at all.
The path the gate guards
The gate is shared, but the interesting caller is the RFC 6062 TCP CONNECT path, handle_turn_connect(). TCP relaying is enabled by default. Once good_peer_addr() returns, the request continues into tcp_start_connection_to_peer() and then to a real bufferevent_socket_connect(), with only family, duplicate, and socket checks in between. There is no second policy check between the decision and the connection, so whatever the gate lets past is what the relay dials. The inbound side, where a peer connects to the relay, consults the same gate and therefore shares the same blind spot.
Against a lab server configured the way the documentation recommends, with denied-peer-ip entries for loopback, the cloud metadata address, and 10.0.0.0/8, the difference between two spellings of one denied destination is the whole finding:
| Destination sent in CONNECT | Result |
|---|---|
::ffff:10.0.0.5 | 403, logged as denied in the range 10.0.0.0-10.255.255.255 |
::ffff:169.254.169.254 | 403, denied |
::1 | 403, denied |
64:ff9b::a00:5 (NAT64 form of the same 10.0.0.5) | 200 Success, relay connected |
64:ff9b::a9fe:a9fe (NAT64 form of the denied metadata address) | 200 Success, relay connected |
::10.0.0.5, 2002:a00:5::1 | passed the gate, connect failed to route |
Two rows need their footnote. The last one cleared the access-control check, which is the bug, but the host could not route the address, so nothing was relayed; that is a bypass of the gate and not a delivered connection. The two NAT64 rows connected through the translator in our lab, and a standards-compliant NAT64 will not map RFC 1918 or metadata ranges at all, so whether those particular destinations are deliverable depends on the translator sitting in front of them. What does not depend on any of that is the part the gate is responsible for: in every non-mapped row, the ACL stopped applying.
What it reaches, and what it doesn’t
In a network-isolated lab we took the bypass as far as it goes. Using a native IPv6 internal address, the kind an IPv4 denylist cannot express, we relayed to an internal Redis, wrote an SSH key through it, and logged in to that internal host as root. Every byte went through the coturn relay, and coturn logged the connection as a success with no denial.
That root shell came from the Redis, not from coturn: the relay only carried traffic to it. A takeover like that needs a writable internal service already in reach, and an unauthenticated Redis is both the easiest one to demonstrate against and a common one to find. Where nothing like it is reachable, the finding is SSRF into whatever is, which is why the advisory rates it Moderate.
Remediation
If you maintain the code. The durable fix is to reduce an address to a canonical form before any policy check reads it, rather than teaching each check to recognize one more encoding. This is what shipped in 4.13.1: a helper, ioa_addr_get_embedded_ipv4(), extracts the embedded IPv4 from the mapped, compatible, 6to4, and NAT64 well-known-prefix forms, and applies the IPv4 policy to all of them. A companion change denies link-local, unique-local, and site-local peers by default, which closes the native IPv6 gap without requiring every operator to discover it and write new rules.
If you operate it. Upgrade to 4.13.1. Two things are worth doing alongside the upgrade, and the advisory is open about the first:
- The canonicalization covers the well-known NAT64 prefix. A deployment using a custom translator prefix, or the RFC 8215 local-use prefix, is outside what the helper recognizes, so do not treat
denied-peer-ipas complete on its own. - Restrict the relay at the network layer. coturn makes genuine outbound connections as a matter of function, so an egress firewall around the relay host expresses the boundary in a place that does not depend on parsing an address correctly. If you have IPv4 denials today, check whether the destinations you meant to block also have IPv6 addresses.
Disclosure
- Software: coturn, 4.13.0 and earlier. Tested against 4.13.0 built from master.
- Class: CWE-284, improper access control, with CWE-918, server-side request forgery.
- Severity: Moderate, CVSS 5.8 (
AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:H). The vector records the shape of it: no impact on coturn itself, high impact on the systems behind it. - CVE: CVE-2026-73212; advisory public 2026-08-05.
- Fix: 4.13.1.
All testing was in a network-isolated lab against servers we controlled. The internal services and every credential in it are synthetic.
Canonicalize before you compare
The useful thing here is not that an ACL had a gap. It is where the gap came from: the comparison ran before the address was reduced to one form, so the rule was only ever enforced against the spellings the check already knew.
That is why fixing these one encoding at a time keeps not finishing. The advisory before this one carries the pattern in its own title: it was about the case that an earlier fix had not covered. Each fix was correct about the address it was about, and the class stayed wider than the instance. So when you see a check taught to recognize one encoding of a value, the finding is usually still there in two directions: the other encodings of that same value, and the other code paths that reach the same check. Here the first direction gave four more spellings and the second gave the CONNECT path, where the decision is followed immediately by a real connection.
This work sits in our network and infrastructure practice and is part of our ongoing vulnerability research into the open-source software that everyone else builds on.