From b60a883bb594b71569c4dfdc598f5602bf5015b5 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Tue, 3 Dec 2024 13:59:13 +0100 Subject: [PATCH 1/3] An alternative approach to avoiding arbitrary evaluation of code when parsing strings as units. Based on https://stackoverflow.com/a/11952618 by https://stackoverflow.com/users/567292/ecatmur Fixes #250 Also added test for the original issue: #221 --- .github/workflows/test.yml | 1 + quantities/registry.py | 48 ++++++++++++++++++++-------------- quantities/tests/test_units.py | 8 ++++++ 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a445ed1..62a451a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -118,6 +118,7 @@ jobs: python -m pip install -U setuptools python -m pip install -U wheel python -m pip install "numpy==${{ matrix.numpy-version }}" + python -m pip install -U pytest python -m pip install -U mypy - name: Install diff --git a/quantities/registry.py b/quantities/registry.py index 4f9f3c7..d03433e 100644 --- a/quantities/registry.py +++ b/quantities/registry.py @@ -1,41 +1,49 @@ """ """ +import ast import re -import builtins class UnitRegistry: + # Note that this structure ensures that UnitRegistry behaves as a singleton class __Registry: __shared_state = {} + whitelist = ( + ast.Expression, + ast.Constant, + ast.Name, + ast.Load, + ast.BinOp, + ast.UnaryOp, + ast.operator, + ast.unaryop, + ast.Num, + ) def __init__(self): self.__dict__ = self.__shared_state self.__context = {} def __getitem__(self, string): - - # easy hack to prevent arbitrary evaluation of code - all_builtins = dir(builtins) - # because we have kilobytes, other bytes we have to remove bytes - all_builtins.remove("bytes") - # have to deal with octet as well - all_builtins.remove("oct") - # have to remove min which is short for minute - all_builtins.remove("min") - for builtin in all_builtins: - if builtin in string: - raise RuntimeError(f"String parsing error for `{string}`. Enter a string accepted by quantities") - - try: - return eval(string, self.__context) - except NameError: + tree = ast.parse(string, mode="eval") + valid = all(isinstance(node, self.whitelist) for node in ast.walk(tree)) + if valid: + try: + item = eval( + compile(tree, filename="", mode="eval"), + {"__builtins__": {}}, + self.__context, + ) + except NameError: + raise LookupError('Unable to parse units: "%s"' % string) + else: + return item + else: # could return self['UnitQuantity'](string) - raise LookupError( - 'Unable to parse units: "%s"'%string - ) + raise LookupError('Unable to parse units: "%s"' % string) def __setitem__(self, string, val): assert isinstance(string, str) diff --git a/quantities/tests/test_units.py b/quantities/tests/test_units.py index 2bbbf93..1912a3a 100644 --- a/quantities/tests/test_units.py +++ b/quantities/tests/test_units.py @@ -1,6 +1,9 @@ +import pytest + from .. import units as pq from .common import TestCase + class TestUnits(TestCase): def test_compound_units(self): @@ -30,3 +33,8 @@ def test_units_copy(self): self.assertQuantityEqual(pq.m.copy(), pq.m) pc_per_cc = pq.CompoundUnit("pc/cm**3") self.assertQuantityEqual(pc_per_cc.copy(), pc_per_cc) + + def test_code_injection(self): + with pytest.raises(LookupError) as exc_info: + pq.CompoundUnit("exec(\"print('Hello there.')\\nprint('General Wasabi!')\")") + assert "Wasabi" in str(exc_info.value) From 45d8a2ed1d6b448d03a599c6d576f03f96fda0a1 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Wed, 5 Feb 2025 17:44:14 +0100 Subject: [PATCH 2/3] remove ast.Num from whitelist as it is deprecated and will be removed in Python 3.14 Co-authored-by: Zach McKenzie <92116279+zm711@users.noreply.github.com> --- quantities/registry.py | 1 - 1 file changed, 1 deletion(-) diff --git a/quantities/registry.py b/quantities/registry.py index d03433e..0cb5082 100644 --- a/quantities/registry.py +++ b/quantities/registry.py @@ -20,7 +20,6 @@ class __Registry: ast.UnaryOp, ast.operator, ast.unaryop, - ast.Num, ) def __init__(self): From 9f212e9f1939b627ef5caf68bac07ddc9c7d14bb Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Wed, 5 Feb 2025 17:47:06 +0100 Subject: [PATCH 3/3] Add reference to Stack Overflow post --- quantities/registry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/quantities/registry.py b/quantities/registry.py index 0cb5082..7603965 100644 --- a/quantities/registry.py +++ b/quantities/registry.py @@ -27,6 +27,8 @@ def __init__(self): self.__context = {} def __getitem__(self, string): + # This approach to avoiding arbitrary evaluation of code is based on https://stackoverflow.com/a/11952618 + # by https://stackoverflow.com/users/567292/ecatmur tree = ast.parse(string, mode="eval") valid = all(isinstance(node, self.whitelist) for node in ast.walk(tree)) if valid: pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy