People can use the DNSRegistrar::proveAndClaim
function to claim a DNS record on ENS. They have to provide the name to claim (in DNS wire format) and a signed proof. A comment says that the proof is "a chain of signed DNS RRSETs ending with a text record". According to this doc, the TXT record is expected to contain the data a=$ADDRESS
, where $ADDRESS
is the Ethereum address of the record's owner.
The code of the DNSRegistrar
contract is not checking that the name
parameter supplied to proveAndClaim
actually matches the name in the proven DNS record. This means that as long as a user has a valid proof for a legitimate DNS record, they can claim whatever DNS name. The claimed name may not necessarily be associated with the proof. And it's possible to do so for DNS names already claimed by other users.
This secret gist includes two test cases showcasing two proof-of-concept scenarios. They were built using the existing test environment in the test/dnsregistrar/TestDNSRegistrar.js
file, so as to reproduce the testing conditions you're familiar with.
In the first test case, we show how it's possible for anyone to claim multiple names using a single unrelated proof by calling the DNSRegistrar::proveAndClaim
function. For example, if Alice owns the alice.test
domain, she can use the proof for that domain to claim foo.test
and bar.test
on ENS.
In the second test case, we show how it's possible for anyone to takeover claimed DNS domains using a single unrelated proof. Again, calling the DNSRegistrar::proveAndClaim
function. For example, if Alice owns alice.test
, and she has already claimed the name on ENS, then Bob (who owns bob.test
) can claim alice.test
on ENS by providing a proof for bob.test
.
The root cause of this issue is in the getOwnerAddress
function of the DNSClaimChecker
library. Used by the internal DNSRegistrar::_claim
function during the claiming process, once the records have been verified by the oracle.
The code in DNSClaimChecker::getOwnerAddress
starts by building the name that it expects to find in the supplied TXT record. However, the name built (stored in the buf
local variable) is never checked against the real name that can be parsed from the proven record. You can notice that the buf
variable is actually never used in the rest of the getOwnerAddress
function.
The getOwnerAddress
function of the DNSClaimChecker
library should compare the built name to the name parsed from the proven DNS record. If the names do not match, then the DNS record ought to be discarded. At that point, getOwnerAddress
could return a false
flag, so that the DNSRegistrar::_claim
function reverts with error NoOwnerRecordFound()
.