Skip to content

Commit b462216

Browse files
Doc, lint and version
1 parent 9e5d0f7 commit b462216

File tree

4 files changed

+51
-5
lines changed

4 files changed

+51
-5
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ All notable changes to this project will be documented in this file. The format
55
## Table of Contents
66

77
- [Unreleased](#unreleased)
8-
- [1.9.16 - 2025-12-02](#1916---2025-12-02)
8+
- [1.9.17 - 2025-12-01](#1917---2025-12-01)
9+
- [1.9.16 - 2025-12-01](#1916---2025-12-02)
910
- [1.9.15 - 2025-12-01](#1915---2025-12-01)
1011
- [1.9.14 - 2025-12-01](#1914---2025-12-01)
1112
- [1.9.13 - 2025-12-01](#1913---2025-12-01)
@@ -188,7 +189,7 @@ All notable changes to this project will be documented in this file. The format
188189
### Security
189190
---
190191

191-
### [1.9.16] - 2025-12-01
192+
### [1.9.17] - 2025-12-01
192193

193194
### Added
194195

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/primitives/__tests/utils.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
zero2,
55
toHex,
66
encode,
7+
toUTF8,
78
fromBase58,
89
toBase58,
910
fromBase58Check,
@@ -209,6 +210,48 @@ describe('utils', () => {
209210
})
210211
})
211212

213+
describe('toUTF8 bounds checks', () => {
214+
const guarded = (arr: number[]): number[] => {
215+
const target = arr.slice()
216+
const handler: ProxyHandler<number[]> = {
217+
get (t, prop, receiver) {
218+
if (prop === 'length' || typeof prop !== 'string') {
219+
return Reflect.get(t, prop as any, receiver)
220+
}
221+
const idx = Number(prop)
222+
if (Number.isInteger(idx)) {
223+
if (idx < 0 || idx >= t.length) {
224+
throw new Error(`out-of-bounds read at index ${idx} (length ${t.length})`)
225+
}
226+
}
227+
return Reflect.get(t, prop as any, receiver)
228+
}
229+
}
230+
return new Proxy(target, handler) as unknown as number[]
231+
}
232+
233+
it('does not access out-of-bounds on truncated 2-byte sequence', () => {
234+
const input = guarded([0xC3])
235+
expect(() => toUTF8(input)).not.toThrow()
236+
})
237+
238+
it('does not access out-of-bounds on truncated 3-byte sequences', () => {
239+
const input1 = guarded([0xE2])
240+
const input2 = guarded([0xE2, 0x82])
241+
expect(() => toUTF8(input1)).not.toThrow()
242+
expect(() => toUTF8(input2)).not.toThrow()
243+
})
244+
245+
it('does not access out-of-bounds on truncated 4-byte sequences', () => {
246+
const input1 = guarded([0xF0])
247+
const input2 = guarded([0xF0, 0x9F])
248+
const input3 = guarded([0xF0, 0x9F, 0x98])
249+
expect(() => toUTF8(input1)).not.toThrow()
250+
expect(() => toUTF8(input2)).not.toThrow()
251+
expect(() => toUTF8(input3)).not.toThrow()
252+
})
253+
})
254+
212255
describe('toArray base64', () => {
213256
it('decodes empty string to empty array', () => {
214257
expect(toArray('', 'base64')).toEqual([])

src/primitives/utils.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@ export const toUTF8 = (arr: number[]): string => {
237237

238238
for (let i = 0; i < arr.length; i++) {
239239
const byte = arr[i]
240-
240+
// this byte is part of a multi-byte sequence, skip it
241+
// added to avoid modifying i within the loop which is considered unsafe.
241242
if (skip > 0) {
242243
skip--
243244
continue
@@ -287,6 +288,7 @@ export const toUTF8 = (arr: number[]): string => {
287288
((byte3 & 0x3f) << 6) |
288289
(byte4 & 0x3f)
289290

291+
// Convert to UTF-16 surrogate pair
290292
const surrogate1 = 0xd800 + ((codePoint - 0x10000) >> 10)
291293
const surrogate2 = 0xdc00 + ((codePoint - 0x10000) & 0x3ff)
292294
result += String.fromCharCode(surrogate1, surrogate2)

0 commit comments

Comments
 (0)