Skip to content

Commit de48d8e

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

3 files changed

Lines changed: 39 additions & 9 deletions

File tree

CHANGELOG.md

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

55
### Unreleased
66

7+
- fix(packet): use crypto.randomInt for Packet.uuid (RFC 5452)
8+
- fix(packet): preserve RDLENGTH+RDATA for unknown RR types
9+
710
### 2.2.0 - 2026-05-25
811

9-
- fix(packet): preserve RDLENGTH+RDATA for unknown RR types
1012
- feat(client): add retryOverTCP option #117
1113
- feat(client): support `dns` argument, fix docs #116
1214
- feat: add resolveSOA #115

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)