VYPR
Moderate severityNVD Advisory· Published Aug 1, 2022· Updated Apr 23, 2025

Weakness in Transfer Validation Logic in @solana/pay

CVE-2022-35917

Description

Solana Pay is a protocol and set of reference implementations that enable developers to incorporate decentralized payments into their apps and services. When a Solana Pay transaction is located using a reference key, it may be checked to represent a transfer of the desired amount to the recipient, using the supplied validateTransfer function. An edge case regarding this mechanism could cause the validation logic to validate multiple transfers. This issue has been patched as of version 0.2.1. Users of the Solana Pay SDK should upgrade to it. There are no known workarounds for this issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@solana/paynpm
< 0.2.10.2.1

Affected products

1

Patches

1
ac6ce0d0a811

finish transfer validation implementation

https://github.com/solana-labs/solana-payJordan SextonJul 28, 2022via ghsa
1 file changed · +60 18
  • core/src/validateTransfer.ts+60 18 modified
    @@ -1,15 +1,23 @@
    -import { getAssociatedTokenAddress } from '@solana/spl-token';
    +import {
    +    decodeInstruction,
    +    getAssociatedTokenAddress,
    +    isTransferCheckedInstruction,
    +    isTransferInstruction,
    +} from '@solana/spl-token';
     import {
         ConfirmedTransactionMeta,
         Connection,
         Finality,
         LAMPORTS_PER_SOL,
         Message,
    +    SystemInstruction,
    +    Transaction,
         TransactionResponse,
         TransactionSignature,
     } from '@solana/web3.js';
     import BigNumber from 'bignumber.js';
    -import { Amount, Memo, Recipient, References, SPLToken } from './types';
    +import { MEMO_PROGRAM_ID } from './constants';
    +import { Amount, Memo, Recipient, Reference, References, SPLToken } from './types';
     
     /**
      * Thrown when a transaction doesn't contain a valid Solana Pay transfer.
    @@ -58,33 +66,49 @@ export async function validateTransfer(
         if (!meta) throw new ValidateTransferError('missing meta');
         if (meta.err) throw meta.err;
     
    -    const [preAmount, postAmount] = splToken
    -        ? await validateSPLTokenTransfer(message, meta, recipient, splToken)
    -        : await validateSystemTransfer(message, meta, recipient);
    +    if (reference && !Array.isArray(reference)) {
    +        reference = [reference];
    +    }
     
    +    const [preAmount, postAmount] = splToken
    +        ? await validateSPLTokenTransfer(message, meta, recipient, splToken, reference)
    +        : await validateSystemTransfer(message, meta, recipient, reference);
         if (postAmount.minus(preAmount).lt(amount)) throw new ValidateTransferError('amount not transferred');
     
    -    if (reference) {
    -        if (!Array.isArray(reference)) {
    -            reference = [reference];
    -        }
    -
    -        for (const pubkey of reference) {
    -            if (!message.accountKeys.some((accountKey) => accountKey.equals(pubkey)))
    -                throw new ValidateTransferError('reference not found');
    -        }
    +    if (memo) {
    +        // Check that the second instruction is a memo instruction with the expected memo.
    +        const transaction = Transaction.populate(message);
    +        const instruction = transaction.instructions[1];
    +        if (!instruction) throw new ValidateTransferError('missing memo instruction');
    +        if (!instruction.programId.equals(MEMO_PROGRAM_ID)) throw new ValidateTransferError('invalid memo program');
    +        if (!instruction.data.equals(Buffer.from(memo, 'utf8'))) throw new ValidateTransferError('invalid memo');
         }
     
    -    // FIXME: add memo check
    -
         return response;
     }
     
     async function validateSystemTransfer(
         message: Message,
         meta: ConfirmedTransactionMeta,
    -    recipient: Recipient
    +    recipient: Recipient,
    +    references?: Reference[]
     ): Promise<[BigNumber, BigNumber]> {
    +    if (references) {
    +        // Check that the first instruction is a system transfer instruction.
    +        const transaction = Transaction.populate(message);
    +        const instruction = transaction.instructions[0];
    +        SystemInstruction.decodeTransfer(instruction);
    +
    +        // Check that the expected reference keys exactly match the extra keys provided to the instruction.
    +        const [_from, _to, ...extraKeys] = instruction.keys;
    +        const length = extraKeys.length;
    +        if (length !== references.length) throw new ValidateTransferError('invalid references');
    +
    +        for (let i = 0; i < length; i++) {
    +            if (!extraKeys[i].pubkey.equals(references[i])) throw new ValidateTransferError(`invalid reference ${i}`);
    +        }
    +    }
    +
         const accountIndex = message.accountKeys.findIndex((pubkey) => pubkey.equals(recipient));
         if (accountIndex === -1) throw new ValidateTransferError('recipient not found');
     
    @@ -98,8 +122,26 @@ async function validateSPLTokenTransfer(
         message: Message,
         meta: ConfirmedTransactionMeta,
         recipient: Recipient,
    -    splToken: SPLToken
    +    splToken: SPLToken,
    +    references?: Reference[]
     ): Promise<[BigNumber, BigNumber]> {
    +    if (references) {
    +        // Check that the first instruction is an SPL token transfer instruction.
    +        const transaction = Transaction.populate(message);
    +        const instruction = decodeInstruction(transaction.instructions[0]);
    +        if (!isTransferCheckedInstruction(instruction) && !isTransferInstruction(instruction))
    +            throw new ValidateTransferError('invalid transfer');
    +
    +        // Check that the expected reference keys exactly match the extra keys provided to the instruction.
    +        const extraKeys = instruction.keys.multiSigners;
    +        const length = extraKeys.length;
    +        if (length !== references.length) throw new ValidateTransferError('invalid references');
    +
    +        for (let i = 0; i < length; i++) {
    +            if (!extraKeys[i].pubkey.equals(references[i])) throw new ValidateTransferError(`invalid reference ${i}`);
    +        }
    +    }
    +
         const recipientATA = await getAssociatedTokenAddress(splToken, recipient);
         const accountIndex = message.accountKeys.findIndex((pubkey) => pubkey.equals(recipientATA));
         if (accountIndex === -1) throw new ValidateTransferError('recipient not found');
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.