Skip to content

Commit ef68451

Browse files
authored
Redo tempfile.mktemp codemod (#577)
* Change tempfile.mktemp codemod to correctly create a NamedTemporary * refactor * handle import from cases * test all other import types * handle with block * handle args * change all tests * add docs * remove check for alias * add demonstrative unit test * check all simplestatements, not just directly under a module
1 parent 1a28783 commit ef68451

File tree

8 files changed

+351
-90
lines changed

8 files changed

+351
-90
lines changed

integration_tests/sonar/test_sonar_tempfile_mktemp.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
class TestTempfileMktemp(SonarIntegrationTest):
77
codemod = SonarTempfileMktemp
88
code_path = "tests/samples/tempfile_mktemp.py"
9-
replacement_lines = [(3, "tempfile.mkstemp()\n")]
10-
expected_diff = "--- \n+++ \n@@ -1,3 +1,3 @@\n import tempfile\n \n-tempfile.mktemp()\n+tempfile.mkstemp()\n"
9+
replacement_lines = [
10+
(3, "with tempfile.NamedTemporaryFile(delete=False) as tf:\n"),
11+
(4, " filename = tf.name\n"),
12+
]
13+
expected_diff = "--- \n+++ \n@@ -1,3 +1,4 @@\n import tempfile\n \n-filename = tempfile.mktemp()\n+with tempfile.NamedTemporaryFile(delete=False) as tf:\n+ filename = tf.name\n"
1114
expected_line_change = "3"
1215
change_description = TempfileMktempTransformer.change_description

integration_tests/test_tempfile_mktemp.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ class TestTempfileMktemp(BaseIntegrationTest):
77
original_code = """
88
import tempfile
99
10-
tempfile.mktemp()
11-
var = "hello"
10+
filename = tempfile.mktemp()
1211
"""
13-
replacement_lines = [(3, "tempfile.mkstemp()\n")]
14-
expected_diff = '--- \n+++ \n@@ -1,4 +1,4 @@\n import tempfile\n \n-tempfile.mktemp()\n+tempfile.mkstemp()\n var = "hello"\n'
12+
replacement_lines = [
13+
(3, "with tempfile.NamedTemporaryFile(delete=False) as tf:\n"),
14+
(4, " filename = tf.name\n"),
15+
]
16+
expected_diff = "--- \n+++ \n@@ -1,3 +1,4 @@\n import tempfile\n \n-filename = tempfile.mktemp()\n+with tempfile.NamedTemporaryFile(delete=False) as tf:\n+ filename = tf.name\n"
1517
expected_line_change = "3"
1618
change_description = TempfileMktempTransformer.change_description

src/codemodder/scripts/generate_docs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ class DocMetadata:
9494
),
9595
"secure-tempfile": DocMetadata(
9696
importance="High",
97-
guidance_explained="We believe this codemod is safe and will cause no unexpected errors.",
97+
guidance_explained="We believe this codemod is safe. You should review this code before merging to make sure temporary files are created, used, and closed according to your expectations.",
9898
),
9999
"upgrade-sslcontext-minimum-version": DocMetadata(
100100
importance="High",
Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
This codemod replaces all `tempfile.mktemp` calls to the more secure `tempfile.mkstemp`.
1+
This codemod replaces all `tempfile.mktemp` calls with the more secure `tempfile.NamedTemporaryFile`
22

3-
The Python [tempfile documentation](https://docs.python.org/3/library/tempfile.html#tempfile.mktemp) is explicit
4-
that `tempfile.mktemp` should be deprecated to avoid an unsafe and unexpected race condition.
5-
The changes from this codemod look like this:
3+
The Python [tempfile documentation](https://docs.python.org/3/library/tempfile.html#tempfile.mktemp) is explicit that `tempfile.mktemp` should be deprecated to avoid an unsafe and unexpected race condition. `tempfile.mktemp` does not handle the possibility that the returned file name could already be used by another process by the time your code opens the file. A more secure approach to create temporary files is to use `tempfile.NamedTemporaryFile` which will create the file for you and handle all security conditions.
64

5+
The changes from this codemod look like this:
76

87
```diff
98
import tempfile
10-
- tempfile.mktemp(...)
11-
+ tempfile.mkstemp(...)
9+
- filename = tempfile.mktemp()
10+
+ with tempfile.NamedTemporaryFile(delete=False) as tf:
11+
+ filename = tf.name
1212
```
13+
14+
The change sets `delete=False` to closely follow your code's intention when calling `tempfile.mktemp`. However, you should use this as a starting point to determine when your temporary file should be deleted.
Lines changed: 106 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,130 @@
1+
from textwrap import dedent
2+
from typing import Optional
3+
4+
import libcst as cst
5+
from libcst import matchers
6+
17
from codemodder.codemods.libcst_transformer import (
28
LibcstResultTransformer,
39
LibcstTransformerPipeline,
410
)
5-
from codemodder.codemods.semgrep import SemgrepRuleDetector
6-
from codemodder.codemods.utils_mixin import NameResolutionMixin
11+
from codemodder.codemods.utils_mixin import NameAndAncestorResolutionMixin
12+
from codemodder.utils.utils import clean_simplestring
713
from core_codemods.api import CoreCodemod, Metadata, Reference, ReviewGuidance
814

915

10-
class TempfileMktempTransformer(LibcstResultTransformer, NameResolutionMixin):
16+
class TempfileMktempTransformer(
17+
LibcstResultTransformer, NameAndAncestorResolutionMixin
18+
):
1119
change_description = "Replaces `tempfile.mktemp` with `tempfile.mkstemp`."
1220
_module_name = "tempfile"
1321

14-
def on_result_found(self, original_node, updated_node):
15-
maybe_name = self.get_aliased_prefix_name(original_node, self._module_name)
16-
if (maybe_name := maybe_name or self._module_name) == self._module_name:
17-
self.add_needed_import(self._module_name)
18-
self.remove_unused_import(original_node)
19-
return self.update_call_target(updated_node, maybe_name, "mkstemp")
22+
def leave_SimpleStatementLine(self, original_node, updated_node):
23+
match original_node:
24+
case cst.SimpleStatementLine(body=[bsstmt]):
25+
return self.check_mktemp(original_node, bsstmt)
26+
return updated_node
27+
28+
def check_mktemp(
29+
self, original_node: cst.SimpleStatementLine, bsstmt: cst.BaseSmallStatement
30+
) -> cst.SimpleStatementLine | cst.FlattenSentinel:
31+
if maybe_tuple := self._is_assigned_to_mktemp(bsstmt): # type: ignore
32+
assign_name, call = maybe_tuple
33+
return self.report_and_change(call, assign_name)
34+
if maybe_tuple := self._mktemp_is_sink(bsstmt):
35+
wrapper_func_name, call = maybe_tuple
36+
return self.report_and_change(call, wrapper_func_name, assignment=False)
37+
return original_node
38+
39+
def report_and_change(
40+
self, node: cst.Call, name: cst.Name, assignment=True
41+
) -> cst.FlattenSentinel:
42+
self.report_change(node)
43+
self.add_needed_import(self._module_name)
44+
self.remove_unused_import(node)
45+
with_block = (
46+
f"{name.value} = tf.name" if assignment else f"{name.value}(tf.name)"
47+
)
48+
new_stmt = dedent(
49+
f"""
50+
with tempfile.NamedTemporaryFile({self._make_args(node)}) as tf:
51+
{with_block}
52+
"""
53+
).rstrip()
54+
return cst.FlattenSentinel(
55+
[
56+
cst.parse_statement(new_stmt),
57+
]
58+
)
59+
60+
def _make_args(self, node: cst.Call) -> str:
61+
"""Convert args passed to tempfile.mktemp() to string for args to tempfile.NamedTemporaryFile"""
62+
63+
default = "delete=False"
64+
if not node.args:
65+
return default
66+
new_args = ""
67+
arg_keys = ("suffix", "prefix", "dir")
68+
for idx, arg in enumerate(node.args):
69+
cst.ensure_type(val := arg.value, cst.SimpleString)
70+
new_args += f'{arg_keys[idx]}="{clean_simplestring(val)}", '
71+
return f"{new_args}{default}"
72+
73+
def _is_assigned_to_mktemp(
74+
self, bsstmt: cst.BaseSmallStatement
75+
) -> Optional[tuple[cst.Name, cst.Call]]:
76+
match bsstmt:
77+
case cst.Assign(value=value, targets=targets):
78+
maybe_value = self._is_mktemp_call(value) # type: ignore
79+
if maybe_value and all(
80+
map(
81+
lambda t: matchers.matches(
82+
t, matchers.AssignTarget(target=matchers.Name())
83+
),
84+
targets, # type: ignore
85+
)
86+
):
87+
# # Todo: handle multiple potential targets
88+
return (targets[0].target, maybe_value)
89+
case cst.AnnAssign(target=target, value=value):
90+
maybe_value = self._is_mktemp_call(value) # type: ignore
91+
if maybe_value and isinstance(target, cst.Name): # type: ignore
92+
return (target, maybe_value)
93+
return None
94+
95+
def _is_mktemp_call(self, value) -> Optional[cst.Call]:
96+
match value:
97+
case cst.Call() if self.find_base_name(value.func) == "tempfile.mktemp":
98+
return value
99+
return None
100+
101+
def _mktemp_is_sink(
102+
self, bsstmt: cst.BaseSmallStatement
103+
) -> Optional[tuple[cst.Name, cst.Call]]:
104+
match bsstmt:
105+
case cst.Expr(value=cst.Call() as call):
106+
if not (args := call.args):
107+
return None
108+
109+
# todo: handle more complex cases of mktemp in different arg pos
110+
match first_arg_call := args[0].value:
111+
case cst.Call():
112+
if maybe_value := self._is_mktemp_call(first_arg_call): # type: ignore
113+
wrapper_func = call.func
114+
return (wrapper_func, maybe_value)
115+
return None
20116

21117

22118
TempfileMktemp = CoreCodemod(
23119
metadata=Metadata(
24120
name="secure-tempfile",
25121
summary="Upgrade and Secure Temp File Creation",
26-
review_guidance=ReviewGuidance.MERGE_WITHOUT_REVIEW,
122+
review_guidance=ReviewGuidance.MERGE_AFTER_REVIEW,
27123
references=[
28124
Reference(
29125
url="https://docs.python.org/3/library/tempfile.html#tempfile.mktemp"
30126
),
31127
],
32128
),
33-
detector=SemgrepRuleDetector(
34-
"""
35-
rules:
36-
- patterns:
37-
- pattern: tempfile.mktemp(...)
38-
- pattern-inside: |
39-
import tempfile
40-
...
41-
"""
42-
),
43129
transformer=LibcstTransformerPipeline(TempfileMktempTransformer),
44130
)

tests/codemods/sonar/test_sonar_tempfile_mktemp.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ def test_simple(self, tmpdir):
1515
input_code = """
1616
import tempfile
1717
18-
tempfile.mktemp()
18+
filename = tempfile.mktemp()
1919
"""
2020
expected = """
2121
import tempfile
2222
23-
tempfile.mkstemp()
23+
with tempfile.NamedTemporaryFile(delete=False) as tf:
24+
filename = tf.name
2425
"""
2526
issues = {
2627
"issues": [

0 commit comments

Comments
 (0)