Skip to content

fix: mitigate code injection related in #672 #681

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 10, 2025

Conversation

juarezr
Copy link
Member

@juarezr juarezr commented May 2, 2025

This PR has the objective of .

Changes

  1. mitigate code injection related in Code Injection via eval() in util.base.expr #672

Checklist

Use this checklist to ensure the quality of pull requests that include new code and/or make changes to existing code.

  • Source Code guidelines:
    • Includes unit tests
    • New functions have docstrings with examples that can be run with doctest
    • New functions are included in API docs
    • Docstrings include notes for any changes to API or behavior
    • All changes are documented in docs/changes.rst
  • Versioning and history tracking guidelines:
    • Using atomic commits whenever possible
    • Commits are reversible whenever possible
    • There are no incomplete changes in the pull request
    • There is no accidental garbage added to the source code
  • Testing guidelines:
    • Tested locally using tox / pytest
    • Rebased to master branch and tested before sending the PR
    • Automated testing passes (see CI)
    • Unit test coverage has not decreased (see Coveralls)
  • State of these changes is:
    • Just a proof of concept
    • Work in progress / Further changes needed
    • Ready to review
    • Ready to merge

@juarezr juarezr added the Bug It must work in all situations, but this failed label May 2, 2025
@juarezr juarezr self-assigned this May 2, 2025
@juarezr juarezr linked an issue May 2, 2025 that may be closed by this pull request
1 task
@coveralls
Copy link

coveralls commented May 2, 2025

Pull Request Test Coverage Report for Build 16203809193

Details

  • 97 of 136 (71.32%) changed or added relevant lines in 15 files are covered.
  • 9 unchanged lines in 8 files lost coverage.
  • Overall coverage decreased (-0.2%) to 90.924%

Changes Missing Coverage Covered Lines Changed/Added Lines %
petl/test/io/test_db_create.py 2 3 66.67%
petl/test/io/test_db_inference.py 14 16 87.5%
petl/test/util/test_base.py 26 41 63.41%
petl/util/base.py 28 49 57.14%
Files with Coverage Reduction New Missed Lines %
petl/io/remotes.py 1 84.95%
petl/test/io/test_db_create.py 1 93.23%
petl/test/io/test_numpy.py 1 98.43%
petl/test/io/test_pandas.py 1 93.33%
petl/test/io/test_pytables.py 1 98.15%
petl/test/io/test_whoosh.py 1 98.15%
petl/test/transform/test_intervals.py 1 97.73%
petl/test/io/test_remotes.py 2 88.89%
Totals Coverage Status
Change from base Build 15981322831: -0.2%
Covered Lines: 13464
Relevant Lines: 14808

💛 - Coveralls

@l3str4nge
Copy link

l3str4nge commented May 20, 2025

 petl/util/__init__.py:4: in <module>
    from petl.util.base import Table, Record, values, header, data, \
E     File "/home/runner/work/petl/petl/petl/util/base.py", line 686
E       raise ValueError('Invalid expression: %s' % strexpr) from ve
E                                                               ^
E   SyntaxError: invalid syntax

Tests are failing 👀 however they are on the right way I guess 😄

return eval("lambda rec: " + prog.sub(repl, s))
strexpr = "lambda rec: " + prog.sub(repl, expression_text)
try:
fun = eval(strexpr, { '__builtins__': None }, { '__builtins__': None })

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo it's still vulnerable, example:

"{__class__.__mro__[1].__subclasses__()[59]('/etc/passwd').read()}"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you can use https://lmfit.github.io/asteval/ ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo it's still vulnerable, example:

"{__class__.__mro__[1].__subclasses__()[59]('/etc/passwd').read()}"

@l3str4nge,

You are correct.
I think this code wasn't meant to be used in unsafe environments.

Anyway, we have some possible approaches:

  1. Add code like the above to mitigate somewhat the issue for current users.
  2. Implement a second conditional version that uses asteval if available.
  3. Deprecate the current mitigated code/approach and remove it in milestone v2.0 as it's planned for breaking changes.

Patches welcome. :)

res = fu(2)
if res:
print(res)
assert exc_info is not None

Check warning

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Warning test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
from petl.comparison import comparable_itemgetter
from petl.compat import (

Check warning

Code scanning / Prospector (reported by Codacy)

Redefining built-in 'next' (redefined-builtin)

Redefining built-in 'next' (redefined-builtin)
fun = eval(strexpr, restricted, restricted)
return fun
except ValueError as ve:
raise ValueError('Invalid expression: "%s" causes error: %s' % (strexpr, ve))

Check warning

Code scanning / Prospector (reported by Codacy)

Consider explicitly re-raising using 'raise ValueError('Invalid expression: "%s" causes error: %s' % (strexpr, ve)) from ve' (raise-missing-from)

Consider explicitly re-raising using 'raise ValueError('Invalid expression: "%s" causes error: %s' % (strexpr, ve)) from ve' (raise-missing-from)
fun = eval(strexpr, restricted, restricted)
return fun
except ValueError as ve:
raise ValueError('Invalid expression: "%s" causes error: %s' % (strexpr, ve))

Check warning

Code scanning / Prospector (reported by Codacy)

Formatting a regular string which could be an f-string (consider-using-f-string)

Formatting a regular string which could be an f-string (consider-using-f-string)
@@ -330,3 +330,18 @@
table = []
with pytest.raises(FieldSelectionError):
rowgroupby(table, 'foo')


def test_expr_ok():

Check warning

Code scanning / Pylint (reported by Codacy)

Missing function docstring Warning test

Missing function docstring
assert res == 2


def test_expr_inject():

Check warning

Code scanning / Pylint (reported by Codacy)

Missing function docstring Warning test

Missing function docstring
@@ -1,19 +1,38 @@
from __future__ import absolute_import, print_function, division
from __future__ import absolute_import, division, print_function

Check warning

Code scanning / Pylint (reported by Codacy)

Missing module docstring Warning

Missing module docstring
}
fun = eval(strexpr, restricted, restricted)
return fun
except ValueError as ve:

Check warning

Code scanning / Pylint (reported by Codacy)

Variable name "ve" doesn't conform to snake_case naming style Warning

Variable name "ve" doesn't conform to snake_case naming style
@juarezr juarezr added the Help Wanted We are volunteers. We'll be happy if you join us. label Jun 30, 2025

strexpr = "lambda rec: " + prog.sub(repl, expression_text)
try:
fun = eval(strexpr, _RESTRICTED, _RESTRICTED)

Check warning

Code scanning / Bandit (reported by Codacy)

Use of possibly insecure function - consider using safer ast.literal_eval. Warning

Use of possibly insecure function - consider using safer ast.literal_eval.

strexpr = "lambda rec: " + prog.sub(repl, expression_text)
try:
fun = eval(strexpr, _RESTRICTED, _RESTRICTED)

Check warning

Code scanning / Prospector (reported by Codacy)

Use of eval (eval-used)

Use of eval (eval-used)
@@ -678,7 +700,32 @@
def repl(matchobj):
return "rec['%s']" % matchobj.group(1)

return eval("lambda rec: " + prog.sub(repl, s))
global _RESTRICTED

Check notice

Code scanning / Pylint (reported by Codacy)

Using the global statement Note

Using the global statement

strexpr = "lambda rec: " + prog.sub(repl, expression_text)
try:
fun = eval(strexpr, _RESTRICTED, _RESTRICTED)

Check warning

Code scanning / Pylint (reported by Codacy)

Use of eval Warning

Use of eval
@juarezr juarezr requested a review from l3str4nge July 1, 2025 20:38
fu = expr("{foo2} * {bar2}", trusted=True)
with pytest.raises(KeyError) as exc_info:
fu({'foo': 3, 'bar': 2})
assert exc_info is not None

Check warning

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Warning test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
# trick: using trusted=None will allow to skip using asteval
fu = expr("__import__('os').system('ls')", trusted=None)
fu({'foo': 3, 'bar': 2})
assert exc_info is not None

Check warning

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Warning test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
fu = expr("{foo2} * {bar2}", trusted=True)
with pytest.raises(KeyError) as exc_info:
fu({'foo': 3, 'bar': 2})
assert exc_info is not None

Check warning

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Warning test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
def _has_asteval():
if PY3:
try:
import asteval

Check warning

Code scanning / Ruff (reported by Codacy)

asteval imported but unused; consider using importlib.util.find_spec to test for availability (F401) Warning test

asteval imported but unused; consider using importlib.util.find\_spec to test for availability (F401)
Copy link

@github-advanced-security github-advanced-security bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pylint (reported by Codacy) found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Copy link

@github-advanced-security github-advanced-security bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pylintpython3 (reported by Codacy) found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@@ -4,9 +4,9 @@

from petl.errors import FieldSelectionError
from petl.test.helpers import ieq, eq_
from petl.compat import next
from petl.compat import PY3, next

Check warning

Code scanning / Prospector (reported by Codacy)

Redefining built-in 'next' (redefined-builtin)

Redefining built-in 'next' (redefined-builtin)
def _has_asteval():
if PY3:
try:
import asteval

Check warning

Code scanning / Prospector (reported by Codacy)

Unused import asteval (unused-import)

Unused import asteval (unused-import)
def _has_asteval():
if PY3:
try:
import asteval

Check warning

Code scanning / Prospector (reported by Codacy)

Import outside toplevel (asteval) (import-outside-toplevel)

Import outside toplevel (asteval) (import-outside-toplevel)


def iterfieldmap(source, mappings, failonerror, errorvalue):
def iterfieldmap(source, mappings, failonerror, errorvalue, trusted):

Check warning

Code scanning / Prospector (reported by Codacy)

Too many branches (17/15) (too-many-branches)

Too many branches (17/15) (too-many-branches)


def iterfieldmap(source, mappings, failonerror, errorvalue):
def iterfieldmap(source, mappings, failonerror, errorvalue, trusted):

Check warning

Code scanning / Prospector (reported by Codacy)

Too many local variables (18/15) (too-many-locals)

Too many local variables (18/15) (too-many-locals)
self.aeval.symtable['rec'] = rec
evaluated = self.aeval("expr(rec)")
if len(self.aeval.error) > 0:
err = [ e.get_error()[-1] for e in self.aeval.error ]

Check warning

Code scanning / Prospector (reported by Codacy)

Bad indentation. Found 16 spaces, expected 12 (bad-indentation)

Bad indentation. Found 16 spaces, expected 12 (bad-indentation)
evaluated = self.aeval("expr(rec)")
if len(self.aeval.error) > 0:
err = [ e.get_error()[-1] for e in self.aeval.error ]
msg = "\n".join(err)

Check warning

Code scanning / Prospector (reported by Codacy)

Bad indentation. Found 16 spaces, expected 12 (bad-indentation)

Bad indentation. Found 16 spaces, expected 12 (bad-indentation)
if len(self.aeval.error) > 0:
err = [ e.get_error()[-1] for e in self.aeval.error ]
msg = "\n".join(err)
raise ValueError("Failed to evaluate expression due to: %s" % msg)

Check warning

Code scanning / Prospector (reported by Codacy)

Bad indentation. Found 16 spaces, expected 12 (bad-indentation)

Bad indentation. Found 16 spaces, expected 12 (bad-indentation)
if len(self.aeval.error) > 0:
err = [ e.get_error()[-1] for e in self.aeval.error ]
msg = "\n".join(err)
raise ValueError("Failed to evaluate expression due to: %s" % msg)

Check warning

Code scanning / Prospector (reported by Codacy)

Formatting a regular string which could be an f-string (consider-using-f-string)

Formatting a regular string which could be an f-string (consider-using-f-string)
err = [ e.get_error()[-1] for e in self.aeval.error ]
msg = "\n".join(err)
raise ValueError("Failed to evaluate expression due to: %s" % msg)
return evaluated

Check warning

Code scanning / Prospector (reported by Codacy)

Bad indentation. Found 12 spaces, expected 8 (bad-indentation)

Bad indentation. Found 12 spaces, expected 8 (bad-indentation)
@juarezr juarezr force-pushed the fix/util.base.expr branch from c937998 to f0f522e Compare July 10, 2025 19:07
def test_json_inference():
data = [{'a': 1}, {'b': 2}, None]
col = make_sqlalchemy_column(data, 'payload')
assert col.name == 'payload'

Check warning

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Warning test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
data = [{'a': 1}, {'b': 2}, None]
col = make_sqlalchemy_column(data, 'payload')
assert col.name == 'payload'
assert isinstance(col.type, JSON)

Check warning

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Warning test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

def test_int_inference():
col = make_sqlalchemy_column([1, 2, 3], 'n')
assert col.name == 'n'

Check warning

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Warning test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
def test_int_inference():
col = make_sqlalchemy_column([1, 2, 3], 'n')
assert col.name == 'n'
assert isinstance(col.type, Integer)

Check warning

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Warning test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
@juarezr juarezr merged commit f155b6d into petl-developers:master Jul 10, 2025
48 checks passed
@juarezr juarezr deleted the fix/util.base.expr branch July 10, 2025 19:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug It must work in all situations, but this failed Help Wanted We are volunteers. We'll be happy if you join us.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Code Injection via eval() in util.base.expr
3 participants