CVE-2023-32059: Insufficient Information (NVD-CWE-noinfo)
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.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 7.5
Published: 2023-05-11T22:15Z
Repository: https://github.com/vyperlang/vyper
Language: Python
Files changed: tests/parser/features/test_internal_call.py, vyper/codegen/context.py
Commit date: 2023-05-11 13:47:41 -0700
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.
Diff: diff --git a/tests/parser/features/test_internal_call.py b/tests/parser/features/test_internal_call.py
index f576dc5ee5..d7a41acbc0 100644
--- a/tests/parser/features/test_internal_call.py
+++ b/tests/parser/features/test_internal_call.py
@@ -1,8 +1,11 @@
+import string
 from decimal import Decimal
 
+import hypothesis.strategies as st
 import pytest
+from hypothesis import given, settings
 
 from vyper.compiler import compile_code
 from vyper.exceptions import ArgumentException, CallViolation
 
 pytestmark = pytest.mark.usefixtures("memory_mocker")
@@ -640,5 +643,64 @@ def bar() -> String[6]:
     """
 
     c = get_contract_with_gas_estimation(contract)
 
     assert c.bar() == "hello"
+
+
+# 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()
diff --git a/vyper/codegen/context.py b/vyper/codegen/context.py
index cc9f9744f0..9902cd0cf7 100644
--- a/vyper/codegen/context.py
+++ b/vyper/codegen/context.py
@@ -265,14 +265,12 @@ class Context:
         _check(len(sig.base_args) <= len(args_ir) <= len(sig.args))
         # more sanity check, that the types match
         # _check(all(l.typ == r.typ for (l, r) in zip(args_ir, sig.args))
 
         num_provided_kwargs = len(args_ir) - len(sig.base_args)
-        num_kwargs = len(sig.default_args)
-        kwargs_needed = num_kwargs - num_provided_kwargs
 
-        kw_vals = list(sig.default_values.values())[:kwargs_needed]
+        kw_vals = list(sig.default_values.values())[num_provided_kwargs:]
 
         return sig, kw_vals
 
     # Pretty print constancy for error messages
     def pp_constancy(self):

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:]