Skip to content

Commit f7dbb54

Browse files
committed
Add R8009 for builtin open() vs os.open() for file permissions
1 parent 8b1e0f3 commit f7dbb54

File tree

4 files changed

+142
-14
lines changed

4 files changed

+142
-14
lines changed

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
12+
- Added R8009 to prefer `os.open()` to the builtin `open` when in writing mode
13+
1014
### Repository
1115

1216
- Update pre-commit configuration
@@ -15,6 +19,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1519

1620
Initial release
1721

18-
[Unreleased]: https://github.com/Takishima/pylint-secure-coding-standard/compare/1.0.0...HEAD
22+
[Unreleased]: https://github.com/Takishima/pylint-secure-coding-standard/compare/v1.0.0...HEAD
1923

20-
[1.0.0]: https://github.com/Takishima/pylint-secure-coding-standard/compare/375145a3dec096ff4e33901ef749a1a9a6f4edc6...1.0.0
24+
[1.0.0]: https://github.com/Takishima/pylint-secure-coding-standard/compare/375145a3dec096ff4e33901ef749a1a9a6f4edc6...v1.0.0

README.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,18 @@ pylint plugin that enforces some secure coding standards.
1111

1212
## Pylint codes
1313

14-
| Code | Description |
15-
|-------|---------------------------------------------------------------------------------|
16-
| R8000 | Use `os.path.realpath()` instead of `os.path.abspath()` and `os.path.relpath()` |
17-
| E8001 | Avoid using `exec()` and `eval()` |
18-
| E8002 | Avoid using `os.sytem()` |
19-
| E8003 | Avoid using `shell=True` when calling `subprocess` functions |
20-
| R8004 | Avoid using `tempfile.mktemp()`, prefer `tempfile.mkstemp()` instead |
21-
| E8005 | Avoid using unsafe PyYAML loading functions |
22-
| E8006 | Avoid using `jsonpickle.decode()` |
23-
| C8007 | Avoid debug statement in production code |
24-
| C8008 | Avoid `assert` statements in production code |
14+
| Code | Description |
15+
|-------|--------------------------------------------------------------------------------------------------------------|
16+
| R8000 | Use `os.path.realpath()` instead of `os.path.abspath()` and `os.path.relpath()` |
17+
| E8001 | Avoid using `exec()` and `eval()` |
18+
| E8002 | Avoid using `os.sytem()` |
19+
| E8003 | Avoid using `shell=True` when calling `subprocess` functions |
20+
| R8004 | Avoid using `tempfile.mktemp()`, prefer `tempfile.mkstemp()` instead |
21+
| E8005 | Avoid using unsafe PyYAML loading functions |
22+
| E8006 | Avoid using `jsonpickle.decode()` |
23+
| C8007 | Avoid debug statement in production code |
24+
| C8008 | Avoid `assert` statements in production code |
25+
| R8009 | Use of builtin `open` for writing is discouraged in favor of `os.open` to allow for setting file permissions |
2526

2627

2728
## Pre-commit hook
@@ -31,7 +32,7 @@ See [pre-commit](https://github.com/pre-commit/pre-commit) for instructions
3132
Sample `.pre-commit-config.yaml`:
3233

3334
```yaml
34-
- repo: https://github.com/pycqa/pylint
35+
- repo: https://github.com/PyCQA/pylint/
3536
rev: pylint-2.6.0
3637
hooks:
3738
- id: pylint

pylint_secure_coding_standard.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,31 @@ def _is_os_path_call(node):
4949
)
5050

5151

52+
def _is_builtin_open_for_writing(node):
53+
if isinstance(node.func, astroid.Name) and node.func.name == 'open':
54+
mode = ''
55+
if len(node.args) > 1:
56+
if isinstance(node.args[1], astroid.Name):
57+
return True # variable -> to be on the safe side, flag as inappropriate
58+
if isinstance(node.args[1], astroid.Const):
59+
mode = node.args[1].value
60+
elif node.keywords:
61+
for keyword in node.keywords:
62+
if keyword.arg == 'mode':
63+
if not isinstance(keyword.value, astroid.Const):
64+
return True # variable -> to be on the safe side, flag as inappropriate
65+
mode = keyword.value.value
66+
break
67+
68+
if any(m in mode for m in 'awx'):
69+
# open(..., "w")
70+
# open(..., "wb")
71+
# open(..., "a")
72+
# open(..., "x")
73+
return True
74+
return False
75+
76+
5277
def _is_subprocess_shell_true_call(node):
5378
if (
5479
isinstance(node.func, astroid.Attribute)
@@ -190,6 +215,12 @@ class SecureCodingStandardChecker(BaseChecker):
190215
'avoid-assert',
191216
'`assert` statements should not be present in production code',
192217
),
218+
'R8005': (
219+
'Avoid builtin open() when writing to files, prefer os.open()',
220+
'replace-builtin-open',
221+
'Use of builtin `open` for writing is discouraged in favor of `os.open` to allow for setting file '
222+
'permissions',
223+
),
193224
}
194225

195226
options = {}
@@ -212,6 +243,8 @@ def visit_call(self, node):
212243
self.add_message('replace-os-relpath-abspath', node=node)
213244
elif _is_subprocess_shell_true_call(node):
214245
self.add_message('avoid-shell-true', node=node)
246+
elif _is_builtin_open_for_writing(node):
247+
self.add_message('replace-builtin-open', node=node)
215248
elif isinstance(node.func, astroid.Name) and (node.func.name in ('eval', 'exec')):
216249
self.add_message('avoid-eval-exec', node=node)
217250

@@ -238,6 +271,14 @@ def visit_importfrom(self, node):
238271
elif node.modname == 'os' and [name for (name, _) in node.names if name == 'system']:
239272
self.add_message('avoid-os-system', node=node)
240273

274+
def visit_with(self, node):
275+
"""
276+
Visitor method called for astroid.With nodes
277+
"""
278+
for item in node.items:
279+
if item and isinstance(item[0], astroid.Call) and _is_builtin_open_for_writing(item[0]):
280+
self.add_message('replace-builtin-open', node=node)
281+
241282
def visit_assert(self, node):
242283
"""
243284
Visitor method called for astroid.Assert nodes

tests/builtin_open_test.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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+
def test_builtin_open_ok(self):
27+
nodes = astroid.extract_node(
28+
"""
29+
int(0) #@
30+
foo() #@
31+
open("file.txt") #@
32+
bla.open("file.txt") #@
33+
bla.open("file.txt", "w") #@
34+
with open("file.txt") as fd: fd.read() #@
35+
with bla.open("file.txt") as fd: fd.read() #@
36+
with bla.open("file.txt", "w") as fd: fd.read() #@
37+
"""
38+
)
39+
40+
call_nodes = nodes[:5]
41+
with_nodes = nodes[5:]
42+
43+
with self.assertNoMessages():
44+
for idx, node in enumerate(call_nodes):
45+
self.checker.visit_call(node)
46+
for idx, node in enumerate(with_nodes):
47+
self.checker.visit_with(node)
48+
49+
_calls_not_ok = (
50+
'open("file.txt", "w")',
51+
'open("file.txt", "wb")',
52+
'open("file.txt", "bw")',
53+
'open("file.txt", "a")',
54+
'open("file.txt", "ab")',
55+
'open("file.txt", "ba")',
56+
'open("file.txt", "x")',
57+
'open("file.txt", "xb")',
58+
'open("file.txt", "bx")',
59+
'open("file.txt", mode)',
60+
'open("file.txt", mode="w")',
61+
'open("file.txt", mode="wb")',
62+
'open("file.txt", mode="bw")',
63+
'open("file.txt", mode="a")',
64+
'open("file.txt", mode="ab")',
65+
'open("file.txt", mode="ba")',
66+
'open("file.txt", mode="x")',
67+
'open("file.txt", mode="xb")',
68+
'open("file.txt", mode="bx")',
69+
'open("file.txt", mode=mode)',
70+
)
71+
72+
@pytest.mark.parametrize('s', _calls_not_ok)
73+
def test_builtin_open_call(self, s):
74+
node = astroid.extract_node(s + ' #@')
75+
with self.assertAddsMessages(pylint.testutils.Message(msg_id='replace-builtin-open', node=node)):
76+
self.checker.visit_call(node)
77+
78+
@pytest.mark.parametrize('s', ('with ' + s + ' as fd: fd.read()' for s in _calls_not_ok))
79+
def test_builtin_open_with(self, s):
80+
node = astroid.extract_node(s + ' #@')
81+
with self.assertAddsMessages(pylint.testutils.Message(msg_id='replace-builtin-open', node=node)):
82+
self.checker.visit_with(node)

0 commit comments

Comments
 (0)