CVE-2024-30253 (TypeScript, published 2024-04-17T15:15Z)
Description: value: @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.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/solana-labs/solana-web3.js
Files changed:
packages/library-legacy/src/message/legacy.ts
packages/library-legacy/src/message/v0.ts
packages/library-legacy/src/transaction/legacy.ts
packages/library-legacy/src/transaction/versioned.ts
packages/library-legacy/src/utils/guarded-array-utils.ts
packages/library-legacy/src/validator-info.ts
packages/library-legacy/test/guarded-array-utils.test.ts
Commit message: fix: bounds check


Vulnerable code:
    const numRequiredSignatures = byteArray.shift()!;
    const numReadonlySignedAccounts = byteArray.shift()!;
    const numReadonlyUnsignedAccounts = byteArray.shift()!;
      const account = byteArray.splice(0, PUBLIC_KEY_LENGTH);
    const recentBlockhash = byteArray.splice(0, PUBLIC_KEY_LENGTH);
      const programIdIndex = byteArray.shift()!;
      const accounts = byteArray.splice(0, accountCount);
      const dataSlice = byteArray.splice(0, dataLength);
    const prefix = byteArray.shift() as number;
      numRequiredSignatures: byteArray.shift() as number,
      numReadonlySignedAccounts: byteArray.shift() as number,
      numReadonlyUnsignedAccounts: byteArray.shift() as number,
        new PublicKey(byteArray.splice(0, PUBLIC_KEY_LENGTH)),
    const recentBlockhash = bs58.encode(byteArray.splice(0, PUBLIC_KEY_LENGTH));
      const programIdIndex = byteArray.shift() as number;
      const accountKeyIndexes = byteArray.splice(0, accountKeyIndexesLength);
      const data = new Uint8Array(byteArray.splice(0, dataLength));
      const accountKey = new PublicKey(byteArray.splice(0, PUBLIC_KEY_LENGTH));
      const writableIndexes = byteArray.splice(0, writableIndexesLength);
      const readonlyIndexes = byteArray.splice(0, readonlyIndexesLength);
      const signature = byteArray.splice(0, SIGNATURE_LENGTH_IN_BYTES);
        new Uint8Array(byteArray.splice(0, SIGNATURE_LENGTH_IN_BYTES)),
      const publicKey = new PublicKey(byteArray.splice(0, PUBLIC_KEY_LENGTH));
      const isSigner = byteArray.splice(0, 1)[0] === 1;

Fixed code:
import {guardedShift, guardedSplice} from '../utils/guarded-array-utils';
    const numRequiredSignatures = guardedShift(byteArray);
    const numReadonlySignedAccounts = guardedShift(byteArray);
    const numReadonlyUnsignedAccounts = guardedShift(byteArray);
      const account = guardedSplice(byteArray, 0, PUBLIC_KEY_LENGTH);
    const recentBlockhash = guardedSplice(byteArray, 0, PUBLIC_KEY_LENGTH);
      const programIdIndex = guardedShift(byteArray);
      const accounts = guardedSplice(byteArray, 0, accountCount);
      const dataSlice = guardedSplice(byteArray, 0, dataLength);
import {guardedShift, guardedSplice} from '../utils/guarded-array-utils';
    const prefix = guardedShift(byteArray);
      numRequiredSignatures: guardedShift(byteArray),
      numReadonlySignedAccounts: guardedShift(byteArray),
      numReadonlyUnsignedAccounts: guardedShift(byteArray),
        new PublicKey(guardedSplice(byteArray, 0, PUBLIC_KEY_LENGTH)),
    const recentBlockhash = bs58.encode(
      guardedSplice(byteArray, 0, PUBLIC_KEY_LENGTH),
    );
      const programIdIndex = guardedShift(byteArray);
      const accountKeyIndexes = guardedSplice(
        byteArray,
        0,
        accountKeyIndexesLength,
      );
      const data = new Uint8Array(guardedSplice(byteArray, 0, dataLength));
      const accountKey = new PublicKey(
        guardedSplice(byteArray, 0, PUBLIC_KEY_LENGTH),
      );
      const writableIndexes = guardedSplice(
        byteArray,
        0,
        writableIndexesLength,
      );
      const readonlyIndexes = guardedSplice(
        byteArray,
        0,
        readonlyIndexesLength,
      );
import {guardedSplice} from '../utils/guarded-array-utils';
      const signature = guardedSplice(byteArray, 0, SIGNATURE_LENGTH_IN_BYTES);
import {guardedSplice} from '../utils/guarded-array-utils';
        new Uint8Array(guardedSplice(byteArray, 0, SIGNATURE_LENGTH_IN_BYTES)),
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>),
  );
}
import {guardedShift, guardedSplice} from './utils/guarded-array-utils';
      const publicKey = new PublicKey(
        guardedSplice(byteArray, 0, PUBLIC_KEY_LENGTH),
      );
      const isSigner = guardedShift(byteArray) === 1;
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();
  });
});