1616"""Main file for the flake8_secure_coding_standard plugin."""
1717
1818import ast
19+ import platform
1920import sys
2021from typing import Any , Dict , Generator , List , Tuple , Type
2122
2223if sys .version_info < (3 , 8 ): # pragma: no cover (<PY38)
2324 import importlib_metadata # pylint: disable=E0401
2425
25- ast_Constant = ast .NameConstant
26+ ast_Constant = ast .NameConstant # pylint: disable=invalid-name
2627else : # pragma: no cover (PY38+)
2728 ast_Constant = ast .Constant
2829 import importlib .metadata as importlib_metadata
4748 'SCS109 Use of builtin `open` for writing is discouraged in favor of `os.open` '
4849 + 'to allow for setting file permissions'
4950) # noqa: E501
51+ SCS110 = (
52+ 'Use of `os.popen()` should be avoided, as it internally uses `subprocess.Popen` with `shell=True`' # noqa: E501
53+ )
54+ SCS111 = 'Use of `shlex.quote()` should be avoided on non-POSIX platforms (such as Windows)' # noqa: E501
55+
56+
57+ # ==============================================================================
58+ # Helper functions
59+
60+
61+ def _is_posix ():
62+ """Return True if the current system is POSIX-compatible."""
63+ # NB: we could simply use `os.name` instead of `platform.system()`. However, that solution would be difficult to
64+ # test using `mock` as a few modules (like `pytest`) actually use it internally...
65+ return platform .system () in ('Linux' , 'Darwin' )
66+
67+
68+ # ==============================================================================
5069
5170
5271def _is_os_system_call (node : ast .Call ) -> bool :
@@ -58,6 +77,15 @@ def _is_os_system_call(node: ast.Call) -> bool:
5877 )
5978
6079
80+ def _is_os_popen_call (node : ast .Call ) -> bool :
81+ return (
82+ isinstance (node .func , ast .Attribute )
83+ and isinstance (node .func .value , ast .Name )
84+ and node .func .value .id == 'os'
85+ and node .func .attr == 'popen'
86+ )
87+
88+
6189def _is_os_path_call (node : ast .Call ) -> bool :
6290 return (
6391 isinstance (node .func , ast .Attribute ) # pylint: disable=R0916
@@ -193,6 +221,15 @@ def _is_jsonpickle_encode_call(node: ast.Call) -> bool:
193221 return False
194222
195223
224+ def _is_shlex_quote_call (node : ast .Call ) -> bool :
225+ return not _is_posix () and (
226+ isinstance (node .func , ast .Attribute )
227+ and isinstance (node .func .value , ast .Name )
228+ and node .func .value .id == 'shlex'
229+ and node .func .attr == 'quote'
230+ )
231+
232+
196233class Visitor (ast .NodeVisitor ):
197234 """AST visitor class for the plugin."""
198235
@@ -215,12 +252,17 @@ def visit_Call(self, node: ast.Call) -> None:
215252 self .errors .append ((node .lineno , node .col_offset , SCS102 ))
216253 elif _is_os_path_call (node ):
217254 self .errors .append ((node .lineno , node .col_offset , SCS100 ))
255+ elif _is_os_popen_call (node ):
256+ self .errors .append ((node .lineno , node .col_offset , SCS110 ))
218257 elif _is_subprocess_shell_true_call (node ):
219258 self .errors .append ((node .lineno , node .col_offset , SCS103 ))
220259 elif _is_builtin_open_for_writing (node ):
221260 self .errors .append ((node .lineno , node .col_offset , SCS109 ))
222261 elif isinstance (node .func , ast .Name ) and (node .func .id in ('eval' , 'exec' )):
223262 self .errors .append ((node .lineno , node .col_offset , SCS101 ))
263+ elif _is_shlex_quote_call (node ):
264+ self .errors .append ((node .lineno , node .col_offset , SCS111 ))
265+
224266 self .generic_visit (node )
225267
226268 def visit_Import (self , node : ast .Import ) -> None :
@@ -254,6 +296,15 @@ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
254296 # Cover:
255297 # * from os import system
256298 self .errors .append ((node .lineno , node .col_offset , SCS102 ))
299+ elif node .module == 'os' and alias .name == 'popen' :
300+ # Cover:
301+ # * from os import popen
302+ self .errors .append ((node .lineno , node .col_offset , SCS110 ))
303+ elif not _is_posix () and node .module == 'shlex' and alias .name == 'quote' :
304+ # Cover:
305+ # * from shlex import quote
306+ # * from shlex import quote as quoted
307+ self .errors .append ((node .lineno , node .col_offset , SCS111 ))
257308
258309 self .generic_visit (node )
259310
0 commit comments