Visitar URL original
Validate the byte arrays Address6 is given by beaugunderson 路 Pull Request #217 路 beaugunderson/ip-address 路 GitHub
Skip to content

Validate the byte arrays Address6 is given - #217

Merged
beaugunderson merged 1 commit into
mainfrom
bg-validate-v6-bytes
Jul 31, 2026
Merged

beaugunderson merged 1 commit into
mainfrom
bg-validate-v6-bytes

Conversation

@beaugunderson

@beaugunderson beaugunderson commented Jul 31, 2026 •

Copy link
Copy Markdown
Owner

Supersedes #214, whose report and test matrix this builds on. Credited via Co-authored-by.

The bug

Address6.fromByteArray and fromUnsignedByteArray fold whatever they are handed into a BigInt and bounds-check only the aggregate, so wrong-length, out-of-range and non-integer input produces a plausible wrong address rather than an error. All four reproduce on main:

Address6.fromByteArray([1, 2, 3])                            // -> ::1:203
Address6.fromByteArray([...sixteen zeros, 1])                // -> ::1     (17 bytes)
Address6.fromUnsignedByteArray([...fifteen zeros, 300])      // -> ::12c
Address6.fromByteArray([1.5, ...fifteen zeros])              // -> 100::

The 17-byte case is why the existing fromBigInt bound cannot cover this: a leading zero byte keeps the magnitude under 2**128, so only a length check sees it. Address4 rejects all four.

Why it matters downstream

socks calls Address6.fromByteArray at three sites as Array.from(buff.readBuffer(16)). In parseUDPFrame that buffer is an unvalidated datagram, and smart-buffer's readBuffer clamps a short read with Math.min instead of throwing:

const lengthVal = typeof length === 'number' ? length : this.length;
const endPoint = Math.min(this.length, this._readOffset + lengthVal);

So a SOCKS5 UDP frame carrying four address bytes yields a remote host of 0000:0000:0000:0000:0000:0000:dead:beef today, silently. With the length check it raises AddressError. Severity is bounded: it needs the consumer to be doing UDP associate, and the result is a misattributed remote host rather than memory unsafety. The root cause is also on the socks side, since parseUDPFrame should reject a datagram too short for the host type it claims, and that is worth an issue there separately.

socks depends on ip-address: ^10.1.1, which is why this is deliberately a 10.x change and carries no break.

What changed

Both methods now require exactly 16 bytes, each an integer, and reject out-of-range values.

fromByteArray keeps folding signed bytes to unsigned, so an Int8Array or a Java byte[] works exactly as before and nothing that parses today stops parsing. Its floor is -128, below which folding would produce a value the caller did not mean: -129 & 0xff is 127. fromUnsignedByteArray takes 0 to 255 and rejects negatives.

The length and range checks live in one place in common.ts that Address4 now shares, replacing its own copy of the same loop. Address4's error messages are byte-identical after that refactor, which its existing tests cover.

Byte array parameters are typed number[] rather than any[], matching Address4. any[] remains assignable to number[], so callers still compile.

What is deferred, and how we will not lose track of it

Two differences are left alone here because settling them means a breaking change, and a major release would not reach the socks range that most downloads resolve through: Address6.fromByteArray accepts signed bytes while Address4.fromByteArray rejects them, and toUnsignedByteArray returns exactly what toByteArray returns, since toByteArray already emits 0 to 255 and unsigning it is an identity mapping.

Rather than a comment nobody reads, a test asserts the major version is below 11 and names what to delete when it is not. Verified by temporarily setting the version to 11.0.0:

AssertionError: ip-address is at 11.x, so collapse the byte array API: make
Address6.fromByteArray reject anything outside 0-255 as Address4.fromByteArray
does, delete unsignByte and the -128 floor from src/ipv6.ts, and reduce
fromUnsignedByteArray and toUnsignedByteArray to deprecated aliases. Then
delete this test.

Verified

3583 tests pass, up from 3571. npm run lint is clean, the committed README matches its generator, and the dist smoke test still passes on node 12.22.12. Each of the four reported inputs now throws, the short-datagram case throws, and sixteen -1s, a -128 first byte, sixteen unsigned bytes, and a toByteArray round trip all still parse.

The version stays at 10.3.1 here, since bumps in this repository are their own commit before the tag.

Address6.fromByteArray and fromUnsignedByteArray fold whatever they are given
into a BigInt and bounds-check only the aggregate, so wrong-length,
out-of-range and non-integer input produces a plausible wrong address instead
of an error: three bytes give ::1:203, seventeen bytes whose first is zero
give ::1, a low byte of 300 gives ::12c, and a byte of 1.5 gives 100::. A
17-byte array shows why the aggregate bound cannot cover this, since a leading
zero keeps the magnitude under 2**128. Address4 rejects all four.

This reaches consumers. socks calls Address6.fromByteArray at three sites with
Array.from(buff.readBuffer(16)), and in parseUDPFrame that buffer is an
unvalidated datagram while smart-buffer's readBuffer clamps a short read with
Math.min rather than throwing. A SOCKS5 UDP frame carrying four address bytes
therefore reports a remote host of 0000:0000:0000:0000:0000:0000:dead:beef.
Both methods now require exactly 16 bytes.

fromByteArray keeps folding signed bytes to unsigned, so an Int8Array or a
Java byte[] still works and nothing that parses today stops parsing; its floor
is -128, below which folding would silently produce a value the caller did not
mean. fromUnsignedByteArray takes 0 to 255. The length and range checks live
in one place that Address4 shares, replacing its copy of the same loop, with
its messages unchanged.

Address6.fromByteArray accepting signed bytes while Address4.fromByteArray
rejects them, and toUnsignedByteArray returning what toByteArray already
returns, are differences to settle in a major version rather than here, where
the point is to reach the versions socks resolves. A test asserts the major
version is below 11 and names what to delete when it is not.

The byte array parameters are typed number[] rather than any[], matching
Address4.

Co-authored-by: gaoflow <gaoflow@users.noreply.github.com>
@beaugunderson
beaugunderson merged commit 45a2b11 into main Jul 31, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant