cloudflare / cloudflare/workerd

node:dns lookup() returns CNAME records as IPv4/IPv6 LookupAddress entries

Open
#6,886 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.