Skip to content

Commit f8271ae

Browse files
committed
fix: improve query ID randomness and anti-spoofing
1 parent 3691485 commit f8271ae

5 files changed

Lines changed: 62 additions & 15 deletions

File tree

.github/workflows/release.yml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ jobs:
4343
with:
4444
node-version: "24"
4545
- uses: actions/checkout@v6
46-
- run: npm ci || npm install
46+
- run: npm ci
4747
- run: npm test
4848

49-
create-release:
49+
release:
5050
needs: [build, version]
5151
runs-on: ubuntu-latest
5252
permissions:
@@ -77,3 +77,18 @@ jobs:
7777
tag_name: v${{ needs.version.outputs.version }}
7878
name: ${{ needs.version.outputs.version }}
7979
generate_release_notes: false
80+
81+
publish:
82+
needs: release
83+
runs-on: ubuntu-latest
84+
permissions:
85+
contents: read
86+
id-token: write
87+
steps:
88+
- uses: actions/checkout@v6
89+
- uses: actions/setup-node@v6
90+
with:
91+
node-version: "24"
92+
registry-url: https://registry.npmjs.org/
93+
- run: npm ci
94+
- run: npm publish --provenance

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
44

55
### Unreleased
66

7-
### 2.2.0 - 2026-05-25
7+
### 2.2.1 - 2026-05-25
88

99
- fix(packet): preserve RDLENGTH+RDATA for unknown RR types
10+
- fix(packet): use crypto.randomInt for Packet.uuid (RFC 5452)
11+
12+
### 2.2.0 - 2026-05-25
13+
1014
- feat(client): add retryOverTCP option #117
1115
- feat(client): support `dns` argument, fix docs #116
1216
- feat: add resolveSOA #115

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dns2",
3-
"version": "2.2.0",
3+
"version": "2.2.1",
44
"description": "A DNS Server and Client Implementation in Pure JavaScript with no dependencies.",
55
"main": "index.js",
66
"types": "ts/index.d.ts",
@@ -41,12 +41,12 @@
4141
"license": "MIT",
4242
"repository": {
4343
"type": "git",
44-
"url": "git+https://github.com/song940/node-dns.git"
44+
"url": "git+https://github.com/lsongdev/node-dns.git"
4545
},
4646
"bugs": {
47-
"url": "https://github.com/song940/node-dns/issues"
47+
"url": "https://github.com/lsongdev/node-dns/issues"
4848
},
49-
"homepage": "https://github.com/song940/node-dns#readme",
49+
"homepage": "https://github.com/lsongdev/node-dns#readme",
5050
"devDependencies": {
5151
"@eslint/js": "^10.0.1",
5252
"@stylistic/eslint-plugin": "^5.10.0",

packet.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const { debuglog } = require('node:util');
2+
const { randomInt } = require('node:crypto');
23
const BufferReader = require('./lib/reader');
34
const BufferWriter = require('./lib/writer');
45

@@ -145,11 +146,13 @@ Packet.EDNS_OPTION_CODE = {
145146
};
146147

147148
/**
148-
* [uuid description]
149-
* @return {[type]} [description]
149+
* Generate a cryptographically random 16-bit DNS transaction ID.
150+
* RFC 5452 §3 — the full 16-bit space must be used from a CSPRNG to make
151+
* response forgery / cache poisoning impractical.
152+
* @return {number} integer in [0, 0xFFFF]
150153
*/
151154
Packet.uuid = function() {
152-
return Math.floor(Math.random() * 1e5);
155+
return randomInt(0x10000);
153156
};
154157

155158
/**

test/packet.js

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -672,8 +672,8 @@ test('Packet.RCODE is preserved through encode/parse round-trip', function() {
672672
});
673673

674674
test('Resource encode round-trips unknown type via raw data fallback', function() {
675-
// C1 (AUDIT-RFC.md): the encoder must write RDLENGTH+RDATA for types it
676-
// doesn't know how to serialize, otherwise the wire format is truncated.
675+
// the encoder must write RDLENGTH+RDATA for types it doesn't know how
676+
// to serialize, else the wire format is truncated.
677677
// 0xABCD is intentionally not in Packet.TYPE.
678678
const rdata = Buffer.from([ 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01 ]);
679679
const packet = new Packet();
@@ -695,9 +695,8 @@ test('Resource encode round-trips unknown type via raw data fallback', function(
695695
});
696696

697697
test('Resource encode of unknown type does not corrupt following records', function() {
698-
// The strongest signal for the C1 fix: without it, the missing RDLENGTH
699-
// would make the parser interpret the next record's bytes as RDATA, and
700-
// the A record below would never appear in `answers`.
698+
// without the fix, the missing RDLENGTH would make the parser interpret the
699+
// next record's bytes as RDATA, and the A record would never appear in `answers`.
701700
const packet = new Packet();
702701
packet.header.qr = 1;
703702
packet.answers.push({
@@ -747,6 +746,32 @@ test('Resource encode of unknown type with no data writes empty RDATA', function
747746
assert.equal(parsed.answers[1].address, '198.51.100.1');
748747
});
749748

749+
test('Packet.uuid returns a 16-bit integer', function() {
750+
// must use the full 16-bit space, not Math.random()*1e5.
751+
for (let i = 0; i < 1000; i++) {
752+
const id = Packet.uuid();
753+
assert.ok(Number.isInteger(id), `not an integer: ${id}`);
754+
assert.ok(id >= 0 && id <= 0xFFFF, `out of range: ${id}`);
755+
}
756+
});
757+
758+
test('Packet.uuid exercises the full 16-bit range with high diversity', function() {
759+
// Sample size large enough that a CSPRNG over [0, 0xFFFF] almost certainly
760+
// produces values in every quartile of the range. Catches regressions to a
761+
// constant, low-entropy, or capped implementation.
762+
const samples = new Set();
763+
const quartile = [ 0, 0, 0, 0 ];
764+
for (let i = 0; i < 5000; i++) {
765+
const id = Packet.uuid();
766+
samples.add(id);
767+
quartile[Math.floor((id / 0x10000) * 4)]++;
768+
}
769+
assert.ok(samples.size > 4000, `expected high diversity, got ${samples.size}`);
770+
for (let q = 0; q < 4; q++) {
771+
assert.ok(quartile[q] > 200, `quartile ${q} underrepresented (${quartile[q]}/5000)`);
772+
}
773+
});
774+
750775
test('Packet.parse tolerates multiple questions', function() {
751776
const request = new Packet();
752777
request.header.id = 0x9999;

0 commit comments

Comments
 (0)