Skip to content

Commit 9d10919

Browse files
WIP: Add E8011 discouraging shlex.quote() usages on Windows (#7)
* WIP: Add E8010 discouraging shlex.quote() usages on Windows TODO : write Pylint tests * Add unit tests and minor improvements * Update CHANGELOG * Fix typo during last merge commit Co-authored-by: Nguyen Damien <[email protected]>
1 parent d2e79ec commit 9d10919

File tree

6 files changed

+106
-0
lines changed

6 files changed

+106
-0
lines changed

CHANGELOG.md

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

1212
- Added E8010 to avoid using `os.popen()` as it internally uses `subprocess.Popen` with `shell=True`
13+
- Added E8011 to avoid using `shlex.quote()` on non-POSIX platforms.
1314

1415
## [1.1.0] - 2021-07-02
1516

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pylint plugin that enforces some secure coding standards.
2424
| C8008 | Avoid `assert` statements in production code |
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` |
27+
| E8011 | Use of `shlex.quote()` should be avoided on non-POSIX platforms |
2728

2829

2930
## Pre-commit hook

pylint_secure_coding_standard.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,25 @@
1515

1616
"""Main file for the pylint_secure_coding_standard plugin"""
1717

18+
import platform
19+
1820
import astroid
1921

2022
from pylint.checkers import BaseChecker
2123
from pylint.interfaces import IAstroidChecker
2224

2325

26+
# ==============================================================================
27+
# Helper functions
28+
29+
30+
def _is_posix():
31+
"""Return True if the current system is POSIX-compatible."""
32+
# NB: we could simply use `os.name` instead of `platform.system()`. However, that solution would be difficult to
33+
# test using `mock` as a few modules (like `pytest`) actually use it internally...
34+
return platform.system() in ('Linux', 'Darwin')
35+
36+
2437
# ==============================================================================
2538

2639

@@ -171,6 +184,15 @@ def _is_jsonpickle_encode_call(node):
171184
return False
172185

173186

187+
def _is_shlex_quote_call(node):
188+
return not _is_posix() and (
189+
isinstance(node.func, astroid.Attribute)
190+
and isinstance(node.func.expr, astroid.Name)
191+
and node.func.expr.name == 'shlex'
192+
and node.func.attrname == 'quote'
193+
)
194+
195+
174196
# ==============================================================================
175197

176198

@@ -235,6 +257,11 @@ class SecureCodingStandardChecker(BaseChecker):
235257
'avoid-os-popen',
236258
'Use of `os.popen()` should be avoided, as it internally uses `subprocess.Popen` with `shell=True`',
237259
),
260+
'E8011': (
261+
'Avoid using `shlex.quote()` on non-POSIX platforms',
262+
'avoid-shlex-quote-on-non-posix',
263+
'Use of `shlex.quote()` should be avoided on non-POSIX platforms (such as Windows)',
264+
),
238265
}
239266

240267
options = {}
@@ -263,6 +290,8 @@ def visit_call(self, node):
263290
self.add_message('replace-builtin-open', node=node)
264291
elif isinstance(node.func, astroid.Name) and (node.func.name in ('eval', 'exec')):
265292
self.add_message('avoid-eval-exec', node=node)
293+
elif _is_shlex_quote_call(node):
294+
self.add_message('avoid-shlex-quote-on-non-posix', node=node)
266295

267296
def visit_import(self, node):
268297
"""
@@ -288,6 +317,8 @@ def visit_importfrom(self, node):
288317
self.add_message('avoid-os-system', node=node)
289318
elif node.modname == 'os' and [name for (name, _) in node.names if name == 'popen']:
290319
self.add_message('avoid-os-popen', node=node)
320+
elif not _is_posix() and node.modname == 'shlex' and [name for (name, _) in node.names if name == 'quote']:
321+
self.add_message('avoid-shlex-quote-on-non-posix', node=node)
291322

292323
def visit_with(self, node):
293324
"""

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ build-backend = "setuptools.build_meta"
6666
minversion = '6.0'
6767
addopts = '-pno:warnings'
6868
testpaths = ['tests']
69+
mock_traceback_monkeypatch = false
6970

7071
[tool.setuptools_scm]
7172
write_to = 'VERSION.txt'

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@ install_requires =
3131

3232
[options.extras_require]
3333
test =
34+
mock
3435
pytest
3536
pytest-cov
37+
pytest-mock
3638

3739

3840
[bdist_wheel]

tests/shlex_quote_test.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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 pytest
18+
19+
import pylint_secure_coding_standard as pylint_scs
20+
import pylint.testutils
21+
22+
23+
class TestSecureCodingStandardChecker(pylint.testutils.CheckerTestCase):
24+
CHECKER_CLASS = pylint_scs.SecureCodingStandardChecker
25+
26+
@pytest.mark.parametrize(
27+
'platform, expected_success',
28+
(
29+
('Linux', True),
30+
('Darwin', True),
31+
('Java', False),
32+
('Windows', False),
33+
),
34+
)
35+
@pytest.mark.parametrize('s', ('from shlex import quote',))
36+
def test_shlex_quote_importfrom(self, mocker, platform, expected_success, s):
37+
mocker.patch('platform.system', lambda: platform)
38+
39+
node = astroid.extract_node(s + ' #@')
40+
if expected_success:
41+
self.checker.visit_importfrom(node)
42+
else:
43+
with self.assertAddsMessages(pylint.testutils.Message(msg_id='avoid-shlex-quote-on-non-posix', node=node)):
44+
self.checker.visit_importfrom(node)
45+
46+
@pytest.mark.parametrize(
47+
'platform, expected_success',
48+
(
49+
('Linux', True),
50+
('Darwin', True),
51+
('Java', False),
52+
('Windows', False),
53+
),
54+
)
55+
@pytest.mark.parametrize(
56+
's',
57+
(
58+
'shlex.quote("ls -l")',
59+
'shlex.quote(command_str)',
60+
),
61+
)
62+
def test_shlex_call_quote(self, mocker, platform, expected_success, s):
63+
mocker.patch('platform.system', lambda: platform)
64+
65+
node = astroid.extract_node(s + ' #@')
66+
if expected_success:
67+
self.checker.visit_call(node)
68+
else:
69+
with self.assertAddsMessages(pylint.testutils.Message(msg_id='avoid-shlex-quote-on-non-posix', node=node)):
70+
self.checker.visit_call(node)

0 commit comments

Comments
 (0)