Skip to content

Commit 09cf3af

Browse files
committed
Add E8013, E8014 and E8015 + fix test names
- E8013: avoid `pickle.load()` and `pickle.loads()` - E8014: avoid `marshal.load()` and `marshal.loads()` - E8015: avoid `shelve.open()`
1 parent d268171 commit 09cf3af

File tree

7 files changed

+236
-8
lines changed

7 files changed

+236
-8
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Add plugin option to control whether we favour `os.open` over the builtin `open`
1313
- Added W8012 to warn when using `os.open` with unsafe permissions
14+
- Added E8013 to avoid using `pickle.load` and `pickle.loads`
15+
- Added E8014 to avoid using `marshal.load` and `marshal.loads`
16+
- Added E8015 to avoid using `shelve.open`
17+
18+
### Fixed
19+
20+
- Fixed a few test function names
1421

1522
### Repository
1623

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ pylint plugin that enforces some secure coding standards.
2525
| R8009 | Use of builtin `open` for writing is discouraged in favor of `os.open` to allow for setting file permissions |
2626
| E8010 | Avoid using `os.popen()` as it internally uses `subprocess.Popen` with `shell=True` |
2727
| E8011 | Use of `shlex.quote()` should be avoided on non-POSIX platforms |
28-
| W8012 | Avoid using `os.open` with unsafe permissions permissions |
28+
| W8012 | Avoid using `os.open()` with unsafe permissions permissions |
29+
| E8013 | Avoid using `pickle.load()` and `pickle.loads()` |
30+
| E8014 | Avoid using `marshal.load()` and `marshal.loads()` |
31+
| E8015 | Avoid using `shelve.open()` |
2932

3033

3134
## Plugin configuration options

pylint_secure_coding_standard.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ def _is_posix():
3636

3737

3838
def _is_function_call(node, module, function):
39+
if not isinstance(function, (list, tuple)):
40+
function = (function,)
3941
return (
4042
isinstance(node.func, astroid.Attribute)
4143
and isinstance(node.func.expr, astroid.Name)
4244
and node.func.expr.name == module
43-
and node.func.attrname == function
45+
and node.func.attrname in function
4446
)
4547

4648

@@ -314,6 +316,21 @@ class SecureCodingStandardChecker(BaseChecker):
314316
'os-open-unsafe-permissions',
315317
'Avoid using `os.open` with unsafe file permissions (by default 0 <= mode <= 0o755)',
316318
),
319+
'E8013': (
320+
'Avoid using `pickle.load` and `pickle.loads`',
321+
'avoid-pickle-load',
322+
'Use of `pickle.load` and `pickle.loads` should be avoided in favour of safer file formats',
323+
),
324+
'E8014': (
325+
'Avoid using `marshal.load` and `marshal.loads`',
326+
'avoid-marshal-load',
327+
'Use of `marshal.load` and `marshal.loads` should be avoided in favour of safer file formats',
328+
),
329+
'E8015': (
330+
'Avoid using `shelve.open()`',
331+
'avoid-shelve-open',
332+
'Use of `shelve.open()` should be avoided in favour of safer file formats',
333+
),
317334
}
318335

319336
def __init__(self, *args, **kwargs):
@@ -322,7 +339,7 @@ def __init__(self, *args, **kwargs):
322339
self._prefer_os_open = False
323340
self._os_open_modes_allowed = []
324341

325-
def visit_call(self, node):
342+
def visit_call(self, node): # pylint: disable=too-many-branches
326343
"""Visitor method called for astroid.Call nodes."""
327344
if _is_pdb_call(node):
328345
self.add_message('avoid-debug-stmt', node=node)
@@ -352,6 +369,12 @@ def visit_call(self, node):
352369
and not _is_os_open_allowed_mode(node, self._os_open_modes_allowed)
353370
):
354371
self.add_message('os-open-unsafe-permissions', node=node)
372+
elif _is_function_call(node, module='pickle', function=('load', 'loads')):
373+
self.add_message('avoid-pickle-load', node=node)
374+
elif _is_function_call(node, module='marshal', function=('load', 'loads')):
375+
self.add_message('avoid-marshal-load', node=node)
376+
elif _is_function_call(node, module='shelve', function='open'):
377+
self.add_message('avoid-shelve-open', node=node)
355378

356379
def visit_import(self, node):
357380
"""Visitor method called for astroid.Import nodes."""
@@ -381,18 +404,26 @@ def visit_importfrom(self, node):
381404
self.add_message('avoid-os-popen', node=node)
382405
elif not _is_posix() and node.modname == 'shlex' and [name for (name, _) in node.names if name == 'quote']:
383406
self.add_message('avoid-shlex-quote-on-non-posix', node=node)
407+
elif node.modname == 'pickle' and [name for (name, _) in node.names if name in ('load', 'loads')]:
408+
self.add_message('avoid-pickle-load', node=node)
409+
elif node.modname == 'marshal' and [name for (name, _) in node.names if name in ('load', 'loads')]:
410+
self.add_message('avoid-marshal-load', node=node)
411+
elif node.modname == 'shelve' and [name for (name, _) in node.names if name == 'open']:
412+
self.add_message('avoid-shelve-open', node=node)
384413

385414
def visit_with(self, node):
386415
"""Visitor method called for astroid.With nodes."""
387-
if self._prefer_os_open:
388-
for item in node.items:
389-
if item and isinstance(item[0], astroid.Call):
416+
for item in node.items:
417+
if item and isinstance(item[0], astroid.Call):
418+
if self._prefer_os_open:
390419
if _is_builtin_open_for_writing(item[0]):
391420
self.add_message('replace-builtin-open', node=node)
392421
elif _is_function_call(item[0], module='os', function='open') and not _is_os_open_allowed_mode(
393422
item[0], self._os_open_modes_allowed
394423
):
395424
self.add_message('os-open-unsafe-permissions', node=node)
425+
elif _is_function_call(item[0], module='shelve', function='open'):
426+
self.add_message('avoid-shelve-open', node=node)
396427

397428
def visit_assert(self, node):
398429
"""Visitor method called for astroid.Assert nodes."""

tests/jsonpickle_decode_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
class TestSecureCodingStandardChecker(pylint.testutils.CheckerTestCase):
2424
CHECKER_CLASS = pylint_scs.SecureCodingStandardChecker
2525

26-
def test_yaml_ok(self):
26+
def test_jsonpickle_decode_ok(self):
2727
nodes = astroid.extract_node(
2828
"""
2929
int(0) #@
@@ -41,7 +41,7 @@ def test_yaml_ok(self):
4141
's',
4242
('jsonpickle.decode(payload)',),
4343
)
44-
def test_yaml_not_ok(self, s):
44+
def test_jsonpickle_decode_not_ok(self, s):
4545
node = astroid.extract_node(s + ' #@')
4646
with self.assertAddsMessages(pylint.testutils.Message(msg_id='avoid-jsonpickle-decode', node=node)):
4747
self.checker.visit_call(node)

tests/marshal_load_test.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2021 Damien Nguyen
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import astroid
17+
import pylint.testutils
18+
import pytest
19+
20+
import pylint_secure_coding_standard as pylint_scs
21+
22+
23+
class TestSecureCodingStandardChecker(pylint.testutils.CheckerTestCase):
24+
CHECKER_CLASS = pylint_scs.SecureCodingStandardChecker
25+
26+
def test_marshal_load_ok(self):
27+
nodes = astroid.extract_node(
28+
"""
29+
int(0) #@
30+
foo() #@
31+
marshal.dump(data, "file.txt") #@
32+
marshal.dumps(data) #@
33+
"""
34+
)
35+
36+
with self.assertNoMessages():
37+
for node in nodes:
38+
self.checker.visit_call(node)
39+
40+
@pytest.mark.parametrize(
41+
's',
42+
(
43+
'marshal.load("file.txt")',
44+
r'marshal.loads(b"\xe9\x01\x00\x00\x00")',
45+
'marshal.loads(data)',
46+
),
47+
)
48+
def test_marshal_load_not_ok(self, s):
49+
node = astroid.extract_node(s + ' #@')
50+
with self.assertAddsMessages(pylint.testutils.Message(msg_id='avoid-marshal-load', node=node)):
51+
self.checker.visit_call(node)
52+
53+
@pytest.mark.parametrize(
54+
's',
55+
(
56+
'from marshal import load',
57+
'from marshal import loads',
58+
'from marshal import dump, load',
59+
),
60+
)
61+
def test_marshal_open_importfrom(self, s):
62+
node = astroid.extract_node(s + ' #@')
63+
with self.assertAddsMessages(pylint.testutils.Message(msg_id='avoid-marshal-load', node=node)):
64+
self.checker.visit_importfrom(node)

tests/pickle_load_test.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2021 Damien Nguyen
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import astroid
17+
import pylint.testutils
18+
import pytest
19+
20+
import pylint_secure_coding_standard as pylint_scs
21+
22+
23+
class TestSecureCodingStandardChecker(pylint.testutils.CheckerTestCase):
24+
CHECKER_CLASS = pylint_scs.SecureCodingStandardChecker
25+
26+
def test_pickle_load_ok(self):
27+
nodes = astroid.extract_node(
28+
"""
29+
int(0) #@
30+
foo() #@
31+
pickle.dump(data, "file.txt") #@
32+
pickle.dumps(data) #@
33+
"""
34+
)
35+
36+
with self.assertNoMessages():
37+
for node in nodes:
38+
self.checker.visit_call(node)
39+
40+
@pytest.mark.parametrize(
41+
's',
42+
(
43+
'pickle.load("file.txt")',
44+
r'pickle.loads(b"\x80\x04K\x01.")',
45+
'pickle.loads(data)',
46+
),
47+
)
48+
def test_pickle_load_not_ok(self, s):
49+
node = astroid.extract_node(s + ' #@')
50+
with self.assertAddsMessages(pylint.testutils.Message(msg_id='avoid-pickle-load', node=node)):
51+
self.checker.visit_call(node)
52+
53+
@pytest.mark.parametrize(
54+
's',
55+
(
56+
'from pickle import load',
57+
'from pickle import loads',
58+
'from pickle import dump, load',
59+
),
60+
)
61+
def test_pickle_open_importfrom(self, s):
62+
node = astroid.extract_node(s + ' #@')
63+
with self.assertAddsMessages(pylint.testutils.Message(msg_id='avoid-pickle-load', node=node)):
64+
self.checker.visit_importfrom(node)

tests/shelve_open_test.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2021 Damien Nguyen
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import astroid
17+
import pylint.testutils
18+
import pytest
19+
20+
import pylint_secure_coding_standard as pylint_scs
21+
22+
23+
class TestSecureCodingStandardChecker(pylint.testutils.CheckerTestCase):
24+
CHECKER_CLASS = pylint_scs.SecureCodingStandardChecker
25+
26+
def test_shelve_open_ok(self):
27+
nodes = astroid.extract_node(
28+
"""
29+
int(0) #@
30+
foo() #@
31+
"""
32+
)
33+
34+
with self.assertNoMessages():
35+
for node in nodes:
36+
self.checker.visit_call(node)
37+
38+
_not_ok = (
39+
'shelve.open("file.txt")',
40+
'shelve.open(filename)',
41+
)
42+
43+
@pytest.mark.parametrize('s', _not_ok)
44+
def test_shelve_open_call(self, s):
45+
node = astroid.extract_node(s + ' #@')
46+
with self.assertAddsMessages(pylint.testutils.Message(msg_id='avoid-shelve-open', node=node)):
47+
self.checker.visit_call(node)
48+
49+
@pytest.mark.parametrize('s', _not_ok)
50+
def test_shelve_open_with(self, s):
51+
node = astroid.extract_node(f'with {s} as fd: fd.read() #@')
52+
with self.assertAddsMessages(pylint.testutils.Message(msg_id='avoid-shelve-open', node=node)):
53+
self.checker.visit_with(node)
54+
55+
@pytest.mark.parametrize('s', ('from shelve import open',))
56+
def test_shelve_open_importfrom(self, s):
57+
node = astroid.extract_node(s + ' #@')
58+
with self.assertAddsMessages(pylint.testutils.Message(msg_id='avoid-shelve-open', node=node)):
59+
self.checker.visit_importfrom(node)

0 commit comments

Comments
 (0)