Skip to content

[AI Security] Medium: Oversized integer ABI inputs wrap instead of rejecting in Spec.funcArgsToScVals() #1358

@sagpatil

Description

@sagpatil

Security Finding: Contract ABI integer inputs wrap silently instead of rejecting out-of-range values

Subsystem: contract
Severity: Medium
Model: claude-sonnet-4.6, default


Root Cause

contract.Spec.funcArgsToScVals() forwards caller-supplied arguments directly into nativeToScVal() without enforcing the declared integer range. For u64 / i64 / u128 / i128 / u256 / i256 / Timepoint / Duration, the SDK then delegates to XdrLargeInt or xdr.Uint64, which encode fixed-width values after truncating them to the target width instead of rejecting oversized inputs.

The high-level contract client path inherits this behavior unchanged: generated client methods call spec.funcArgsToScVals(method, args) before assembly, so an out-of-range value supplied by the application is silently transformed into a different on-chain argument.

Attack Vector

An attacker who can influence a dapp or backend's contract-call parameters can submit oversized integer values that look materially different from the value eventually signed and sent. For example, 2^64 encodes as 0 for u64 / Timepoint, 2^128 encodes as 0 for u128, and 2^127 encodes as INT_MIN for i128.

This is realistic anywhere the SDK is used as the ABI-validation boundary for untrusted user input, API payloads, or cross-service messages. The issue does not require a malicious RPC server.

PoC Summary

Confirmed with a minimal one-argument contract spec and a Vitest PoC. The test calls spec.funcArgsToScVals() with out-of-range string and bigint inputs, confirms the call does not throw, and then decodes the emitted ScVal back to the wrapped value.

The PoC passed for all tested cases:

  • u64: 18446744073709551616 / 1n << 64n0n
  • Timepoint: 18446744073709551616 / 1n << 64n0n
  • u128: 340282366920938463463374607431768211456 / 1n << 128n0n
  • i128: 170141183460469231731687303715884105728 / 1n << 127n-2^127

PoC Code

import { describe, expect, it } from "vitest";
import { scValToBigInt } from "@stellar/stellar-base";

import {
  StellarSdk,
  type ScSpecTypeDef,
} from "../test-utils/stellar-sdk-import";

const { contract, xdr } = StellarSdk;

function makeSingleArgSpec(methodName: string, typeDef: ScSpecTypeDef) {
  const entry = xdr.ScSpecEntry.scSpecEntryFunctionV0(
    new xdr.ScSpecFunctionV0({
      doc: "",
      name: methodName,
      inputs: [
        new xdr.ScSpecFunctionInputV0({
          doc: "",
          name: "value",
          type: typeDef,
        }),
      ],
      outputs: [typeDef],
    }),
  );

  return new contract.Spec([entry]);
}

describe("Security PoC - contract ABI integer wraparound", () => {
  it("encodes out-of-range integer inputs as wrapped values instead of rejecting them", () => {
    const intMinI128 = -(1n << 127n);
    const cases = [
      {
        label: "u64 string input",
        methodName: "wrap_u64_string",
        typeDef: xdr.ScSpecTypeDef.scSpecTypeU64(),
        input: "18446744073709551616",
        expected: 0n,
      },
      {
        label: "u64 bigint input",
        methodName: "wrap_u64_bigint",
        typeDef: xdr.ScSpecTypeDef.scSpecTypeU64(),
        input: 1n << 64n,
        expected: 0n,
      },
      {
        label: "timepoint string input",
        methodName: "wrap_timepoint_string",
        typeDef: xdr.ScSpecTypeDef.scSpecTypeTimepoint(),
        input: "18446744073709551616",
        expected: 0n,
      },
      {
        label: "timepoint bigint input",
        methodName: "wrap_timepoint_bigint",
        typeDef: xdr.ScSpecTypeDef.scSpecTypeTimepoint(),
        input: 1n << 64n,
        expected: 0n,
      },
      {
        label: "u128 string input",
        methodName: "wrap_u128_string",
        typeDef: xdr.ScSpecTypeDef.scSpecTypeU128(),
        input: "340282366920938463463374607431768211456",
        expected: 0n,
      },
      {
        label: "u128 bigint input",
        methodName: "wrap_u128_bigint",
        typeDef: xdr.ScSpecTypeDef.scSpecTypeU128(),
        input: 1n << 128n,
        expected: 0n,
      },
      {
        label: "i128 string input",
        methodName: "wrap_i128_string",
        typeDef: xdr.ScSpecTypeDef.scSpecTypeI128(),
        input: "170141183460469231731687303715884105728",
        expected: intMinI128,
      },
      {
        label: "i128 bigint input",
        methodName: "wrap_i128_bigint",
        typeDef: xdr.ScSpecTypeDef.scSpecTypeI128(),
        input: 1n << 127n,
        expected: intMinI128,
      },
    ];

    for (const testCase of cases) {
      const spec = makeSingleArgSpec(testCase.methodName, testCase.typeDef);

      let scVals: ReturnType<typeof spec.funcArgsToScVals> | undefined;
      expect(() => {
        scVals = spec.funcArgsToScVals(testCase.methodName, {
          value: testCase.input,
        });
      }, testCase.label).not.toThrow();

      expect(scVals, testCase.label).toHaveLength(1);

      expect(scValToBigInt(scVal), testCase.label).toBe(testCase.expected);
      expect(spec.scValToNative<bigint>(scVal, testCase.typeDef)).toBe(
        testCase.expected,
      );
    }
  });
});

Recommendation

Add explicit min/max validation in Spec.nativeToScVal() and stringToScVal() for every fixed-width integer-like contract type before delegating to XDR helpers. Reject out-of-range string, number, and bigint inputs with a RangeError, and tighten jsonSchema() so its unsigned/signed integer definitions express the true numeric bounds instead of only digit-count limits.


This issue was generated by an automated AI security analysis pipeline.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ai-generatedGenerated by AI security analysis pipelinesecuritySecurity vulnerability or concern

    Type

    No type

    Projects

    Status

    Backlog (Not Ready)

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions