CVE-2023-32059 (published 2023-05-11T22:15Z)

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.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N)

Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/vyperlang/vyper
commit_details: repo_url: https://github.com/vyperlang/vyper
author: Charles Cooper
author_date: 2023-05-11 13:47:41-07:00
msg: 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.
repository: repo_url: https://github.com/vyperlang/vyper
repo_name: vyperlang/vyper
description: Pythonic Smart Contract Language for the EVM
repo_language: Python
owner: vyperlang
file_changes: filename: test_internal_call.py
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,9 @@
+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
@@ -642,3 +645,62 @@ 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()

programming_language: Python
filename: context.py
change_type: ModificationType.MODIFY
diff: @@ -267,10 +267,8 @@ class Context:
         # _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
 

programming_language: Python