Skip to content

Commit 968a31f

Browse files
authored
Merge pull request #400 from bsv-blockchain/fix/TOB-BSV-27
Fix/tob bsv 27
2 parents efdf8e7 + cebdfb6 commit 968a31f

File tree

6 files changed

+105
-31
lines changed

6 files changed

+105
-31
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +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.17 - 2025-12-01](#1917---2025-12-01)
9+
- [1.9.16 - 2025-12-01](#1916---2025-12-02)
810
- [1.9.15 - 2025-12-01](#1915---2025-12-01)
911
- [1.9.14 - 2025-12-01](#1914---2025-12-01)
1012
- [1.9.13 - 2025-12-01](#1913---2025-12-01)
@@ -185,6 +187,13 @@ All notable changes to this project will be documented in this file. The format
185187
### Fixed
186188

187189
### Security
190+
---
191+
192+
### [1.9.17] - 2025-12-01
193+
194+
### Added
195+
196+
- Added bound checking in the toUTF8 function to prevent buffer overflows.
188197

189198
---
190199

docs/reference/primitives.md

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6099,32 +6099,39 @@ toUTF8 = (arr: number[]): string => {
60996099
}
61006100
if (byte <= 127) {
61016101
result += String.fromCharCode(byte);
6102+
continue;
61026103
}
6103-
else if (byte >= 192 && byte <= 223) {
6104-
const byte2 = arr[i + 1];
6105-
skip = 1;
6104+
if (byte >= 192 && byte <= 223) {
6105+
const avail = arr.length - (i + 1);
6106+
const byte2 = avail >= 1 ? arr[i + 1] : 0;
6107+
skip = Math.min(1, avail);
61066108
const codePoint = ((byte & 31) << 6) | (byte2 & 63);
61076109
result += String.fromCharCode(codePoint);
6110+
continue;
61086111
}
6109-
else if (byte >= 224 && byte <= 239) {
6110-
const byte2 = arr[i + 1];
6111-
const byte3 = arr[i + 2];
6112-
skip = 2;
6112+
if (byte >= 224 && byte <= 239) {
6113+
const avail = arr.length - (i + 1);
6114+
const byte2 = avail >= 1 ? arr[i + 1] : 0;
6115+
const byte3 = avail >= 2 ? arr[i + 2] : 0;
6116+
skip = Math.min(2, avail);
61136117
const codePoint = ((byte & 15) << 12) | ((byte2 & 63) << 6) | (byte3 & 63);
61146118
result += String.fromCharCode(codePoint);
6119+
continue;
61156120
}
6116-
else if (byte >= 240 && byte <= 247) {
6117-
const byte2 = arr[i + 1];
6118-
const byte3 = arr[i + 2];
6119-
const byte4 = arr[i + 3];
6120-
skip = 3;
6121+
if (byte >= 240 && byte <= 247) {
6122+
const avail = arr.length - (i + 1);
6123+
const byte2 = avail >= 1 ? arr[i + 1] : 0;
6124+
const byte3 = avail >= 2 ? arr[i + 2] : 0;
6125+
const byte4 = avail >= 3 ? arr[i + 3] : 0;
6126+
skip = Math.min(3, avail);
61216127
const codePoint = ((byte & 7) << 18) |
61226128
((byte2 & 63) << 12) |
61236129
((byte3 & 63) << 6) |
61246130
(byte4 & 63);
61256131
const surrogate1 = 55296 + ((codePoint - 65536) >> 10);
61266132
const surrogate2 = 56320 + ((codePoint - 65536) & 1023);
61276133
result += String.fromCharCode(surrogate1, surrogate2);
6134+
continue;
61286135
}
61296136
}
61306137
return result;

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.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bsv/sdk",
3-
"version": "1.9.16",
3+
"version": "1.9.17",
44
"type": "module",
55
"description": "BSV Blockchain Software Development Kit",
66
"main": "dist/cjs/mod.js",

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: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,6 @@ export const toUTF8 = (arr: number[]): string => {
237237

238238
for (let i = 0; i < arr.length; i++) {
239239
const byte = arr[i]
240-
241240
// this byte is part of a multi-byte sequence, skip it
242241
// added to avoid modifying i within the loop which is considered unsafe.
243242
if (skip > 0) {
@@ -248,26 +247,41 @@ export const toUTF8 = (arr: number[]): string => {
248247
// 1-byte sequence (0xxxxxxx)
249248
if (byte <= 0x7f) {
250249
result += String.fromCharCode(byte)
251-
} else if (byte >= 0xc0 && byte <= 0xdf) {
252-
// 2-byte sequence (110xxxxx 10xxxxxx)
253-
const byte2 = arr[i + 1]
254-
skip = 1
250+
continue
251+
}
252+
253+
// 2-byte sequence (110xxxxx 10xxxxxx)
254+
if (byte >= 0xc0 && byte <= 0xdf) {
255+
const avail = arr.length - (i + 1)
256+
const byte2 = avail >= 1 ? arr[i + 1] : 0
257+
skip = Math.min(1, avail)
258+
255259
const codePoint = ((byte & 0x1f) << 6) | (byte2 & 0x3f)
256260
result += String.fromCharCode(codePoint)
257-
} else if (byte >= 0xe0 && byte <= 0xef) {
258-
// 3-byte sequence (1110xxxx 10xxxxxx 10xxxxxx)
259-
const byte2 = arr[i + 1]
260-
const byte3 = arr[i + 2]
261-
skip = 2
261+
continue
262+
}
263+
264+
// 3-byte sequence (1110xxxx 10xxxxxx 10xxxxxx)
265+
if (byte >= 0xe0 && byte <= 0xef) {
266+
const avail = arr.length - (i + 1)
267+
const byte2 = avail >= 1 ? arr[i + 1] : 0
268+
const byte3 = avail >= 2 ? arr[i + 2] : 0
269+
skip = Math.min(2, avail)
270+
262271
const codePoint =
263272
((byte & 0x0f) << 12) | ((byte2 & 0x3f) << 6) | (byte3 & 0x3f)
264273
result += String.fromCharCode(codePoint)
265-
} else if (byte >= 0xf0 && byte <= 0xf7) {
266-
// 4-byte sequence (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx)
267-
const byte2 = arr[i + 1]
268-
const byte3 = arr[i + 2]
269-
const byte4 = arr[i + 3]
270-
skip = 3
274+
continue
275+
}
276+
277+
// 4-byte sequence (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx)
278+
if (byte >= 0xf0 && byte <= 0xf7) {
279+
const avail = arr.length - (i + 1)
280+
const byte2 = avail >= 1 ? arr[i + 1] : 0
281+
const byte3 = avail >= 2 ? arr[i + 2] : 0
282+
const byte4 = avail >= 3 ? arr[i + 3] : 0
283+
skip = Math.min(3, avail)
284+
271285
const codePoint =
272286
((byte & 0x07) << 18) |
273287
((byte2 & 0x3f) << 12) |
@@ -278,6 +292,7 @@ export const toUTF8 = (arr: number[]): string => {
278292
const surrogate1 = 0xd800 + ((codePoint - 0x10000) >> 10)
279293
const surrogate2 = 0xdc00 + ((codePoint - 0x10000) & 0x3ff)
280294
result += String.fromCharCode(surrogate1, surrogate2)
295+
continue
281296
}
282297
}
283298

0 commit comments

Comments
 (0)