cloudflare / cloudflare/workerd
node:dns lookup() returns CNAME records as IPv4/IPv6 LookupAddress entries
- Dominant language
- C++
- Stars
- 8.7k
- Forks
- 739
- Avg merge
- 2d 20h
- Merged PRs (30d)
- 174
Description
### Description
In Workerd, `node:dns` and `node:dns/promises` can return CNAME records as `LookupAddress` entries from `lookup()`. The CNAME hostname is placed in the `address` field and incorrectly labeled with `family: 4` or `family: 6`.
Node.js specifies that, when `all: true`, each returned object contains:
- `address`: a string representation of an IPv4 or IPv6 address
- `family`: `4` or `6`, denoting the family of that address
https://nodejs.org/api/dns.html#dnspromiseslookuphostname-options
### Environment
Reproduced with:
- `workerd@1.20260630.1`
- `wrangler@4.107.0`
- `compatibility_date: "2026-06-16"`
- `compatibility_flags: ["nodejs_compat"]`
- macOS arm64 using the Workers Vitest pool
The relevant implementation is also present at revision `eb7ae528648bc871c6693f7d635745f39b9ddbcf`.
### Reproduction
Configure a Worker with `nodejs_compat` and run:
```ts
import { lookup } from "node:dns/promises";
export default {
async fetch(): Promise {
const addresses = await lookup("writings.hongminhee.org", {
all: true,
});
return Response.json(addresses);
},
};
```
At the time of reproduction, the result was:
```json
[
{
"address": "hongminhee-writings.netlify.app.",
"family": 4
},
{
"address": "13.215.239.219",
"family": 4
},
{
"address": "52.74.6.109",
"family": 4
},
{
"address": "hongminhee-writings.netlify.app.",
"family": 6
},
{
"address": "2406:da18:b3d:e201::258",
"family": 6
},
{
"address": "2406:da18:b3d:e201::259",
"family": 6
}
]
```
`hongminhee-writings.netlify.app.` is a CNAME target, not an IPv4 or IPv6 literal, but it is returned twice and labeled as both address families.
The corresponding DNS-over-HTTPS responses contain a CNAME record followed by the requested address records. For example, the A response contains record type 5 followed by type 1 records:
```json
{
"Answer": [
{
"name": "writings.hongminhee.org",
"type": 5,
"data": "hongminhee-writings.netlify.app."
},
{
"name": "hongminhee-writings.netlify.app",
"type": 1,
"data": "13.215.239.219"
},
{
"name": "hongminhee-writings.netlify.app",
"type": 1,
"data": "52.74.6.109"
}
]
}
```
### Expected behavior
`lookup()` should return only actual IP address literals:
```json
[
{
"address": "13.215.239.219",
"family": 4
},
{
"address": "52.74.6.109",
"family": 4
},
{
"address": "2406:da18:b3d:e201::258",
"family": 6
},
{
"address": "2406:da18:b3d:e201::259",
"family": 6
}
]
```
The exact addresses and order may vary, but every `address` must be an IP literal matching its `family`.
### Suspected cause
For `lookup(hostname, { all: true })`, Workerd maps every record in each DNS-over-HTTPS `Answer` array without checking the record type:
https://github.com/cloudflare/workerd/blob/eb7ae528648bc871c6693f7d635745f39b9ddbcf/src/node/internal/internal_dns.ts#L145-L161
As a result:
- Every record from an A response is labeled `family: 4`, including CNAME records.
- Every record from an AAAA response is labeled `family: 6`, including CNAME records.
The non-`all` path similarly takes `Answer.at(0)?.data` without checking its type. If the first answer is a CNAME, it returns that hostname as an IP address:
https://github.com/cloudflare/workerd/blob/eb7ae528648bc871c6693f7d635745f39b9ddbcf/src/node/internal/internal_dns.ts#L181-L209
The explicit-family path has the same issue:
https://github.com/cloudflare/workerd/blob/eb7ae528648bc871c6693f7d635745f39b9ddbcf/src/node/internal/internal_dns.ts#L214-L234
`internal_dns_promises.ts` forwards these results through the promise API:
https://github.com/cloudflare/workerd/blob/eb7ae528648bc871c6693f7d635745f39b9ddbcf/src/node/internal/internal_dns_promises.ts#L31
### Suggested fix
Filter the DNS-over-HTTPS answers by record type before selecting or mapping them:
- Only type 1 records should be returned from an A query.
- Only type 28 records should be returned from an AAAA query.
- If no records of the requested type remain, return the appropriate lookup error rather than returning a CNAME.
Regression tests covering CNAME chains would be useful for:
- `all: true`, `family: 0`
- `all: false`, `family: 0`
- explicit `family: 4`
- explicit `family: 6`
### Impact
Libraries commonly assume the Node.js `LookupAddress` contract and pass `address` to IP parsing or SSRF-protection logic. The spec-violating CNAME entries can cause otherwise valid public URLs to be rejected or incorrectly processed.
This was observed in [Fedify](https://github.com/fedify-dev/fedify) URL validation when resolving an ActivityPub Article URL hosted behind a CNAME on [SiliconBeest](https://github.com/SJang1/siliconbeest).
Contributor guide
Research direction
Start with src/node/internal/internal_dns.ts, especially the all, non-all, and explicit-family paths linked in the issue, then check how src/node/internal/internal_dns_promises.ts forwards results. Add regression coverage for CNAME chains across the four listed lookup modes. Done means only type 1 or type 28 records are returned with matching families, and missing records produce the appropriate lookup error.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js
- Domain
- networking
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100