CVE-2023-32059 (Python, published 2023-05-11T22:15Z)
Description: value: Vyper is a Pythonic smart contract language for the Ethereum virtual machine. Prior to version 0.3.8, internal calls with default arguments are compiled incorrectly. Depending on the number of arguments provided in the call, the defaults are added not right-to-left, but left-to-right. If the types are incompatible, typechecking is bypassed. The ability to pass kwargs to internal functions is an undocumented feature that is not well known about. The issue is patched in version 0.3.8.
Severity: nan (CVSS v3 base score: 7.5)
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/vyperlang/vyper
Files changed:
tests/parser/features/test_internal_call.py
vyper/codegen/context.py
Commit message: Merge pull request from GHSA-ph9x-4vc9-m39g

the routine for aligning call-site posargs and kwargs in
`vyper.codegen.context.lookup_internal_function` was incorrect in cases
where the internal function had more than one default argument - it
consumed default args at the call site from the end instead of the
beginning of the defaults list. this commit fixes and adds some tests
for the alignment routine.

Vulnerable code:
        num_kwargs = len(sig.default_args)
        kwargs_needed = num_kwargs - num_provided_kwargs
        kw_vals = list(sig.default_values.values())[:kwargs_needed]

Fixed code:
import string
import hypothesis.strategies as st
from hypothesis import given, settings


# TODO probably want to refactor these into general test utils
st_uint256 = st.integers(min_value=0, max_value=2**256 - 1)
st_string65 = st.text(max_size=65, alphabet=string.printable)
st_bytes65 = st.binary(max_size=65)
st_sarray3 = st.lists(st_uint256, min_size=3, max_size=3)
st_darray3 = st.lists(st_uint256, max_size=3)

internal_call_kwargs_cases = [
    ("uint256", st_uint256),
    ("String[65]", st_string65),
    ("Bytes[65]", st_bytes65),
    ("uint256[3]", st_sarray3),
    ("DynArray[uint256, 3]", st_darray3),
]


@pytest.mark.parametrize("typ1,strategy1", internal_call_kwargs_cases)
@pytest.mark.parametrize("typ2,strategy2", internal_call_kwargs_cases)
def test_internal_call_kwargs(get_contract, typ1, strategy1, typ2, strategy2):
    # GHSA-ph9x-4vc9-m39g

    @given(kwarg1=strategy1, default1=strategy1, kwarg2=strategy2, default2=strategy2)
    @settings(deadline=None, max_examples=5)  # len(cases) * len(cases) * 5 * 5
    def fuzz(kwarg1, kwarg2, default1, default2):
        code = f"""
@internal
def foo(a: {typ1} = {repr(default1)}, b: {typ2} = {repr(default2)}) -> ({typ1}, {typ2}):
    return a, b

@external
def test0() -> ({typ1}, {typ2}):
    return self.foo()

@external
def test1() -> ({typ1}, {typ2}):
    return self.foo({repr(kwarg1)})

@external
def test2() -> ({typ1}, {typ2}):
    return self.foo({repr(kwarg1)}, {repr(kwarg2)})

@external
def test3(x1: {typ1}) -> ({typ1}, {typ2}):
    return self.foo(x1)

@external
def test4(x1: {typ1}, x2: {typ2}) -> ({typ1}, {typ2}):
    return self.foo(x1, x2)
        """
        c = get_contract(code)
        assert c.test0() == [default1, default2]
        assert c.test1() == [kwarg1, default2]
        assert c.test2() == [kwarg1, kwarg2]
        assert c.test3(kwarg1) == [kwarg1, default2]
        assert c.test4(kwarg1, kwarg2) == [kwarg1, kwarg2]

    fuzz()
        kw_vals = list(sig.default_values.values())[num_provided_kwargs:]