CVE-2024-30253 (published 2024-04-17T15:15Z)

@solana/web3.js is the Solana JavaScript SDK. Using particular inputs with `@solana/web3.js` will result in memory exhaustion (OOM). If you have a server, client, mobile, or desktop product that accepts untrusted input for use with `@solana/web3.js`, your application/service may crash, resulting in a loss of availability. This vulnerability is fixed in 1.0.1, 1.10.2, 1.11.1, 1.12.1, 1.1.2, 1.13.1, 1.14.1, 1.15.1, 1.16.2, 1.17.1, 1.18.1, 1.19.1, 1.20.3, 1.21.1, 1.22.1, 1.23.1, 1.24.3, 1.25.1, 1.26.1, 1.27.1, 1.28.1, 1.2.8, 1.29.4, 1.30.3, 1.31.1, 1.3.1, 1.32.3, 1.33.1, 1.34.1, 1.35.2, 1.36.1, 1.37.3, 1.38.1, 1.39.2, 1.40.2, 1.41.11, 1.4.1, 1.42.1, 1.43.7, 1.44.4, 1.45.1, 1.46.1, 1.47.5, 1.48.1, 1.49.1, 1.50.2, 1.51.1, 1.5.1, 1.52.1, 1.53.1, 1.54.2, 1.55.1, 1.56.3, 1.57.1, 1.58.1, 1.59.2, 1.60.1, 1.61.2, 1.6.1, 1.62.2, 1.63.2, 1.64.1, 1.65.1, 1.66.6, 1.67.3, 1.68.2, 1.69.1, 1.70.4, 1.71.1, 1.72.1, 1.7.2, 1.73.5, 1.74.1, 1.75.1, 1.76.1, 1.77.4, 1.78.8, 1.79.1, 1.80.1, 1.81.1, 1.8.1, 1.82.1, 1.83.1, 1.84.1, 1.85.1, 1.86.1, 1.87.7, 1.88.1, 1.89.2, 1.90.2, 1.9.2, and 1.91.3.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/solana-labs/solana-web3.js
commit_details: repo_url: https://github.com/solana-labs/solana-web3.js
author: steveluscher
author_date: 2024-04-01 14:33:05+00:00
msg: fix: bounds check
repository: repo_url: https://github.com/solana-labs/solana-web3.js
repo_name: solana-labs/solana-web3.js
description: Solana JavaScript SDK
repo_language: TypeScript
owner: solana-labs
file_changes: filename: legacy.ts
change_type: ModificationType.MODIFY
diff: @@ -16,6 +16,7 @@ import {
 import {TransactionInstruction} from '../transaction';
 import {CompiledKeys} from './compiled-keys';
 import {MessageAccountKeys} from './account-keys';
+import {guardedShift, guardedSplice} from '../utils/guarded-array-utils';
 
 /**
  * An instruction to execute by a program
@@ -268,7 +269,7 @@ export class Message {
     // Slice up wire data
     let byteArray = [...buffer];
 
-    const numRequiredSignatures = byteArray.shift()!;
+    const numRequiredSignatures = guardedShift(byteArray);
     if (
       numRequiredSignatures !==
       (numRequiredSignatures & VERSION_PREFIX_MASK)
@@ -278,26 +279,26 @@ export class Message {
       );
     }
 
-    const numReadonlySignedAccounts = byteArray.shift()!;
-    const numReadonlyUnsignedAccounts = byteArray.shift()!;
+    const numReadonlySignedAccounts = guardedShift(byteArray);
+    const numReadonlyUnsignedAccounts = guardedShift(byteArray);
 
     const accountCount = shortvec.decodeLength(byteArray);
     let accountKeys = [];
     for (let i = 0; i < accountCount; i++) {
-      const account = byteArray.splice(0, PUBLIC_KEY_LENGTH);
+      const account = guardedSplice(byteArray, 0, PUBLIC_KEY_LENGTH);
       accountKeys.push(new PublicKey(Buffer.from(account)));
     }
 
-    const recentBlockhash = byteArray.splice(0, PUBLIC_KEY_LENGTH);
+    const recentBlockhash = guardedSplice(byteArray, 0, PUBLIC_KEY_LENGTH);
 
     const instructionCount = shortvec.decodeLength(byteArray);
     let instructions: CompiledInstruction[] = [];
     for (let i = 0; i < instructionCount; i++) {
-      const programIdIndex = byteArray.shift()!;
+      const programIdIndex = guardedShift(byteArray);
       const accountCount = shortvec.decodeLength(byteArray);
-      const accounts = byteArray.splice(0, accountCount);
+      const accounts = guardedSplice(byteArray, 0, accountCount);
       const dataLength = shortvec.decodeLength(byteArray);
-      const dataSlice = byteArray.splice(0, dataLength);
+      const dataSlice = guardedSplice(byteArray, 0, dataLength);
       const data = bs58.encode(Buffer.from(dataSlice));
       instructions.push({
         programIdIndex,

programming_language: TypeScript
filename: v0.ts
change_type: ModificationType.MODIFY
diff: @@ -16,6 +16,7 @@ import {TransactionInstruction} from '../transaction';
 import {AddressLookupTableAccount} from '../programs';
 import {CompiledKeys} from './compiled-keys';
 import {AccountKeysFromLookups, MessageAccountKeys} from './account-keys';
+import {guardedShift, guardedSplice} from '../utils/guarded-array-utils';
 
 /**
  * Message constructor arguments
@@ -426,7 +427,7 @@ export class MessageV0 {
   static deserialize(serializedMessage: Uint8Array): MessageV0 {
     let byteArray = [...serializedMessage];
 
-    const prefix = byteArray.shift() as number;
+    const prefix = guardedShift(byteArray);
     const maskedPrefix = prefix & VERSION_PREFIX_MASK;
     assert(
       prefix !== maskedPrefix,
@@ -440,29 +441,35 @@ export class MessageV0 {
     );
 
     const header: MessageHeader = {
-      numRequiredSignatures: byteArray.shift() as number,
-      numReadonlySignedAccounts: byteArray.shift() as number,
-      numReadonlyUnsignedAccounts: byteArray.shift() as number,
+      numRequiredSignatures: guardedShift(byteArray),
+      numReadonlySignedAccounts: guardedShift(byteArray),
+      numReadonlyUnsignedAccounts: guardedShift(byteArray),
     };
 
     const staticAccountKeys = [];
     const staticAccountKeysLength = shortvec.decodeLength(byteArray);
     for (let i = 0; i < staticAccountKeysLength; i++) {
       staticAccountKeys.push(
-        new PublicKey(byteArray.splice(0, PUBLIC_KEY_LENGTH)),
+        new PublicKey(guardedSplice(byteArray, 0, PUBLIC_KEY_LENGTH)),
       );
     }
 
-    const recentBlockhash = bs58.encode(byteArray.splice(0, PUBLIC_KEY_LENGTH));
+    const recentBlockhash = bs58.encode(
+      guardedSplice(byteArray, 0, PUBLIC_KEY_LENGTH),
+    );
 
     const instructionCount = shortvec.decodeLength(byteArray);
     const compiledInstructions: MessageCompiledInstruction[] = [];
     for (let i = 0; i < instructionCount; i++) {
-      const programIdIndex = byteArray.shift() as number;
+      const programIdIndex = guardedShift(byteArray);
       const accountKeyIndexesLength = shortvec.decodeLength(byteArray);
-      const accountKeyIndexes = byteArray.splice(0, accountKeyIndexesLength);
+      const accountKeyIndexes = guardedSplice(
+        byteArray,
+        0,
+        accountKeyIndexesLength,
+      );
       const dataLength = shortvec.decodeLength(byteArray);
-      const data = new Uint8Array(byteArray.splice(0, dataLength));
+      const data = new Uint8Array(guardedSplice(byteArray, 0, dataLength));
       compiledInstructions.push({
         programIdIndex,
         accountKeyIndexes,
@@ -473,11 +480,21 @@ export class MessageV0 {
     const addressTableLookupsCount = shortvec.decodeLength(byteArray);
     const addressTableLookups: MessageAddressTableLookup[] = [];
     for (let i = 0; i < addressTableLookupsCount; i++) {
-      const accountKey = new PublicKey(byteArray.splice(0, PUBLIC_KEY_LENGTH));
+      const accountKey = new PublicKey(
+        guardedSplice(byteArray, 0, PUBLIC_KEY_LENGTH),
+      );
       const writableIndexesLength = shortvec.decodeLength(byteArray);
-      const writableIndexes = byteArray.splice(0, writableIndexesLength);
+      const writableIndexes = guardedSplice(
+        byteArray,
+        0,
+        writableIndexesLength,
+      );
       const readonlyIndexesLength = shortvec.decodeLength(byteArray);
-      const readonlyIndexes = byteArray.splice(0, readonlyIndexesLength);
+      const readonlyIndexes = guardedSplice(
+        byteArray,
+        0,
+        readonlyIndexesLength,
+      );
       addressTableLookups.push({
         accountKey,
         writableIndexes,

programming_language: TypeScript
filename: legacy.ts
change_type: ModificationType.MODIFY
diff: @@ -12,6 +12,7 @@ import type {Signer} from '../keypair';
 import type {Blockhash} from '../blockhash';
 import type {CompiledInstruction} from '../message';
 import {sign, verify} from '../utils/ed25519';
+import {guardedSplice} from '../utils/guarded-array-utils';
 
 /** @internal */
 type MessageSignednessErrors = {
@@ -904,7 +905,7 @@ export class Transaction {
     const signatureCount = shortvec.decodeLength(byteArray);
     let signatures = [];
     for (let i = 0; i < signatureCount; i++) {
-      const signature = byteArray.splice(0, SIGNATURE_LENGTH_IN_BYTES);
+      const signature = guardedSplice(byteArray, 0, SIGNATURE_LENGTH_IN_BYTES);
       signatures.push(bs58.encode(Buffer.from(signature)));
     }
 

programming_language: TypeScript
filename: versioned.ts
change_type: ModificationType.MODIFY
diff: @@ -8,6 +8,7 @@ import * as shortvec from '../utils/shortvec-encoding';
 import * as Layout from '../layout';
 import {sign} from '../utils/ed25519';
 import {PublicKey} from '../publickey';
+import {guardedSplice} from '../utils/guarded-array-utils';
 
 export type TransactionVersion = 'legacy' | 0;
 
@@ -82,7 +83,7 @@ export class VersionedTransaction {
     const signaturesLength = shortvec.decodeLength(byteArray);
     for (let i = 0; i < signaturesLength; i++) {
       signatures.push(
-        new Uint8Array(byteArray.splice(0, SIGNATURE_LENGTH_IN_BYTES)),
+        new Uint8Array(guardedSplice(byteArray, 0, SIGNATURE_LENGTH_IN_BYTES)),
       );
     }
 

programming_language: TypeScript
filename: guarded-array-utils.ts
change_type: ModificationType.ADD
diff: @@ -0,0 +1,34 @@
+const END_OF_BUFFER_ERROR_MESSAGE = 'Reached end of buffer unexpectedly';
+
+/**
+ * Delegates to `Array#shift`, but throws if the array is zero-length.
+ */
+export function guardedShift<T>(byteArray: T[]): T {
+  if (byteArray.length === 0) {
+    throw new Error(END_OF_BUFFER_ERROR_MESSAGE);
+  }
+  return byteArray.shift() as T;
+}
+
+/**
+ * Delegates to `Array#splice`, but throws if the section being spliced out extends past the end of
+ * the array.
+ */
+export function guardedSplice<T>(
+  byteArray: T[],
+  ...args:
+    | [start: number, deleteCount?: number]
+    | [start: number, deleteCount: number, ...items: T[]]
+): T[] {
+  const [start] = args;
+  if (
+    args.length === 2 // Implies that `deleteCount` was supplied
+      ? start + (args[1] ?? 0) > byteArray.length
+      : start >= byteArray.length
+  ) {
+    throw new Error(END_OF_BUFFER_ERROR_MESSAGE);
+  }
+  return byteArray.splice(
+    ...(args as Parameters<typeof Array.prototype.splice>),
+  );
+}

programming_language: TypeScript
filename: validator-info.ts
change_type: ModificationType.MODIFY
diff: @@ -9,6 +9,7 @@ import {
 import * as Layout from './layout';
 import * as shortvec from './utils/shortvec-encoding';
 import {PublicKey, PUBLIC_KEY_LENGTH} from './publickey';
+import {guardedShift, guardedSplice} from './utils/guarded-array-utils';
 
 export const VALIDATOR_INFO_KEY = new PublicKey(
   'Va1idator1nfo111111111111111111111111111111',
@@ -83,8 +84,10 @@ export class ValidatorInfo {
 
     const configKeys: Array<ConfigKey> = [];
     for (let i = 0; i < 2; i++) {
-      const publicKey = new PublicKey(byteArray.splice(0, PUBLIC_KEY_LENGTH));
-      const isSigner = byteArray.splice(0, 1)[0] === 1;
+      const publicKey = new PublicKey(
+        guardedSplice(byteArray, 0, PUBLIC_KEY_LENGTH),
+      );
+      const isSigner = guardedShift(byteArray) === 1;
       configKeys.push({publicKey, isSigner});
     }
 

programming_language: TypeScript
filename: guarded-array-utils.test.ts
change_type: ModificationType.ADD
diff: @@ -0,0 +1,52 @@
+import {expect} from 'chai';
+import {spy} from 'sinon';
+
+import {guardedShift, guardedSplice} from '../src/utils/guarded-array-utils';
+
+describe('guardedShift', () => {
+  it('delegates to Array#shift', () => {
+    const arr = [1, 2, 3];
+    const shiftSpy = spy(arr, 'shift');
+    const result = guardedShift(arr);
+    expect(shiftSpy).is.calledWithExactly();
+    expect(result).to.eq(shiftSpy.returnValues[0]);
+  });
+  it('throws when the array is zero-length', () => {
+    const arr: number[] = [];
+    expect(() => guardedShift(arr)).to.throw();
+  });
+});
+
+describe('guardedSplice', () => {
+  it('delegates to Array#splice', () => {
+    const arr = [1, 2, 3];
+    const spliceSpy = spy(arr, 'splice');
+    const result = guardedSplice(
+      arr,
+      /* start */ 0,
+      /* deleteCount */ 3,
+      /* ...items */
+      100,
+      101,
+      102,
+    );
+    expect(spliceSpy).is.calledWithExactly(0, 3, 100, 101, 102);
+    expect(result).to.eq(spliceSpy.returnValues[0]);
+  });
+  it('allows zero-length splices', () => {
+    const arr: number[] = [1, 2, 3];
+    expect(guardedSplice(arr, 0, 0)).to.be.an.empty('array');
+  });
+  it('allows zero-length splices via the `deleteCount` argument being the explicit value `undefined`', () => {
+    const arr: number[] = [1, 2, 3];
+    expect(guardedSplice(arr, 0, undefined)).to.be.an.empty('array');
+  });
+  it('throws when the `start` would take you past the end of the array', () => {
+    const arr: number[] = [1, 2, 3];
+    expect(() => guardedSplice(arr, 3)).to.throw();
+  });
+  it('throws when the `deleteCount` and `start` would take you past the end of the array', () => {
+    const arr: number[] = [1, 2, 3];
+    expect(() => guardedSplice(arr, 1, 3)).to.throw();
+  });
+});

programming_language: TypeScript