GHSA-89v8-rhwq-hf77: Medium severity pip/asteval vulnerability
Summary
An attacker who can supply expressions to asteval.Interpreter.eval() can raise SystemExit, KeyboardInterrupt, GeneratorExit, or BaseException from inside the sandbox. These exceptions are subclasses of BaseException but not Exception, so they bypass the except Exception: safety net in both run() and eval(). The exception propagates verbatim to the calling application, terminating the process or disrupting signal and cleanup handlers.
This is distinct from prior vulnerabilities CVE-2025-24359 (format string injection) and GHSA-vp47-9734-prjw (AST mutation TOCTOU), both fixed in 1.0.6. This vector is present in all versions including 1.0.6 and current HEAD.
---
Affected Code
asteval/astutils.py, lines 89–108 — FROMPY exposes dangerous classes to sandbox users:
python FROMPY = ('ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', # ← escapes except Exception: 'BufferError', 'BytesWarning', ... 'GeneratorExit', # ← escapes except Exception: ... 'KeyboardInterrupt', # ← escapes except Exception: ... 'SystemExit', # ← escapes except Exception: ...)
asteval/asteval.py, line 322 — run() exception handler:
python except Exception: # ← does NOT catch BaseException subclasses if withraise and self.expr is not None: self.raiseexception(node, expr=self.expr)
asteval/asteval.py, line 370 — eval() exception handler:
python except Exception: # ← same gap if showerrors and not raiseerrors: ...
asteval/asteval.py, line 264 — raiseexception() raises the class directly:
python raise exc(self.errormsg) # ← when exc=SystemExit, escapes both handlers above
---
Root Cause
Python's exception hierarchy has two distinct branches under BaseException:
BaseException ├── SystemExit ← NOT caught by except Exception: ├── KeyboardInterrupt ← NOT caught by except Exception: ├── GeneratorExit ← NOT caught by except Exception: └── Exception ← caught normally ├── RuntimeError ├── ValueError └── ...
FROMPY exposes all four non-Exception classes to sandbox users. When a user writes raise SystemExit("msg"), the onraise() handler calls:
python self.raiseexception(None, exc=out.class, msg=msg, expr='')
which executes raise SystemExit(msg). This propagates through both except Exception: guards unchecked and surfaces in the calling application.
---
Proof of Concept
python from asteval import Interpreter
Variant 1: terminate the process aeval = Interpreter() try: aeval.eval('raise SystemExit("terminated by sandbox user")') except SystemExit as e: print(f"[CONFIRMED] SystemExit escaped: {e.code!r}")
Variant 2: disrupt signal/finally handling aeval = Interpreter() try: aeval.eval('raise KeyboardInterrupt("interrupt injected")') except KeyboardInterrupt as e: print(f"[CONFIRMED] KeyboardInterrupt escaped: {str(e)!r}")
Variant 3: GeneratorExit aeval = Interpreter() try: aeval.eval('raise GeneratorExit("gen escape")') except GeneratorExit as e: print(f"[CONFIRMED] GeneratorExit escaped: {str(e)!r}")
Variant 4: BaseException base class aeval = Interpreter() try: aeval.eval('raise BaseException("base escape")') except BaseException as e: if not isinstance(e, Exception): print(f"[CONFIRMED] BaseException escaped: {str(e)!r}")
Output (tested on asteval 1.0.6, Python 3.11/3.12):
[CONFIRMED] SystemExit escaped: 'terminated by sandbox user' [CONFIRMED] KeyboardInterrupt escaped: 'interrupt injected' [CONFIRMED] GeneratorExit escaped: 'gen escape' [CONFIRMED] BaseException escaped: 'base escape'
Real-world server scenario
python from asteval import Interpreter
def handlerequest(userexpression): aeval = Interpreter() return aeval.eval(userexpression) # SystemExit propagates here
Attacker sends: raise SystemExit(1) Application terminates. Top-level except Exception: handlers do not protect it. try: handlerequest('raise SystemExit(1)') except Exception: pass # <-- does NOT catch SystemExit; process exits
---
Impact
| Variant | Impact | |---------|--------| | SystemExit | Process terminates; exit code and message attacker-controlled | | KeyboardInterrupt | Disrupts finally blocks, signal handlers, and KeyboardInterrupt-aware loops | | GeneratorExit | Disrupts generator cleanup in calling code | | BaseException | Generic escape, same propagation |
Any application that: - Accepts user-supplied expressions via asteval - Relies on except Exception: at the top level (standard practice) - Does not wrap aeval.eval() in except BaseException: (non-standard, unexpected requirement)
...is vulnerable to attacker-triggered process termination (DoS).
CVSS breakdown: Network-reachable (AV:N), no special conditions (AC:L), no credentials (PR:N), no interaction (UI:N), scope unchanged (S:U), no confidentiality/integrity impact (C:N/I:N), high availability impact — process termination (A:H).
---
Additional Note: File Read Capability (Acknowledged Limitation)
Independently of this vulnerability, asteval exposes a read-only open() wrapper (open in astutils.py) that allows reading arbitrary files with the permissions of the calling process:
python aeval.eval("open('/etc/passwd').read()") # returns /etc/passwd contents
This is documented in doc/motivation.rst as a known design choice ("If reading from disk must be forbidden, you will want to overwrite the open() function from the symbol table"). It is included here for completeness, not as a separate advisory claim.
---
Recommended Fix
Option A — Remove dangerous classes from FROMPY (minimal, preferred):
python asteval/astutils.py
FROMPY = ('ArithmeticError', 'AssertionError', 'AttributeError', # Remove: 'BaseException', 'BufferError', 'BytesWarning', 'DeprecationWarning', 'EOFError', 'EnvironmentError', 'Exception', 'False', 'FloatingPointError', # Remove: 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'KeyError', # Remove: 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'None', 'NotImplementedError', 'OSError', 'OverflowError', 'ReferenceError', 'RuntimeError', 'RuntimeWarning', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', # Remove: 'SystemExit', 'True', 'TypeError', ...)
Option B — Block non-Exception raises in onraise():
python asteval/asteval.py
def onraise(self, node): excnode = node.exc msgnode = node.cause out = self.run(excnode) # Prevent BaseException subclasses from escaping the sandbox if not issubclass(out.class, Exception): self.raiseexception(node, exc=RuntimeError, msg=f"raising {out.class.name!r} is not permitted") return msg = ' '.join(str(a) for a in out.args) msg2 = self.run(msgnode) if msg2 not in (None, 'None'): msg = f"{msg}: {msg2}" self.raiseexception(None, exc=out.class, msg=msg, expr='')
Note: Option B also fixes a secondary bug on the same line — ' '.join(out.args) crashes with TypeError when args contain non-strings (e.g., raise SystemExit(0) with integer code). The fix uses str(a) for a in out.args.
Option C — Catch BaseException in run() and eval() (broadest, requires care):
python except BaseException as exc: if isinstance(exc, (SystemExit, KeyboardInterrupt, GeneratorExit)): # Re-raise as RuntimeError to contain within sandbox self.raiseexception(node, exc=RuntimeError, msg=f"{type(exc).name} raised in sandbox") elif withraise and self.expr is not None: self.raiseexception(node, expr=self.expr)
Option A is the simplest and least likely to introduce regressions. Option B additionally addresses the str.join crash on integer args.
---
Disclosure Timeline
| Date | Event | |------|-------| | 2026-06-09 | Vulnerability discovered during code review | | 2026-06-09 | Report submitted via GitHub Security Advisory | | TBD | Maintainer acknowledgment | | TBD + 90 days | Public disclosure deadline |
---
Researcher
Independent security researcher. No bug bounty program exists for this project. CVE assignment requested via GitHub Security Advisory submission.
---
References
- Prior CVE: CVE-2025-24359 (format string injection, fixed 1.0.6) - Prior advisory: GHSA-vp47-9734-prjw (AST mutation TOCTOU, fixed 1.0.6) - Python exception hierarchy: https://docs.python.org/3/library/exceptions.html#exception-hierarchy - asteval documentation: https://lmfit.github.io/asteval/
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/astevalto a version that resolves this vulnerability.Fixed in 1.0.9 - Upgrade
Upgrade
astevalto a version that resolves this vulnerability.Fixed in 1.0.6Patch CVE-2025-24359 - Upgrade
Upgrade
astevalto a version that resolves this vulnerability.Fixed in 1.0.6Patch GHSA-vp47-9734-prjw - Configuration
Option A: remove the dangerous non-Exception classes from FROM_PY so they are not exposed to sandbox users (FROM_PY currently includes 'BaseException', 'GeneratorExit', 'KeyboardInterrupt', and 'SystemExit').
asteval/astutils.py FROM_PY = remove 'BaseException', 'GeneratorExit', 'KeyboardInterrupt', 'SystemExit' - Configuration
Option B: in on_raise(), prevent sandbox raises that are not subclasses of Exception (explicitly block SystemExit, KeyboardInterrupt, GeneratorExit, and BaseException escapes; the material notes on_raise() currently calls raise SystemExit(msg) and then does not contain it).
asteval/asteval.py on_raise() handler = block non-`Exception` raises - Configuration
The material notes `asteval` includes a read-only `open()` wrapper for file reads; overwrite the `open()` function from the symbol table (or otherwise block it) if file read is not intended/allowed.
asteval/astutils.py open() wrapper in symbol table = overwrite/disable open() - Compensating control
If you allow user-supplied expressions, apply an external mitigation: prevent the calling application from reaching attacker-controlled `asteval.Interpreter.eval()` (e.g., only evaluate trusted expressions, or isolate the evaluation in a separate process/accounting so process termination/interrupt can’t terminate or disrupt the main service).
Event History
Frequently Asked Questions
Which deployments are exposed?
Applications are exposed when an attacker can supply expressions to asteval.Interpreter.eval(). The affected vector is present in all versions, including 1.0.6 and current HEAD.
What does an attacker need to trigger the issue?
The attacker needs the ability to provide an expression evaluated by asteval.Interpreter.eval(). The published vector also indicates local access, low privileges, and required user interaction.
What is the operational impact of successful exploitation?
An attacker can raise SystemExit, KeyboardInterrupt, GeneratorExit, or BaseException so that it bypasses handlers catching Exception. The exception can propagate to the calling application, terminating the process or disrupting signal and cleanup handlers.
What can be done while a fix is unavailable?
Do not evaluate attacker-supplied expressions with asteval.Interpreter.eval(). Restrict expression input to trusted sources until the exposed exception classes and exception handling are remediated.
How can I determine whether my application is at risk?
Review whether any untrusted or lower-privileged user can influence expressions passed to asteval.Interpreter.eval(). Version checking alone will not exclude this issue, because it is reported as present through 1.0.6 and current HEAD.