Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
erp5_fork
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Eteri
erp5_fork
Commits
1cbff971
Commit
1cbff971
authored
May 19, 2022
by
Levin Zimmermann
Browse files
Options
Browse Files
Download
Plain Diff
Allow patched pandas.read_* in restricted Python
See merge request
nexedi/erp5!1615
parents
85317472
4360dbc6
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
233 additions
and
16 deletions
+233
-16
bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testRestrictedPythonSecurity.py
...rtal_components/test.erp5.testRestrictedPythonSecurity.py
+143
-8
product/ERP5Type/Pandas.py
product/ERP5Type/Pandas.py
+84
-0
product/ERP5Type/patches/Restricted.py
product/ERP5Type/patches/Restricted.py
+6
-8
No files found.
bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testRestrictedPythonSecurity.py
View file @
1cbff971
...
...
@@ -25,6 +25,7 @@
#
##############################################################################
import
json
import
os.path
import
tempfile
import
textwrap
...
...
@@ -572,14 +573,6 @@ class TestRestrictedPythonSecurity(ERP5TypeTestCase):
)
def
testPandasIORead
(
self
):
self
.
assertRaises
(
Unauthorized
,
self
.
createAndRunScript
,
'''
import pandas as pd
pd.read_csv('testPandasIORead.csv')
'''
)
# Test the black_list configuration validity
for
read_method
in
pandas_black_list
:
self
.
assertRaises
(
...
...
@@ -635,6 +628,148 @@ class TestRestrictedPythonSecurity(ERP5TypeTestCase):
write_method('testPandasSeriesIOWrite.data')
'''
.
format
(
write_method
=
write_method
))
def
_assertPandasRestrictedReadFunctionIsEqualTo
(
self
,
read_function
,
read_argument
,
expected_data_frame_init
):
self
.
createAndRunScript
(
'''
import pandas as pd
expected_data_frame = pd.DataFrame({expected_data_frame_init})
return pd.{read_function}({read_argument}).equals(expected_data_frame)
'''
.
format
(
expected_data_frame_init
=
expected_data_frame_init
,
read_function
=
read_function
,
read_argument
=
read_argument
,
),
expected
=
True
)
def
testPandasRestrictedReadFunctionProhibitedInput
(
self
):
"""
Test if patched pandas read_* functions raise with any input which isn't a string.
"""
for
pandas_read_function
in
(
"read_json"
,
"read_csv"
,
"read_fwf"
):
for
preparation
,
prohibited_input
in
(
(
''
,
100
),
(
'from StringIO import StringIO'
,
'StringIO("[1, 2, 3]")'
),
):
self
.
assertRaises
(
ZopeGuardsUnauthorized
,
self
.
createAndRunScript
,
'''
import pandas as pd
{preparation}
pd.{pandas_read_function}({prohibited_input})
'''
.
format
(
preparation
=
preparation
,
pandas_read_function
=
pandas_read_function
,
prohibited_input
=
prohibited_input
,
)
)
def
testPandasReadFwf
(
self
):
read_function
=
"read_fwf"
# Normal input should be correctly handled
self
.
_assertPandasRestrictedReadFunctionIsEqualTo
(
read_function
,
r'"100\n200"'
,
r"[[200]], columns=['100']"
,
)
# Ensure monkey patch parses keyword arguments to patched function
self
.
_assertPandasRestrictedReadFunctionIsEqualTo
(
read_function
,
r'"1020\n3040", widths=[2, 2]'
,
r"[[30, 40]], columns=['10', '20']"
,
)
# A string containing an url or file path should be handled as if
# it would be a normal csv string entry
self
.
_assertPandasRestrictedReadFunctionIsEqualTo
(
read_function
,
r'"file://path/to/fwf/file.fwf"'
,
r"[], columns=['file://path/to/fwf/file.fwf']"
,
)
def
testPandasReadCSV
(
self
):
read_function
=
"read_csv"
# Normal input should be correctly handled
self
.
_assertPandasRestrictedReadFunctionIsEqualTo
(
read_function
,
r'"11,2,300\n50.5,99,hello"'
,
r"[[50.5, 99, 'hello']], columns='11 2 300'.split(' ')"
,
)
# Ensure monkey patch parses keyword arguments to patched function
self
.
_assertPandasRestrictedReadFunctionIsEqualTo
(
read_function
,
r'"a;b", sep=";"'
,
r"[], columns=['a', 'b']"
,
)
# A string containing an url or file path should be handled as if
# it would be a normal csv string entry
self
.
_assertPandasRestrictedReadFunctionIsEqualTo
(
read_function
,
r'"https://people.sc.fsu.edu/~jburkardt/data/csv/addresses.csv"'
,
r"[], columns=['https://people.sc.fsu.edu/~jburkardt/data/csv/addresses.csv']"
,
)
self
.
_assertPandasRestrictedReadFunctionIsEqualTo
(
read_function
,
r'"file://path/to/csv/file.csv"'
,
r"[], columns=['file://path/to/csv/file.csv']"
,
)
def
testPandasReadJsonParsesInput
(
self
):
read_function
=
"read_json"
# Normal input should be correctly handled
self
.
_assertPandasRestrictedReadFunctionIsEqualTo
(
read_function
,
'"[1, 2, 3]"'
,
"[1, 2, 3]"
)
self
.
_assertPandasRestrictedReadFunctionIsEqualTo
(
read_function
,
'
\
'
{"column_name": [1, 2, 3], "another_column": [3, 9.2, 100]}
\
'
'
,
'{"column_name": [1, 2, 3], "another_column": [3, 9.2, 100]}'
,
)
# Ensure monkey patch parses keyword arguments to patched function
self
.
_assertPandasRestrictedReadFunctionIsEqualTo
(
read_function
,
r'"[1, 2, 3]\n[4, 5, 6]", lines=True'
,
"[[1, 2, 3], [4, 5, 6]]"
,
)
# URLs, etc. should raise a ValueError
# (see testPandasReadJsonProhibitsMalicousString)
def
testPandasReadJsonProhibitsMalicousString
(
self
):
"""
Test if file path, urls and other bad strings
raise value errors
"""
# Create valid json file which could be read
# by a non-patched read_json function.
test_file_path
=
".testPandasReadJson.json"
json_test_data
=
[
1
,
2
,
3
]
with
open
(
test_file_path
,
'w'
)
as
json_file
:
json
.
dump
(
json_test_data
,
json_file
)
self
.
addCleanup
(
os
.
remove
,
test_file_path
)
# Ensure json creation was successful
self
.
assertTrue
(
os
.
path
.
isfile
(
test_file_path
))
with
open
(
test_file_path
,
"r"
)
as
json_file
:
self
.
assertEqual
(
json_test_data
,
json
.
loads
(
json_file
.
read
()))
for
malicous_input
in
(
# If pandas would read this as an URL it should
# raise an URLError. But because it will try
# to read it as a json string, it will raise
# a ValueError.
"https://test-url.com/test-name.json"
,
"file://path/to/json/file.json"
,
# This shouldn't raise any error in case
# pandas read function wouldn't be patched.
test_file_path
,
# Gibberish should also raise a ValueError
"Invalid-string"
):
self
.
assertRaises
(
ValueError
,
self
.
createAndRunScript
,
'''
import pandas as pd
pd.read_json("{}")
'''
.
format
(
malicous_input
)
)
def
test_suite
():
suite
=
unittest
.
TestSuite
()
...
...
product/ERP5Type/Pandas.py
0 → 100644
View file @
1cbff971
##############################################################################
#
# Copyright (c) 2012 Nexedi SARL and Contributors. All Rights Reserved.
# Levin Zimmermann <levin.zimmermann@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
"""
Restricted pandas module.
From restricted python, use "import pandas" (see patches/Restricted.py).
"""
from
pandas
import
*
# Add restricted versions of IO functions
import
six
as
_six
from
AccessControl.ZopeGuards
import
Unauthorized
as
_ZopeGuardsUnauthorized
if
_six
.
PY2
:
from
StringIO
import
StringIO
as
_StringIO
else
:
from
io
import
StringIO
as
_StringIO
def
_addRestrictedPandasReadFunction
(
function_name
):
original_function
=
getattr
(
__import__
(
'pandas'
),
function_name
)
def
Pandas_read
(
data_string
,
*
args
,
**
kwargs
):
# Strict: don't use 'isinstance', only allow buildin str
# objects
if
type
(
data_string
)
is
not
str
:
raise
_ZopeGuardsUnauthorized
(
"Parsing object '%s' of type '%s' is prohibited!"
%
(
data_string
,
type
(
data_string
))
)
string_io
=
_StringIO
(
data_string
)
return
original_function
(
string_io
,
*
args
,
**
kwargs
)
disclaimer
=
"""
\
n
Disclaimer:
This function has been patched by ERP5 for zope sandbox usage.
Only objects of type 'str' are valid inputs, file paths, files,
urls, etc. are prohibited or ignored.
"""
Pandas_read
.
__doc__
=
original_function
.
__doc__
+
disclaimer
globals
().
update
({
function_name
:
Pandas_read
})
def
_addRestrictedPandasReadFunctionTuple
():
pandas_read_function_to_restrict_tuple
=
(
"read_json"
,
# "read_html", # needs installation of additional dependency: html5lib
"read_csv"
,
"read_fwf"
,
# "read_xml", # only available for pandas version >= 1.3.0
)
for
pandas_read_function_to_restrict
in
pandas_read_function_to_restrict_tuple
:
_addRestrictedPandasReadFunction
(
pandas_read_function_to_restrict
)
_addRestrictedPandasReadFunctionTuple
()
\ No newline at end of file
product/ERP5Type/patches/Restricted.py
View file @
1cbff971
...
...
@@ -371,6 +371,7 @@ MNAME_MAP = {
'calendar'
:
'Products.ERP5Type.Calendar'
,
'collections'
:
'Products.ERP5Type.Collections'
,
'six'
:
'Products.ERP5Type.Six'
,
'pandas'
:
'Products.ERP5Type.Pandas'
,
}
for
alias
,
real
in
six
.
iteritems
(
MNAME_MAP
):
assert
'.'
not
in
alias
,
alias
# TODO: support this
...
...
@@ -478,23 +479,20 @@ def restrictedMethod(s,name):
raise
Unauthorized
(
name
)
return
dummyMethod
try
:
import
pandas
as
pd
except
ImportError
:
pass
else
:
allow_module
(
'pandas'
)
allow_type
(
pd
.
Series
)
allow_type
(
pd
.
Timestamp
)
allow_type
(
pd
.
DatetimeIndex
)
# XXX: pd.DataFrame has its own security thus disable
# until we can fully integrate it
#allow_type(pd.DataFrame)
allow_type
(
pd
.
MultiIndex
)
allow_type
(
pd
.
indexes
.
range
.
RangeIndex
)
allow_type
(
pd
.
indexes
.
numeric
.
Int64Index
)
allow_type
(
pd
.
core
.
groupby
.
DataFrameGroupBy
)
allow_type
(
pd
.
core
.
groupby
.
SeriesGroupBy
)
allow_class
(
pd
.
DataFrame
)
# Note: These black_list methods are for pandas 0.19.2
...
...
@@ -503,10 +501,10 @@ else:
ContainerAssertions
[
pd
.
Series
]
=
_check_access_wrapper
(
pd
.
Series
,
dict
.
fromkeys
(
series_black_list
,
restrictedMethod
))
pandas_black_list
=
(
'read_
csv'
,
'read_json'
,
'read_
pickle'
,
'read_hdf'
,
'read_
fwf'
,
'read_
excel'
,
'read_html'
,
'read_msgpack'
,
pandas_black_list
=
(
'read_pickle'
,
'read_hdf'
,
'read_excel'
,
'read_html'
,
'read_msgpack'
,
'read_gbq'
,
'read_sas'
,
'read_stata'
)
ModuleSecurityInfo
(
'pandas'
).
declarePrivate
(
*
pandas_black_list
)
ModuleSecurityInfo
(
MNAME_MAP
[
'pandas'
]
).
declarePrivate
(
*
pandas_black_list
)
dataframe_black_list
=
(
'to_csv'
,
'to_json'
,
'to_pickle'
,
'to_hdf'
,
'to_excel'
,
'to_html'
,
'to_sql'
,
'to_msgpack'
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment