Skip to content

Commit 5cc8594

Browse files
committed
Reword SCS103 and extend it to cover a few more cases
1 parent df2eb17 commit 5cc8594

4 files changed

Lines changed: 116 additions & 71 deletions

File tree

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 SCS103 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 SCS110 to avoid using `os.popen()` as it internally uses `subprocess.Popen` with `shell=True`

README.md

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,20 @@ flake8 plugin that enforces some secure coding standards.
1111

1212
## Flake8 codes
1313

14-
| Code | Description |
15-
|--------|--------------------------------------------------------------------------------------------------------------|
16-
| SCS100 | Use of `os.path.abspath()` and `os.path.relpath()` should be avoided in favor of `os.path.realpath()` |
17-
| SCS101 | Use of `eval()` and `exec()` represent a security risk and should be avoided |
18-
| SCS102 | Use of `os.system()` should be avoided |
19-
| SCS103 | Use of `shell=True` in subprocess functions should be avoided |
20-
| SCS104 | Use of `tempfile.mktemp()` should be avoided, prefer `tempfile.mkstemp()` |
21-
| SCS105 | Use of `yaml.load()` should be avoided, prefer `yaml.safe_load()` or `yaml.load(xxx, Loader=SafeLoader)` |
22-
| SCS106 | Use of `jsonpickle.decode()` should be avoided |
23-
| SCS107 | Use of debugging code shoud not be present in production code (e.g. `import pdb`) |
24-
| SCS108 | `assert` statements should not be present in production code |
25-
| SCS109 | Use of builtin `open` for writing is discouraged in favor of `os.open` to allow for setting file permissions |
26-
| SCS110 | Avoid using `os.popen()` as it internally uses `subprocess.Popen` with `shell=True` |
27-
| SCS111 | Use of `shlex.quote()` should be avoided on non-POSIX platforms |
14+
| Code | Description |
15+
|--------|---------------------------------------------------------------------------------------------------------------|
16+
| SCS100 | Use of `os.path.abspath()` and `os.path.relpath()` should be avoided in favor of `os.path.realpath()` |
17+
| SCS101 | Use of `eval()` and `exec()` represent a security risk and should be avoided |
18+
| SCS102 | Use of `os.system()` should be avoided |
19+
| SCS103 | Use of `shell=True` in subprocess functions or use of functions that internally set this should be avoided |
20+
| SCS104 | Use of `tempfile.mktemp()` should be avoided, prefer `tempfile.mkstemp()` |
21+
| SCS105 | Use of `yaml.load()` should be avoided, prefer `yaml.safe_load()` or `yaml.load(xxx, Loader=SafeLoader)` |
22+
| SCS106 | Use of `jsonpickle.decode()` should be avoided |
23+
| SCS107 | Use of debugging code shoud not be present in production code (e.g. `import pdb`) |
24+
| SCS108 | `assert` statements should not be present in production code |
25+
| SCS109 | Use of builtin `open` for writing is discouraged in favor of `os.open` to allow for setting file permissions |
26+
| SCS110 | Avoid using `os.popen()` as it internally uses `subprocess.Popen` with `shell=True` |
27+
| SCS111 | Use of `shlex.quote()` should be avoided on non-POSIX platforms |
2828

2929
## Pre-commit hook
3030

@@ -33,7 +33,7 @@ See [pre-commit](https://github.com/pre-commit/pre-commit) for instructions
3333
Sample `.pre-commit-config.yaml`:
3434

3535
```yaml
36-
- repo: https://github.com/PyCQA/flake8
36+
- repo: https://github.com/PyCQA/flake8g
3737
rev: 3.7.8
3838
hooks:
3939
- id: flake8

flake8_secure_coding_standard.py

Lines changed: 56 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,38 +20,43 @@
2020
import sys
2121
from typing import Any, Dict, Generator, List, Tuple, Type
2222

23-
if sys.version_info < (3, 8): # pragma: no cover (<PY38)
23+
if sys.version_info < (3, 8): # pragma: no cover
2424
import importlib_metadata # pylint: disable=E0401
2525

2626
ast_Constant = ast.NameConstant # pylint: disable=invalid-name
27-
else: # pragma: no cover (PY38+)
27+
else: # pragma: no cover
2828
ast_Constant = ast.Constant
2929
import importlib.metadata as importlib_metadata
3030

3131
# ==============================================================================
3232

33-
SCS100 = (
34-
'SCS100 use of os.path.abspath() and os.path.relpath() should be avoided in favor of ' + 'os.path.realpath()'
35-
) # noqa: E501
36-
SCS101 = 'SCS101 `eval()` and `exec()` represent a security risk and should be avoided' # noqa: E501
37-
SCS102 = 'SCS102 use of `os.system()` should be avoided' # noqa: E501
38-
SCS103 = 'SCS103 use of `shell=True` in subprocess functions should be avoided' # noqa: E501
39-
SCS104 = 'SCS104 use of `tempfile.mktemp()` should be avoided, prefer `tempfile.mkstemp()`' # noqa: E501
40-
SCS105 = (
41-
'SCS105 use of `yaml.load()` should be avoided, prefer `yaml.safe_load()` or '
42-
+ '`yaml.load(xxx, Loader=SafeLoader)`'
43-
) # noqa: E501
44-
SCS106 = 'SCS106 use of `jsonpickle.decode()` should be avoided' # noqa: E501
45-
SCS107 = 'SCS107 debugging code shoud not be present in production code (e.g. `import pdb`)' # noqa: E501
46-
SCS108 = 'SCS108 `assert` statements should not be present in production code' # noqa: E501
47-
SCS109 = (
48-
'SCS109 Use of builtin `open` for writing is discouraged in favor of `os.open` '
49-
+ 'to allow for setting file permissions'
50-
) # noqa: E501
51-
SCS110 = (
52-
'Use of `os.popen()` should be avoided, as it internally uses `subprocess.Popen` with `shell=True`' # noqa: E501
33+
SCS100 = 'SCS100 use of os.path.abspath() and os.path.relpath() should be avoided in favor of os.path.realpath()'
34+
SCS101 = 'SCS101 `eval()` and `exec()` represent a security risk and should be avoided'
35+
SCS102 = 'SCS102 use of `os.system()` should be avoided'
36+
SCS103 = ' '.join(
37+
[
38+
'SCS103 use of `shell=True` in subprocess functions or use of functions that internally set it should be',
39+
'avoided',
40+
]
5341
)
54-
SCS111 = 'Use of `shlex.quote()` should be avoided on non-POSIX platforms (such as Windows)' # noqa: E501
42+
SCS104 = 'SCS104 use of `tempfile.mktemp()` should be avoided, prefer `tempfile.mkstemp()`'
43+
SCS105 = ' '.join(
44+
[
45+
'SCS105 use of `yaml.load()` should be avoided, prefer `yaml.safe_load()` or',
46+
'`yaml.load(xxx, Loader=SafeLoader)`',
47+
]
48+
)
49+
SCS106 = 'SCS106 use of `jsonpickle.decode()` should be avoided'
50+
SCS107 = 'SCS107 debugging code shoud not be present in production code (e.g. `import pdb`)'
51+
SCS108 = 'SCS108 `assert` statements should not be present in production code'
52+
SCS109 = ' '.join(
53+
[
54+
'SCS109 Use of builtin `open` for writing is discouraged in favor of `os.open` to allow for setting file',
55+
'permissions',
56+
]
57+
)
58+
SCS110 = 'Use of `os.popen()` should be avoided, as it internally uses `subprocess.Popen` with `shell=True`'
59+
SCS111 = 'Use of `shlex.quote()` should be avoided on non-POSIX platforms (such as Windows)'
5560

5661

5762
# ==============================================================================
@@ -129,18 +134,27 @@ def _is_builtin_open_for_writing(node: ast.Call) -> bool:
129134
return False
130135

131136

132-
def _is_subprocess_shell_true_call(node: ast.Call) -> bool:
133-
if (
134-
isinstance(node.func, ast.Attribute)
135-
and isinstance(node.func.value, ast.Name)
136-
and node.func.value.id in ('subprocess', 'sp')
137-
):
138-
for keyword in node.keywords:
139-
if keyword.arg == 'shell' and isinstance(keyword.value, ast_Constant) and bool(keyword.value.value):
140-
return True
137+
def _is_shell_true_call(node: ast.Call) -> bool:
138+
if not (isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name)):
139+
return False
141140

142-
if len(node.args) > 8 and isinstance(node.args[8], ast_Constant) and bool(node.args[8].value):
141+
# subprocess module
142+
if node.func.value.id in ('subprocess', 'sp'):
143+
if node.func.attr in ('call', 'check_call', 'check_output', 'Popen', 'run'):
144+
for keyword in node.keywords:
145+
if keyword.arg == 'shell' and isinstance(keyword.value, ast_Constant) and bool(keyword.value.value):
146+
return True
147+
if len(node.args) > 8 and isinstance(node.args[8], ast_Constant) and bool(node.args[8].value):
148+
return True
149+
if node.func.attr in ('getoutput', 'getstatusoutput'):
143150
return True
151+
152+
# asyncio module
153+
if (node.func.value.id == 'asyncio' and node.func.attr == 'create_subprocess_shell') or (
154+
node.func.value.id == 'loop' and node.func.attr == 'subprocess_shell'
155+
):
156+
return True
157+
144158
return False
145159

146160

@@ -254,7 +268,7 @@ def visit_Call(self, node: ast.Call) -> None:
254268
self.errors.append((node.lineno, node.col_offset, SCS100))
255269
elif _is_os_popen_call(node):
256270
self.errors.append((node.lineno, node.col_offset, SCS110))
257-
elif _is_subprocess_shell_true_call(node):
271+
elif _is_shell_true_call(node):
258272
self.errors.append((node.lineno, node.col_offset, SCS103))
259273
elif _is_builtin_open_for_writing(node):
260274
self.errors.append((node.lineno, node.col_offset, SCS109))
@@ -292,6 +306,14 @@ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
292306
# * from os.path import relpath, abspath
293307
# * import os.path as op; from op import relpath, abspath
294308
self.errors.append((node.lineno, node.col_offset, SCS100))
309+
elif (node.module == 'subprocess' and alias.name in ('getoutput', 'getstatusoutput')) or (
310+
(node.module == 'asyncio' and alias.name == 'create_subprocess_shell')
311+
):
312+
# Cover:
313+
# * from subprocess import getoutput
314+
# * from subprocess import getstatusoutput
315+
# * from asyncio import create_subprocess_shell
316+
self.errors.append((node.lineno, node.col_offset, SCS103))
295317
elif node.module == 'os' and alias.name == 'system':
296318
# Cover:
297319
# * from os import system

tests/shell_exec_test.py

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -52,28 +52,45 @@ def test_shell_exec_ok(s):
5252
@pytest.mark.parametrize(
5353
's, msg_id',
5454
(
55-
('os.system("ls -l")', flake8_scs.SCS102),
56-
('from os import system', flake8_scs.SCS102),
57-
('from os import system as os_system', flake8_scs.SCS102),
58-
('from os import popen', flake8_scs.SCS110),
59-
('from os import popen as os_popen', flake8_scs.SCS110),
60-
('subprocess.Popen(["cat", "/etc/passwd"], b, e, i, o, e, pre, c, True)', flake8_scs.SCS103),
61-
('subprocess.Popen(["cat", "/etc/passwd"], b, e, i, o, e, pre, c, True, cwd)', flake8_scs.SCS103),
62-
('subprocess.run(["cat", "/etc/passwd"], shell=True)', flake8_scs.SCS103),
63-
('sp.run(["cat", "/etc/passwd"], shell=True)', flake8_scs.SCS103),
64-
('subprocess.call(["cat", "/etc/passwd"], shell=True)', flake8_scs.SCS103),
65-
('sp.call(["cat", "/etc/passwd"], shell=True)', flake8_scs.SCS103),
66-
('subprocess.check_call(["cat", "/etc/passwd"], shell=True)', flake8_scs.SCS103),
67-
('sp.check_call(["cat", "/etc/passwd"], shell=True)', flake8_scs.SCS103),
68-
('subprocess.check_output(["cat", "/etc/passwd"], shell=True)', flake8_scs.SCS103),
69-
('sp.check_output(["cat", "/etc/passwd"], shell=True)', flake8_scs.SCS103),
70-
('os.popen("cat")', flake8_scs.SCS110),
71-
('os.popen("cat", "r")', flake8_scs.SCS110),
72-
('os.popen("cat", "r", 1)', flake8_scs.SCS110),
73-
('os.popen("cat", buffering=1)', flake8_scs.SCS110),
74-
('os.popen("cat", mode="w")', flake8_scs.SCS110),
75-
('os.popen("cat", mode="w", buffering=1)', flake8_scs.SCS110),
55+
('os.system("ls -l")', 'SCS102'),
56+
('from os import system', 'SCS102'),
57+
('from os import system as os_system', 'SCS102'),
58+
('from os import popen', 'SCS110'),
59+
('from os import popen as os_popen', 'SCS110'),
60+
('from subprocess import getoutput', 'SCS103'),
61+
('from subprocess import getoutput as sp_getoutput', 'SCS103'),
62+
('from subprocess import getstatusoutput', 'SCS103'),
63+
('from subprocess import getstatusoutput as sp_getstatusoutput', 'SCS103'),
64+
('from asyncio import create_subprocess_shell', 'SCS103'),
65+
('from asyncio import create_subprocess_shell as create_sp_shell', 'SCS103'),
66+
('subprocess.Popen(["cat", "/etc/passwd"], b, e, i, o, e, pre, c, True)', 'SCS103'),
67+
('subprocess.Popen(["cat", "/etc/passwd"], b, e, i, o, e, pre, c, True, cwd)', 'SCS103'),
68+
('subprocess.run(["cat", "/etc/passwd"], shell=True)', 'SCS103'),
69+
('sp.run(["cat", "/etc/passwd"], shell=True)', 'SCS103'),
70+
('subprocess.call(["cat", "/etc/passwd"], shell=True)', 'SCS103'),
71+
('sp.call(["cat", "/etc/passwd"], shell=True)', 'SCS103'),
72+
('subprocess.check_call(["cat", "/etc/passwd"], shell=True)', 'SCS103'),
73+
('sp.check_call(["cat", "/etc/passwd"], shell=True)', 'SCS103'),
74+
('subprocess.check_output(["cat", "/etc/passwd"], shell=True)', 'SCS103'),
75+
('sp.check_output(["cat", "/etc/passwd"], shell=True)', 'SCS103'),
76+
('subprocess.getoutput("ls /bin/ls")', 'SCS103'),
77+
('sp.getoutput("ls /bin/ls")', 'SCS103'),
78+
('subprocess.getstatusoutput("ls /bin/ls")', 'SCS103'),
79+
('sp.getstatusoutput("ls /bin/ls")', 'SCS103'),
80+
('asyncio.create_subprocess_shell("ls /bin/ls")', 'SCS103'),
81+
('asyncio.create_subprocess_shell(cmd)', 'SCS103'),
82+
('asyncio.create_subprocess_shell("ls /bin/ls", stdin=PIPE, stdout=PIPE)', 'SCS103'),
83+
('asyncio.create_subprocess_shell(cmd, stdin=PIPE, stdout=PIPE)', 'SCS103'),
84+
('loop.subprocess_shell(asyncio.SubprocessProtocol, "ls /bin/ls")', 'SCS103'),
85+
('loop.subprocess_shell(asyncio.SubprocessProtocol, cmd)', 'SCS103'),
86+
('loop.subprocess_shell(asyncio.SubprocessProtocol, cmd, **kwds)', 'SCS103'),
87+
('os.popen("cat")', 'SCS110'),
88+
('os.popen("cat", "r")', 'SCS110'),
89+
('os.popen("cat", "r", 1)', 'SCS110'),
90+
('os.popen("cat", buffering=1)', 'SCS110'),
91+
('os.popen("cat", mode="w")', 'SCS110'),
92+
('os.popen("cat", mode="w", buffering=1)', 'SCS110'),
7693
),
7794
)
7895
def test_shell_exec_not_ok(s, msg_id):
79-
assert results(s) == {'1:0: ' + msg_id}
96+
assert results(s) == {'1:0: ' + getattr(flake8_scs, msg_id)}

0 commit comments

Comments
 (0)