Skip to content

Commit 8787e73

Browse files
committed
Reword E8003 and extend it to cover a few more cases
1 parent 511a33a commit 8787e73

File tree

4 files changed

+63
-15
lines changed

4 files changed

+63
-15
lines changed

CHANGELOG.md

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

88
## [Unreleased]
99

10+
- Reworded E8003 and extend it to include a few more cases:
11+
+ `subprocess.getoutput()`
12+
+ `subprocess.getstatusoutput()`
13+
+ `asyncio.create_subprocess_shell()`
14+
+ `loop.subprocess_shell()`
15+
1016
## [1.2.0] - 2021-07-19
1117

1218
### Added

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pylint plugin that enforces some secure coding standards.
1616
| R8000 | Use `os.path.realpath()` instead of `os.path.abspath()` and `os.path.relpath()` |
1717
| E8001 | Avoid using `exec()` and `eval()` |
1818
| E8002 | Avoid using `os.sytem()` |
19-
| E8003 | Avoid using `shell=True` when calling `subprocess` functions |
19+
| E8003 | Avoid using `shell=True` in subprocess functions or using functions that internally set this |
2020
| R8004 | Avoid using `tempfile.mktemp()`, prefer `tempfile.mkstemp()` instead |
2121
| E8005 | Avoid using unsafe PyYAML loading functions |
2222
| E8006 | Avoid using `jsonpickle.decode()` |

pylint_secure_coding_standard.py

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,19 +95,34 @@ def _is_builtin_open_for_writing(node):
9595
return False
9696

9797

98-
def _is_subprocess_shell_true_call(node):
99-
if (
100-
isinstance(node.func, astroid.Attribute)
101-
and isinstance(node.func.expr, astroid.Name)
102-
and node.func.expr.name in ('subprocess', 'sp')
103-
):
104-
if node.keywords:
105-
for keyword in node.keywords:
106-
if keyword.arg == 'shell' and isinstance(keyword.value, astroid.Const) and bool(keyword.value.value):
107-
return True
98+
def _is_shell_true_call(node):
99+
if not (isinstance(node.func, astroid.Attribute) and isinstance(node.func.expr, astroid.Name)):
100+
return False
101+
102+
# subprocess module
103+
if node.func.expr.name in ('subprocess', 'sp'):
104+
if node.func.attrname in ('call', 'check_call', 'check_output', 'Popen', 'run'):
105+
if node.keywords:
106+
for keyword in node.keywords:
107+
if (
108+
keyword.arg == 'shell'
109+
and isinstance(keyword.value, astroid.Const)
110+
and bool(keyword.value.value)
111+
):
112+
return True
108113

109-
if len(node.args) > 8 and isinstance(node.args[8], astroid.Const) and bool(node.args[8].value):
114+
if len(node.args) > 8 and isinstance(node.args[8], astroid.Const) and bool(node.args[8].value):
115+
return True
116+
117+
if node.func.attrname in ('getoutput', 'getstatusoutput'):
110118
return True
119+
120+
# asyncio module
121+
if (node.func.expr.name == 'asyncio' and node.func.attrname == 'create_subprocess_shell') or (
122+
node.func.expr.name == 'loop' and node.func.attrname == 'subprocess_shell'
123+
):
124+
return True
125+
111126
return False
112127

113128

@@ -229,9 +244,14 @@ class SecureCodingStandardChecker(BaseChecker):
229244
),
230245
'E8002': ('Avoid using `os.sytem()`', 'avoid-os-system', 'Use of `os.system()` should be avoided'),
231246
'E8003': (
232-
'Avoid using `shell=True` when calling `subprocess` functions',
247+
'Avoid using `shell=True` when calling `subprocess` functions and avoid functions that internally call it',
233248
'avoid-shell-true',
234-
'Use of `shell=True` in subprocess functions should be avoided',
249+
' '.join(
250+
[
251+
'Use of `shell=True` in subprocess functions or use of functions that internally set it should be'
252+
'should be avoided',
253+
]
254+
),
235255
),
236256
'R8004': (
237257
'Avoid using `tempfile.mktemp()`, prefer `tempfile.mkstemp()` instead',
@@ -292,7 +312,7 @@ def visit_call(self, node):
292312
self.add_message('avoid-os-system', node=node)
293313
elif _is_os_path_call(node):
294314
self.add_message('replace-os-relpath-abspath', node=node)
295-
elif _is_subprocess_shell_true_call(node):
315+
elif _is_shell_true_call(node):
296316
self.add_message('avoid-shell-true', node=node)
297317
elif _is_os_popen_call(node):
298318
self.add_message('avoid-os-popen', node=node)
@@ -320,6 +340,11 @@ def visit_importfrom(self, node):
320340
self.add_message('replace-mktemp', node=node)
321341
elif node.modname in ('os.path', 'op') and [name for (name, _) in node.names if name in ('relpath', 'abspath')]:
322342
self.add_message('replace-os-relpath-abspath', node=node)
343+
elif (
344+
node.modname == 'subprocess'
345+
and [name for (name, _) in node.names if name in ('getoutput', 'getstatusoutput')]
346+
) or ((node.modname == 'asyncio' and [name for (name, _) in node.names if name == 'create_subprocess_shell'])):
347+
self.add_message('avoid-shell-true', node=node)
323348
elif node.modname == 'os' and [name for (name, _) in node.names if name == 'system']:
324349
self.add_message('avoid-os-system', node=node)
325350
elif node.modname == 'os' and [name for (name, _) in node.names if name == 'popen']:

tests/shell_exec_test.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ def test_shell_true_ok(self):
5353
('from os import system as os_system', 'avoid-os-system'),
5454
('from os import popen', 'avoid-os-popen'),
5555
('from os import popen as os_popen', 'avoid-os-popen'),
56+
('from subprocess import getoutput', 'avoid-shell-true'),
57+
('from subprocess import getoutput as sp_getoutput', 'avoid-shell-true'),
58+
('from subprocess import getstatusoutput', 'avoid-shell-true'),
59+
('from subprocess import getstatusoutput as sp_getstatusoutput', 'avoid-shell-true'),
60+
('from asyncio import create_subprocess_shell', 'avoid-shell-true'),
61+
('from asyncio import create_subprocess_shell as create_sp_shell', 'avoid-shell-true'),
5662
),
5763
)
5864
def test_shell_true_importfrom(self, s, msg_id):
@@ -74,6 +80,17 @@ def test_shell_true_importfrom(self, s, msg_id):
7480
('sp.check_call(["cat", "/etc/passwd"], shell=True)', 'avoid-shell-true'),
7581
('subprocess.check_output(["cat", "/etc/passwd"], shell=True)', 'avoid-shell-true'),
7682
('sp.check_output(["cat", "/etc/passwd"], shell=True)', 'avoid-shell-true'),
83+
('subprocess.getoutput("ls /bin/ls")', 'avoid-shell-true'),
84+
('sp.getoutput("ls /bin/ls")', 'avoid-shell-true'),
85+
('subprocess.getstatusoutput("ls /bin/ls")', 'avoid-shell-true'),
86+
('sp.getstatusoutput("ls /bin/ls")', 'avoid-shell-true'),
87+
('asyncio.create_subprocess_shell("ls /bin/ls")', 'avoid-shell-true'),
88+
('asyncio.create_subprocess_shell(cmd)', 'avoid-shell-true'),
89+
('asyncio.create_subprocess_shell("ls /bin/ls", stdin=PIPE, stdout=PIPE)', 'avoid-shell-true'),
90+
('asyncio.create_subprocess_shell(cmd, stdin=PIPE, stdout=PIPE)', 'avoid-shell-true'),
91+
('loop.subprocess_shell(asyncio.SubprocessProtocol, "ls /bin/ls")', 'avoid-shell-true'),
92+
('loop.subprocess_shell(asyncio.SubprocessProtocol, cmd)', 'avoid-shell-true'),
93+
('loop.subprocess_shell(asyncio.SubprocessProtocol, cmd, **kwds)', 'avoid-shell-true'),
7794
('os.popen("cat")', 'avoid-os-popen'),
7895
('os.popen("cat", "r")', 'avoid-os-popen'),
7996
('os.popen("cat", "r", 1)', 'avoid-os-popen'),

0 commit comments

Comments
 (0)