A recursive resolver doesn’t answer one client. It remembers what it learns and serves that memory to everyone who asks next. Caching is where DNS gets its scale, and it’s also why a single bad answer can matter so much. Poison the memory once and the resolver repeats the lie on its own.
That’s DNS cache poisoning. The classic version is a race, where an attacker tries to slip a forged response into the gap while a resolver waits for a real one.
A forged answer has to match an outstanding query
DNS over UDP opens no connection. The resolver sends a datagram to an authoritative server and waits for something that matches the transaction it has in flight. The DNS header carries a 16-bit transaction ID chosen by the requester, and the response has to echo it back, but modern acceptance rules demand considerably more than that.
RFC 5452 requires a response to match the outstanding query on all of the following before its data is accepted:
- The question in the response matches an outstanding question
- The transaction ID matches
- The response comes from the address the query was sent to
- The response arrives on the same local address and port the query left from
An off-path attacker, one who can’t see the real query go out, has to guess enough of those values to make a forgery look authentic.
The race itself
Say a resolver needs www.example.com A. It queries the authoritative server, and the genuine response is now in flight. The attacker floods forged responses claiming to come from that server, each carrying something like:
www.example.com. 300 IN A 192.0.2.66
A forgery only works if the resolver accepts it as the answer to that outstanding query. Wrong ID, wrong port, wrong source, wrong question, and it should be discarded. If a matching forgery arrives first, the false RRset gets cached. If the real answer lands first, the transaction closes, and later forgeries no longer match anything outstanding.
Kaminsky made the race repeatable
Attacking a name directly used to carry a real cost. Lose the race for www.example.com, the genuine answer gets cached, and you wait for that RRset to expire before another upstream race is even available.
In 2008 Dan Kaminsky removed the wait. Instead of attacking the popular name, query one nobody has cached: a1.example.com, then a2.example.com, and onward. Each unique name misses the cache, forces a fresh upstream query, and opens a new race immediately. Losing costs almost nothing beyond moving to the next label. (Not literally unlimited: negative caching, resolver rate limits, and query coalescing all push back. But the attacker no longer waits on a TTL.)
The payload made it worse. Those forged responses didn’t have to carry a bogus address for the throwaway name. They could carry bogus delegation data in the authority section, with matching addresses alongside it, pointing the zone’s name servers at attacker infrastructure. Win once and the attacker can redirect later lookups for the entire zone through that resolver, for as long as the poisoned data survives in its cache.
Source-port randomization widened the guess
The 16-bit transaction ID allows 65,536 values, which is a small search space for an attacker sending thousands of packets. Randomizing the UDP source port per query adds a second value the attacker has to guess, and that was the emergency mitigation the industry shipped in 2008. RFC 5452, published the following January, standardized it alongside unpredictable transaction IDs and strict response matching.
How much unpredictability this buys depends on the deployment. A resolver may be confined to a narrow port range, and network address translation can rewrite source ports on the way out. What protects the resolver is the combined uncertainty genuinely available to it, not a theoretical bit count. Port randomization also raises the cost of an attack rather than eliminating it.
Admission rules limit the blast radius
Kaminsky worked because a response can carry more than the record you asked for. Delegations bring NS records and glue addresses, and other answers can trigger additional-section processing. So which of that data is a resolver willing to keep?
Two separate mechanisms are at work. Relevance and bailiwick checks decide which authority and additional records are even eligible for caching, rejecting data about zones the responder has no business speaking for. Separately, RFC 2181’s ranking of data trustworthiness decides which data wins when a newly received record conflicts with something already cached. Neither authenticates a packet, and a well-formed forgery can still look thoroughly authoritative, but together they stop one response from rewriting whatever it likes.
One poisoned cache serves everyone behind it
The damage scales with how widely a resolver is shared. Once false data is in the cache, every client within that cache’s scope can receive it, and none of them has to be present for the original forgery.
www.example.com and stores it. The next client asking that resolver for the same record gets the false address straight from cache. The attacker never has to win a second race while that entry stays usable.
The entry sticks until ordinary cache behavior clears it: expiry, eviction, an administrative flush, or a refresh path. There’s no single number for that lifetime, since resolver policies including stale serving all play a part. This is what separates poisoning from spoofing one reply to one endpoint.
On-path attackers don’t have to guess
Transaction IDs and port randomization defend against attackers who must predict the transaction. Someone who can observe the traffic simply reads the query name, ID, source port, destination, and timing off the wire. RFC 5452 says plainly that its defenses don’t protect against a party able to observe, modify, or inject traffic on the resolver’s path.
That turns the problem from prediction into authentication. Encrypted transports such as DNS over TLS or HTTPS protect a covered hop from both observation and tampering, depending on where the encryption ends, but they don’t authenticate ordinary unsigned DNS data across the full recursive path.
Encryption helps most against exactly the attacker randomization can’t stop, the one reading the query off the wire. But it only protects the hop it covers, and the recursive-to-authoritative leg, where much of this exposure sits, is today still mostly cleartext.
DNSSEC makes a matching forgery insufficient
A validating resolver runs the usual transaction checks and then verifies the data cryptographically. For a signed zone with an intact chain of trust, an attacker can’t mint a replacement RRset that validates without the signing key, so winning the UDP race stops being enough.
The test changes from “does this match the transaction?” to “does it match the transaction and validate under the trusted chain?” That’s a far stronger question, and it’s the subject of DNSSEC. The protection is also specific. DNSSEC offers no confidentiality and claims none against denial of service. It authenticates data that validates as secure, so a zone a resolver has proven to be unsigned gains nothing, and parent-side delegation records are themselves unsigned, meaning forged delegation data can still break resolution for a signed zone even when it can’t produce a validating answer.
Cookies and fragmentation narrow other surfaces
DNS Cookies (RFC 7873, updated by RFC 9018) add a lightweight transaction check. Client and server exchange cookie values that later messages carry, making blind forgery harder. The protection is deliberately modest, requires support at both ends, and does nothing once an attacker can observe the cookie.
Fragmentation is the other soft spot. Large UDP responses can fragment at the IP layer, and because individual fragments don’t each carry a complete DNS message, fragment handling has been abused to inject data into otherwise legitimate responses. RFC 9715 recommends avoiding IP fragmentation for DNS over UDP: keep response sizes below the point where fragmentation happens, have requestors drop fragmented UDP responses rather than reassembling them, and fall back to TCP instead.
Summary
Cache poisoning succeeds when false data is accepted into a recursive resolver’s memory and reused as though it were legitimate. Classic off-path attacks race a forged response against the real one, and Kaminsky’s contribution was showing that fresh names make that race endlessly repeatable, with a delegation as the prize rather than a single hostname.
Nothing about the resolver’s job has changed. It still learns answers from upstream and remembers them for whoever asks next. What changed is how much has to line up before something is allowed into that memory. Transaction IDs and randomized ports make blind guessing expensive, admission rules limit what one response can touch, cookies and fragmentation guidance close specific gaps, encrypted transport denies an on-path attacker the values they would otherwise read off the wire, and DNSSEC authenticates the data itself.