CVE-2023-42441 (Python, published 2023-09-18T21:16Z)
Description: value: Vyper is a Pythonic Smart Contract Language for the Ethereum Virtual Machine (EVM). Starting in version 0.2.9 and prior to version 0.3.10, locks of the type `@nonreentrant("")` or `@nonreentrant('')` do not produce reentrancy checks at runtime. This issue is fixed in version 0.3.10. As a workaround, ensure the lock name is a non-empty string.
Severity: nan (CVSS v3 base score: 5.3)
Weakness: CWE-667 \u2014 Improper Locking
The product does not properly acquire or release a lock on a resource, leading to unexpected resource state changes and behaviors.
Repository: https://github.com/vyperlang/vyper
Files changed:
tests/parser/exceptions/test_structure_exception.py
tests/parser/features/decorators/test_nonreentrant.py
tests/parser/test_call_graph_stability.py
tests/parser/types/test_identifier_naming.py
vyper/ast/identifiers.py
vyper/exceptions.py
vyper/semantics/namespace.py
vyper/semantics/types/base.py
vyper/semantics/types/function.py
Commit message: fix: only allow valid identifiers to be nonreentrant keys (#3605)

disallow invalid identifiers like `" "`, `"123abc"` from being keys for
non-reentrant locks.

this commit also refactors the `validate_identifiers` helper function to
be in the `ast/` subdirectory, and slightly improves the VyperException
constructor by allowing None (optional) annotations.

Vulnerable code:
@nonreentrant("B")
@nonreentrant("C")
def double_nonreentrant():
@nonreentrant('default')
@nonreentrant('default')
from vyper.semantics.namespace import RESERVED_KEYWORDS
from vyper.semantics.namespace import RESERVED_KEYWORDS
            self.annotations = items
import re

from vyper.exceptions import (
    CompilerPanic,
    NamespaceCollision,
    StructureException,
    UndeclaredDefinition,
)


def validate_identifier(attr):
    if not re.match("^[_a-zA-Z][a-zA-Z0-9_]*$", attr):
        raise StructureException(f"'{attr}' contains invalid character(s)")
    if attr.lower() in RESERVED_KEYWORDS:
        raise StructureException(f"'{attr}' is a reserved keyword")


# https://docs.python.org/3/reference/lexical_analysis.html#keywords
# note we don't technically need to block all python reserved keywords,
# but do it for hygiene
_PYTHON_RESERVED_KEYWORDS = {
    "False",
    "None",
    "True",
    "and",
    "as",
    "assert",
    "async",
    "await",
    "break",
    "class",
    "continue",
    "def",
    "del",
    "elif",
    "else",
    "except",
    "finally",
    "for",
    "from",
    "global",
    "if",
    "import",
    "in",
    "is",
    "lambda",
    "nonlocal",
    "not",
    "or",
    "pass",
    "raise",
    "return",
    "try",
    "while",
    "with",
    "yield",
}
_PYTHON_RESERVED_KEYWORDS = {s.lower() for s in _PYTHON_RESERVED_KEYWORDS}

# Cannot be used for variable or member naming
RESERVED_KEYWORDS = _PYTHON_RESERVED_KEYWORDS | {
    # decorators
    "public",
    "external",
    "nonpayable",
    "constant",
    "immutable",
    "transient",
    "internal",
    "payable",
    "nonreentrant",
    # "class" keywords
    "interface",
    "struct",
    "event",
    "enum",
    # EVM operations
    "unreachable",
    # special functions (no name mangling)
    "init",
    "_init_",
    "___init___",
    "____init____",
    "default",
    "_default_",
    "___default___",
    "____default____",
    # more control flow and special operations
    "range",
    # more special operations
    "indexed",
    # denominations
    "ether",
    "wei",
    "finney",
    "szabo",
    "shannon",
    "lovelace",
    "ada",
    "babbage",
    "gwei",
    "kwei",
    "mwei",
    "twei",
    "pwei",
    # sentinal constant values
    # TODO remove when these are removed from the language
    "zero_address",
    "empty_bytes32",
    "max_int128",
    "min_int128",
    "max_decimal",
    "min_decimal",
    "max_uint256",
    "zero_wei",
}
from vyper.semantics.namespace import validate_identifier
                    kwargs["nonreentrant"] = decorator.args[0].value

Fixed code:
@nonreentrant(" ")
def invalid_nonreentrant_key():
    pass
    """,
    """
@external
@nonreentrant("")
def invalid_nonreentrant_key():
    pass
    """,
    """
@external
@nonreentrant("123")
def invalid_nonreentrant_key():
    pass
    """,
    """
@external
@nonreentrant("!123abcd")
def invalid_nonreentrant_key():
@nonreentrant("lock")
@nonreentrant("lock")
from vyper.ast.identifiers import RESERVED_KEYWORDS
from vyper.ast.identifiers import RESERVED_KEYWORDS
import re

from vyper.exceptions import StructureException


def validate_identifier(attr, ast_node=None):
    if not re.match("^[_a-zA-Z][a-zA-Z0-9_]*$", attr):
        raise StructureException(f"'{attr}' contains invalid character(s)", ast_node)
    if attr.lower() in RESERVED_KEYWORDS:
        raise StructureException(f"'{attr}' is a reserved keyword", ast_node)


# https://docs.python.org/3/reference/lexical_analysis.html#keywords
# note we don't technically need to block all python reserved keywords,
# but do it for hygiene
_PYTHON_RESERVED_KEYWORDS = {
    "False",
    "None",
    "True",
    "and",
    "as",
    "assert",
    "async",
    "await",
    "break",
    "class",
    "continue",
    "def",
    "del",
    "elif",
    "else",
    "except",
    "finally",
    "for",
    "from",
    "global",
    "if",
    "import",
    "in",
    "is",
    "lambda",
    "nonlocal",
    "not",
    "or",
    "pass",
    "raise",
    "return",
    "try",
    "while",
    "with",
    "yield",
}
_PYTHON_RESERVED_KEYWORDS = {s.lower() for s in _PYTHON_RESERVED_KEYWORDS}

# Cannot be used for variable or member naming
RESERVED_KEYWORDS = _PYTHON_RESERVED_KEYWORDS | {
    # decorators
    "public",
    "external",
    "nonpayable",
    "constant",
    "immutable",
    "transient",
    "internal",
    "payable",
    "nonreentrant",
    # "class" keywords
    "interface",
    "struct",
    "event",
    "enum",
    # EVM operations
    "unreachable",
    # special functions (no name mangling)
    "init",
    "_init_",
    "___init___",
    "____init____",
    "default",
    "_default_",
    "___default___",
    "____default____",
    # more control flow and special operations
    "range",
    # more special operations
    "indexed",
    # denominations
    "ether",
    "wei",
    "finney",
    "szabo",
    "shannon",
    "lovelace",
    "ada",
    "babbage",
    "gwei",
    "kwei",
    "mwei",
    "twei",
    "pwei",
    # sentinal constant values
    # TODO remove when these are removed from the language
    "zero_address",
    "empty_bytes32",
    "max_int128",
    "min_int128",
    "max_decimal",
    "min_decimal",
    "max_uint256",
    "zero_wei",
}
            # strip out None sources so that None can be passed as a valid
            # annotation (in case it is only available optionally)
            self.annotations = [k for k in items if k is not None]

from vyper.ast.identifiers import validate_identifier
from vyper.exceptions import CompilerPanic, NamespaceCollision, UndeclaredDefinition
from vyper.ast.identifiers import validate_identifier
from vyper.ast.identifiers import validate_identifier
                    nonreentrant_key = decorator.args[0].value
                    validate_identifier(nonreentrant_key, decorator.args[0])

                    kwargs["nonreentrant"] = nonreentrant_key