Skip to content

Commit a5f85b3

Browse files
joke1196sonartech
authored andcommitted
SONARPY-3115: Rule S7620: AWS Lambda functions should clean up temporary files before completing invocation (#418)
GitOrigin-RevId: c8f1b20c3c65c2ca04c5ef2630b9991ca7be2ab1
1 parent 9880cdf commit a5f85b3

7 files changed

Lines changed: 354 additions & 0 deletions

File tree

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.checks;
18+
19+
import java.util.regex.Pattern;
20+
import java.util.stream.Stream;
21+
import javax.annotation.Nullable;
22+
import org.sonar.check.Rule;
23+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
24+
import org.sonar.plugins.python.api.SubscriptionContext;
25+
import org.sonar.plugins.python.api.tree.CallExpression;
26+
import org.sonar.plugins.python.api.tree.Expression;
27+
import org.sonar.plugins.python.api.tree.FunctionDef;
28+
import org.sonar.plugins.python.api.tree.Name;
29+
import org.sonar.plugins.python.api.tree.RegularArgument;
30+
import org.sonar.plugins.python.api.tree.StringLiteral;
31+
import org.sonar.plugins.python.api.tree.Tree;
32+
import org.sonar.python.checks.utils.AwsLambdaChecksUtils;
33+
import org.sonar.python.checks.utils.Expressions;
34+
import org.sonar.python.semantic.v2.SymbolV2;
35+
import org.sonar.python.semantic.v2.UsageV2;
36+
import org.sonar.python.tree.TreeUtils;
37+
import org.sonar.python.types.v2.TypeCheckBuilder;
38+
39+
@Rule(key = "S7620")
40+
public class AwsLambdaTmpCleanupCheck extends PythonSubscriptionCheck {
41+
42+
private static final String MESSAGE = "Clean up this temporary file before the Lambda function completes.";
43+
private static final String SECONDARY_LOCATION_MESSAGE = "The temporary folder is used in this Lambda function.";
44+
private static final Pattern TMP_PATH_PATTERN = Pattern.compile("^/tmp/.*");
45+
46+
private TypeCheckBuilder openType;
47+
private TypeCheckBuilder osRemoveType;
48+
private TypeCheckBuilder osUnlinkType;
49+
50+
@Override
51+
public void initialize(Context context) {
52+
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, this::initializeTypeChecker);
53+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::visitCallExpression);
54+
}
55+
56+
private void initializeTypeChecker(SubscriptionContext ctx) {
57+
openType = ctx.typeChecker().typeCheckBuilder().isBuiltinWithName("open");
58+
osRemoveType = ctx.typeChecker().typeCheckBuilder().isTypeWithFqn("os.remove");
59+
osUnlinkType = ctx.typeChecker().typeCheckBuilder().isTypeWithFqn("os.unlink");
60+
}
61+
62+
private void visitCallExpression(SubscriptionContext ctx) {
63+
CallExpression callExpression = (CallExpression) ctx.syntaxNode();
64+
65+
Tree functionDefAncestor = TreeUtils.firstAncestorOfKind(callExpression, Tree.Kind.FUNCDEF);
66+
if (functionDefAncestor == null) {
67+
return;
68+
}
69+
70+
FunctionDef lambdaHandler = (FunctionDef) functionDefAncestor;
71+
72+
if (!AwsLambdaChecksUtils.isLambdaHandler(ctx, lambdaHandler)) {
73+
return;
74+
}
75+
76+
if (openType.check(callExpression.callee().typeV2()).isTrue() && !isTempFileCleanedUp(callExpression, lambdaHandler)) {
77+
ctx.addIssue(callExpression.callee(), MESSAGE)
78+
.secondary(lambdaHandler.name(), SECONDARY_LOCATION_MESSAGE);
79+
}
80+
}
81+
82+
private boolean isTempFileCleanedUp(CallExpression callExpression, FunctionDef lambdaHandler) {
83+
RegularArgument regularArg = TreeUtils.nthArgumentOrKeyword(0, "file", callExpression.arguments());
84+
85+
if (regularArg == null) {
86+
return true;
87+
}
88+
89+
Expression pathExpression = regularArg.expression();
90+
String pathValue = getStringValue(pathExpression);
91+
92+
return pathValue == null ||
93+
!TMP_PATH_PATTERN.matcher(pathValue).matches() ||
94+
hasCleanupCall(lambdaHandler, pathValue) ||
95+
isPathPassedToOtherFunction(pathExpression);
96+
}
97+
98+
private boolean isPathPassedToOtherFunction(Expression pathExpression) {
99+
if (!(pathExpression instanceof Name)) {
100+
return true;
101+
}
102+
Name name = (Name) pathExpression;
103+
SymbolV2 symbol = name.symbolV2();
104+
if (symbol == null) {
105+
return true;
106+
}
107+
return getUsagesInCallExpr(symbol)
108+
.anyMatch(callExpr -> !isOsRemoveOrUnlinkCall(callExpr) && !openType.check(callExpr.callee().typeV2()).isTrue());
109+
110+
}
111+
112+
private static Stream<CallExpression> getUsagesInCallExpr(SymbolV2 symbol) {
113+
return symbol.usages().stream()
114+
.filter(usage -> usage.kind().equals(UsageV2.Kind.OTHER))
115+
.map(usage -> usage.tree().parent())
116+
.filter(parent -> parent.is(Tree.Kind.REGULAR_ARGUMENT))
117+
.map(parent -> TreeUtils.firstAncestorOfKind(parent, Tree.Kind.CALL_EXPR))
118+
.flatMap(TreeUtils.toStreamInstanceOfMapper(CallExpression.class));
119+
}
120+
121+
private boolean hasCleanupCall(FunctionDef function, String filePath) {
122+
return TreeUtils.firstChild(function, child -> child instanceof CallExpression callExpr && isOsRemoveOrUnlinkCall(callExpr) && hasMatchingPath(callExpr, filePath)).isPresent();
123+
}
124+
125+
private boolean isOsRemoveOrUnlinkCall(CallExpression callExpression) {
126+
return osRemoveType.check(callExpression.callee().typeV2()).isTrue() ||
127+
osUnlinkType.check(callExpression.callee().typeV2()).isTrue();
128+
}
129+
130+
private static boolean hasMatchingPath(CallExpression call, String targetPath) {
131+
RegularArgument regularArg = TreeUtils.nthArgumentOrKeyword(0, "path", call.arguments());
132+
133+
if (regularArg == null) {
134+
return false;
135+
}
136+
137+
String pathValue = getStringValue(regularArg.expression());
138+
return targetPath.equals(pathValue);
139+
}
140+
141+
@Nullable
142+
private static String getStringValue(Expression expression) {
143+
if (expression instanceof StringLiteral stringLiteral) {
144+
return stringLiteral.trimmedQuotesValue();
145+
}
146+
if (expression instanceof Name name) {
147+
var assignedValue = Expressions.singleAssignedValue(name);
148+
if (assignedValue instanceof StringLiteral stringLiteral) {
149+
return stringLiteral.trimmedQuotesValue();
150+
}
151+
}
152+
return null;
153+
}
154+
155+
}

python-checks/src/main/java/org/sonar/python/checks/OpenSourceCheckList.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ public Stream<Class<?>> getChecks() {
133133
AwsLambdaCrossCallCheck.class,
134134
AwsLambdaReservedEnvironmentVariableCheck.class,
135135
AwsLambdaReturnValueAreSerializableCheck.class,
136+
AwsLambdaTmpCleanupCheck.class,
136137
AwsMissingPaginationCheck.class,
137138
AwsWaitersInsteadOfCustomPollingCheck.class,
138139
BackslashInStringCheck.class,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<p>This rule raises an issue when AWS Lambda handlers write to the /tmp directory without properly cleaning up temporary files before function
2+
completion.</p>
3+
<h2>Why is this an issue?</h2>
4+
<p>AWS Lambda provides a temporary file system at /tmp for each execution environment. However, the contents of /tmp can persist across multiple
5+
invocations of the same Lambda function instance during "warm starts." When temporary files are not cleaned up, they remain available to subsequent
6+
invocations of the same function instance.</p>
7+
<h3>What is the potential impact?</h3>
8+
<p>This can lead to serious security and reliability issues: sensitive data from one invocation might leak to unrelated subsequent invocations, disk
9+
space can be exhausted causing function failures, and stale data from previous runs can cause unexpected behavior and hard-to-debug issues.</p>
10+
<h3>Exceptions</h3>
11+
<ul>
12+
<li> Writing to /tmp is recommended for caching data used by future lambda invocations. </li>
13+
</ul>
14+
<h2>How to fix it</h2>
15+
<p>Always clean up temporary files before your Lambda function completes. It is possible to use the <code>tempfile</code> module to create temporary
16+
files that will be automatically deleted.</p>
17+
<p>Otherwise, use a <code>try…​finally</code> block to ensure cleanup happens even if errors occur during processing.</p>
18+
<h3>Code examples</h3>
19+
<h4>Noncompliant code example</h4>
20+
<pre data-diff-id="1" data-diff-type="noncompliant">
21+
def lambda_handler(event, context):
22+
file_path = '/tmp/temp_data.txt' # Noncompliant
23+
with open(file_path, 'w') as f:
24+
f.write("Something")
25+
</pre>
26+
<h4>Compliant solution</h4>
27+
<pre data-diff-id="1" data-diff-type="compliant">
28+
import tempfile
29+
30+
def lambda_handler(event, context):
31+
with tempfile.NamedTemporaryFile() as f: # Compliant
32+
f.write("Something")
33+
</pre>
34+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"title": "AWS Lambda handlers should clean up temporary files in \/tmp directory",
3+
"type": "BUG",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "5min"
8+
},
9+
"tags": [
10+
"aws",
11+
"lambda"
12+
],
13+
"defaultSeverity": "Major",
14+
"ruleSpecification": "RSPEC-7620",
15+
"sqKey": "S7620",
16+
"scope": "Main",
17+
"quickfix": "unknown",
18+
"code": {
19+
"impacts": {
20+
"RELIABILITY": "MEDIUM"
21+
},
22+
"attribute": "COMPLETE"
23+
}
24+
}

python-checks/src/main/resources/org/sonar/l10n/py/rules/python/Sonar_way_profile.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@
295295
"S7618",
296296
"S7621",
297297
"S7622",
298+
"S7620",
298299
"S7632"
299300
]
300301
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.checks;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.sonar.python.checks.utils.PythonCheckVerifier;
21+
22+
class AwsLambdaTmpCleanupCheckTest {
23+
24+
@Test
25+
void test() {
26+
PythonCheckVerifier.verify("src/test/resources/checks/awsLambdaTmpCleanup.py", new AwsLambdaTmpCleanupCheck());
27+
}
28+
29+
}
30+
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import os
2+
import tempfile
3+
4+
# Lambda handler - non-compliant cases
5+
def with_open_lambda_handler(event, context):
6+
# ^^^^^^^^^^^^^^^^^^^^^^^^> {{The temporary folder is used in this Lambda function.}}
7+
file_path = '/tmp/temp_data.txt'
8+
with open(file_path, 'w') as f: # Noncompliant {{Clean up this temporary file before the Lambda function completes.}}
9+
# ^^^^
10+
f.write("Something")
11+
12+
def open_lambda_handler(event, context):
13+
file_path = '/tmp/another_file.txt'
14+
f = open(file_path, 'w') # Noncompliant
15+
f.write("Data")
16+
f.close()
17+
18+
def os_remove_other_path_lambda_handler(event, context):
19+
file_path = '/tmp/temp_data.txt'
20+
with open(file_path, 'w') as f: # Noncompliant
21+
f.write("Something")
22+
os.remove("somefile.txt")
23+
24+
def os_unlink_no_path_lambda_handler(event, context):
25+
file_path = '/tmp/temp_data.txt'
26+
with open(file_path, 'w') as f: # Noncompliant
27+
f.write("Something")
28+
os.unlink()
29+
30+
31+
32+
# Lambda handler - compliant cases
33+
def os_remove_lambda_handler(event, context):
34+
file_path = '/tmp/temp_data.txt'
35+
with open(file_path, 'w') as f:
36+
f.write("Something")
37+
os.remove(file_path)
38+
39+
40+
def cleanup_in_other_function_lambda_handler(event, context):
41+
clean_up_path = '/tmp/temp_data.txt'
42+
with open(clean_up_path, 'w') as f:
43+
f.write("Something")
44+
cleanup(clean_up_path)
45+
46+
def cleanup(file_path):
47+
os.unlink(file_path)
48+
49+
def incorrect_cleanup_lambda_handler(event, context):
50+
clean_up_path = '/tmp/temp_data.txt'
51+
with open(clean_up_path, 'w') as f: # FN as we stop the check once the file path is passed to a function
52+
f.write("Something")
53+
incorrect_cleanup(clean_up_path)
54+
55+
def incorrect_cleanup(file_path):
56+
return file_path
57+
58+
def os_unlink_lambda_handler(event, context):
59+
# Compliant: /tmp file with proper cleanup using os.unlink
60+
file_path = '/tmp/temp_data.txt'
61+
f = open(file_path, 'w')
62+
f.write("Data")
63+
f.close()
64+
os.unlink(file_path)
65+
66+
67+
def tempfile_lambda_handler(event, context):
68+
with tempfile.TemporaryFile() as temp_file:
69+
temp_file.write(b"Some data")
70+
71+
# Lambda handler with non-/tmp paths - should not trigger
72+
def not_tmp_lambda_handler(event, context):
73+
# Compliant: not writing to /tmp
74+
file_path = '/var/log/app.log'
75+
with open(file_path, 'w') as f:
76+
f.write("Log data")
77+
78+
# Non-lambda functions - should not trigger issues
79+
def regular_function():
80+
file_path = '/tmp/temp_data.txt'
81+
with open(file_path, 'w') as f:
82+
f.write("Something")
83+
84+
85+
# =============== Coverage =================
86+
87+
88+
def no_path_lambda_handler(event, context):
89+
with open() as f:
90+
f.write("Log data")
91+
92+
def not_string_path_lambda_handler(event, context):
93+
with open(2, 'w') as f:
94+
f.write("Log data")
95+
96+
def not_string_var_path_lambda_handler(event, context):
97+
file_path = 2
98+
with open(file_path, 'w') as f:
99+
f.write("Log data")
100+
101+
def different_string_lambda_handler(event, context):
102+
with open("/tmp/file.txt", 'w') as f:
103+
f.write("Something")
104+
os.remove("/tmp/other_file.txt")
105+
106+
def same_string_lambda_handler(event, context):
107+
with open("/tmp/file.txt", 'w') as f:
108+
f.write("Something")
109+
os.remove("/tmp/file.txt")

0 commit comments

Comments
 (0)