CVE-2024-23346 (published 2024-02-21T17:15Z)

Pymatgen (Python Materials Genomics) is an open-source Python library for materials analysis. A critical security vulnerability exists in the `JonesFaithfulTransformation.from_transformation_str()` method within the `pymatgen` library prior to version 2024.2.20. This method insecurely utilizes `eval()` for processing input, enabling execution of arbitrary code when parsing untrusted input. Version 2024.2.20 fixes this issue.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/materialsproject/pymatgen
commit_details: repo_url: https://github.com/materialsproject/pymatgen
author: Matthew Horton
author_date: 2024-02-11 18:03:53-08:00
msg: Update `JonesFaithfulTransformation` to use sympy

Signed-off-by: Matthew Horton <mkhorton@users.noreply.github.com>
repository: repo_url: https://github.com/materialsproject/pymatgen
repo_name: materialsproject/pymatgen
description: Python Materials Genomics (pymatgen) is a robust materials analysis code that defines classes for structures and molecules with support for many electronic structure codes. It powers the Materials Project.
repo_language: Python
owner: materialsproject
file_changes: filename: settings.py
change_type: ModificationType.MODIFY
diff: @@ -6,6 +6,8 @@ import re
 from fractions import Fraction
 
 import numpy as np
+from sympy import Matrix
+from sympy.parsing.sympy_parser import parse_expr
 
 from pymatgen.core.lattice import Lattice
 from pymatgen.core.operations import MagSymmOp, SymmOp
@@ -99,21 +101,29 @@ class JonesFaithfulTransformation:
             b_change, o_shift = transformation_string.split(";")
             basis_change = b_change.split(",")
             origin_shift = o_shift.split(",")
+
             # add implicit multiplication symbols
             basis_change = [
                 re.sub(r"(?<=\w|\))(?=\() | (?<=\))(?=\w) | (?<=(\d|a|b|c))(?=([abc]))", r"*", string, flags=re.X)
                 for string in basis_change
             ]
-            # should be fine to use eval here but be mindful for security
-            # reasons
-            # see http://lybniz2.sourceforge.net/safeeval.html
-            # could replace with regex? or sympy expression?
-            P = np.array([eval(x, {"__builtins__": None}, {"a": a, "b": b, "c": c}) for x in basis_change])
-            P = P.transpose()  # by convention
+
+            # basic input sanitation
+            allowed_chars = "0123456789+-*/.abc()"
+            basis_change = ["".join([c for c in string if c in allowed_chars]) for string in basis_change]
+
+            # requires round-trip to sympy to evaluate
+            # (alternatively, `numexpr` looks like a nice solution but requires an additional dependency)
+            basis_change = [
+                parse_expr(string).subs({"a": Matrix(a), "b": Matrix(b), "c": Matrix(c)}) for string in basis_change
+            ]
+            # convert back to numpy, perform transpose by convention
+            P = np.array(basis_change, dtype=float).T[0]
+
             p = [float(Fraction(x)) for x in origin_shift]
             return P, p
-        except Exception:
-            raise ValueError("Failed to parse transformation string.")
+        except Exception as exc:
+            raise ValueError(f"Failed to parse transformation string: {exc}")
 
     @property
     def P(self) -> list[list[float]]:

programming_language: Python