Summary
Vyper has a double eval in For List Iter
Multiple evaluation of a single expression is possible in the iterator target of a for loop. While the iterator expression cannot produce multiple writes, it can consume side effects produced in the loop body (e.g. read a storage variable updated in the loop body) and thus lead to unexpected program behavior. Specifically, reads in iterators which contain an ifexp (e.g. for s: uint256 in ([read(), read()] if True else [])) may interleave reads with writes in the loop body.
The fix is tracked in https://github.com/vyperlang/vyper/pull/4488.
Vulnerability Details
Vyper for loops allow two kinds of iterator targets, namely the range() builtin and an iterable type, like SArray and DArray.
During codegen, iterable lists are required to not produce any side-effects (in the following code, range_scope forces iter_list to be parsed in a constant context, which is checked against is_constant).
def _parse_For_list(self):
with self.context.range_scope():
iter_list = Expr(self.stmt.iter, self.context).ir_node
...
def range_scope(self):
prev_value = self.in_range_expr
self.in_range_expr = True
yield
self.in_range_expr = prev_value
def is_constant(self):
return self.constancy is Constancy.Constant or self.in_range_expr
However, this does not prevent the iterator from consuming side effects provided by the body of the loop. For dynamic arrays, the compiler simply panics:
x: DynArray[uint256, 3]
@external
def test():
for i: uint256 in (self.usesideeffect() if True else self.usesideeffect()):
pass
@view
def usesideeffect() -> DynArray[uint256, 3]:
return self.x
For SArrays on the other hand, iter_list is instantiated in the body of a repeat ir, so it can be evaluated several times.
Here are three illustrating examples. In the first example, the following test case pre-evaluates the iter list and stores the result to a temporary list in memory. So the list is only evaluated once, before entry into the loop body, and the log output will be 0, 0, 0.
event I:
i: uint256
x: uint256
@deploy
def __init__():
self.x = 0
@external
def test():
for i: uint256 in [self.usesideeffect(), self.usesideeffect(), self.usesideeffect()]:
self.x += 1
log I(i)
@view
def usesideeffect() -> uint256:
return self.x
However, in the next two examples, because the iterator target is not a list literal, it will be evaluated in the loop body. In the second example, iter_list is an ifexp, thus it will be evaluated lazily in the loop body. The log output will be 0, 1, 2 due to consumption of side effects.
event I:
i: uint256
x: uint256
@deploy
def __init__():
self.x = 0
@external
def test():
for i: uint256 in ([self.usesideeffect(), self.usesideeffect(), self.usesideeffect()] if True else self.otherclause()):
self.x += 1
log I(i)
@view
def usesideeffect() -> uint256:
return self.x
@view
def otherclause() -> uint256[3]:
return [0, 0, 0]
In the third example, iter_list is also an ifexp, thus it will only be evaluated in the loop body. The log output will be 0, 1, 2 due to consumption of side effects.
event I:
i: uint256
x: uint256[3]
@deploy
def __init__():
self.x = [0, 0, 0]
@external
def test():
for i: uint256 in (self.usesideeffect() if True else self.otherclause()):
self.x[0] += 1
self.x[1] += 1
self.x[2] += 1
log I(i)
@view
def usesideeffect() -> uint256[3]:
return self.x
@view
def otherclause() -> uint256[3]:
return [0, 0, 0]
Impact
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
Frequently Asked Questions
- What is CVE-2025-27104? CVE-2025-27104 is a low-severity security vulnerability in vyper (pip), affecting versions <= 0.4.0. It is fixed in 0.4.1.
- Which versions of vyper are affected by CVE-2025-27104? vyper (pip) versions <= 0.4.0 is affected.
- Is there a fix for CVE-2025-27104? Yes. CVE-2025-27104 is fixed in 0.4.1. Upgrade to this version or later.
- Is CVE-2025-27104 exploitable, and should I be worried? Whether CVE-2025-27104 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2025-27104 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2025-27104? Upgrade
vyperto 0.4.1 or later.