Skip to content

Commit 859e262

Browse files
thomas-serre-sonarsourcesonartech
authored andcommitted
SONARPY-3106 Rule S7617 Reserved environment variable names should not be overridden in Lambda functions (#407)
GitOrigin-RevId: ae30c594b00724160fa1edbc8baae815d6044319
1 parent 62604db commit 859e262

File tree

8 files changed

+286
-2
lines changed

8 files changed

+286
-2
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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.Set;
20+
import org.sonar.check.Rule;
21+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
22+
import org.sonar.plugins.python.api.SubscriptionContext;
23+
import org.sonar.plugins.python.api.TriBool;
24+
import org.sonar.plugins.python.api.tree.AssignmentStatement;
25+
import org.sonar.plugins.python.api.tree.Expression;
26+
import org.sonar.plugins.python.api.tree.ExpressionList;
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.StringLiteral;
30+
import org.sonar.plugins.python.api.tree.SubscriptionExpression;
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.tree.TreeUtils;
35+
import org.sonar.python.types.v2.TypeCheckBuilder;
36+
37+
@Rule(key = "S7617")
38+
public class AWSLambdaReservedEnvironmentVariableCheck extends PythonSubscriptionCheck {
39+
40+
private static final Set<String> AWS_RESERVED_ENVIRONMENT_VARIABLES = Set.of(
41+
"_HANDLER",
42+
"_X_AMZN_TRACE_ID",
43+
"AWS_DEFAULT_REGION",
44+
"AWS_REGION",
45+
"AWS_EXECUTION_ENV",
46+
"AWS_LAMBDA_FUNCTION_NAME",
47+
"AWS_LAMBDA_FUNCTION_MEMORY_SIZE",
48+
"AWS_LAMBDA_FUNCTION_VERSION",
49+
"AWS_LAMBDA_INITIALIZATION_TYPE",
50+
"AWS_LAMBDA_LOG_GROUP_NAME",
51+
"AWS_LAMBDA_LOG_STREAM_NAME",
52+
"AWS_ACCESS_KEY",
53+
"AWS_ACCESS_KEY_ID",
54+
"AWS_SECRET_ACCESS_KEY",
55+
"AWS_SESSION_TOKEN",
56+
"AWS_LAMBDA_RUNTIME_API",
57+
"LAMBDA_TASK_ROOT",
58+
"LAMBDA_RUNTIME_DIR"
59+
);
60+
61+
private TypeCheckBuilder isOsEnvironTypeCheck;
62+
63+
public void initialize(Context context) {
64+
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, this::setupTypeChecker);
65+
context.registerSyntaxNodeConsumer(Tree.Kind.ASSIGNMENT_STMT, this::checkAssignment);
66+
}
67+
68+
private void setupTypeChecker(SubscriptionContext ctx) {
69+
isOsEnvironTypeCheck = ctx.typeChecker().typeCheckBuilder().isTypeWithFqn("os._Environ");
70+
}
71+
72+
private void checkAssignment(SubscriptionContext ctx) {
73+
AssignmentStatement assignment = (AssignmentStatement) ctx.syntaxNode();
74+
75+
if (!isInAWSLambdaFunction(assignment, ctx)) {
76+
return;
77+
}
78+
79+
ExpressionList lhs = assignment.lhsExpressions().get(0);
80+
for (Expression lhsExpression : lhs.expressions()) {
81+
checkIfOsEnvironVariableAssignedToReservedName(ctx, assignment, lhsExpression);
82+
}
83+
}
84+
85+
private void checkIfOsEnvironVariableAssignedToReservedName(SubscriptionContext ctx, AssignmentStatement assignment, Expression lhsExpression) {
86+
if (!lhsExpression.is(Tree.Kind.SUBSCRIPTION)) {
87+
return;
88+
}
89+
SubscriptionExpression lhsSubscription = (SubscriptionExpression) lhsExpression;
90+
91+
Expression object = lhsSubscription.object();
92+
if (!isOsEnvironTypeCheck.check(object.typeV2().unwrappedType()).equals(TriBool.TRUE)) {
93+
return;
94+
}
95+
96+
String subscriptValue = getSubscriptString(lhsSubscription);
97+
if (subscriptValue != null && AWS_RESERVED_ENVIRONMENT_VARIABLES.contains(subscriptValue)) {
98+
ctx.addIssue(assignment, "Do not override reserved environment variable names in Lambda functions.");
99+
}
100+
}
101+
102+
private static String getSubscriptString(SubscriptionExpression lhsSubscription) {
103+
String subscriptValue = null;
104+
Expression subscriptExpression = lhsSubscription.subscripts().expressions().get(0);
105+
if (subscriptExpression.is(Tree.Kind.STRING_LITERAL)) {
106+
subscriptValue = ((StringLiteral) subscriptExpression).trimmedQuotesValue();
107+
} else if (subscriptExpression.is(Tree.Kind.NAME)) {
108+
Name subscriptName = (Name) subscriptExpression;
109+
Expression singleAssignedValueExpression = Expressions.singleAssignedValue(subscriptName);
110+
if (singleAssignedValueExpression != null && singleAssignedValueExpression.is(Tree.Kind.STRING_LITERAL)) {
111+
subscriptValue = ((StringLiteral) singleAssignedValueExpression).trimmedQuotesValue();
112+
}
113+
}
114+
return subscriptValue;
115+
}
116+
117+
private static boolean isInAWSLambdaFunction(AssignmentStatement statement, SubscriptionContext ctx) {
118+
Tree parentFunctionDef = TreeUtils.firstAncestorOfKind(statement.parent(), Tree.Kind.FUNCDEF);
119+
if(parentFunctionDef == null) {
120+
return false;
121+
}
122+
123+
return AwsLambdaChecksUtils.isLambdaHandler(ctx, (FunctionDef) parentFunctionDef);
124+
}
125+
}
126+
127+

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
@@ -127,6 +127,7 @@ public Stream<Class<?>> getChecks() {
127127
AsyncioTaskNotStoredCheck.class,
128128
AsyncLongSleepCheck.class,
129129
AsyncWithContextManagerCheck.class,
130+
AWSLambdaReservedEnvironmentVariableCheck.class,
130131
BackslashInStringCheck.class,
131132
BackticksUsageCheck.class,
132133
BareRaiseInFinallyCheck.class,

python-checks/src/main/java/org/sonar/python/checks/utils/AwsLambdaChecksUtils.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.sonar.python.checks.utils;
1818

1919
import org.sonar.plugins.python.api.PythonVisitorContext;
20+
import org.sonar.plugins.python.api.SubscriptionContext;
2021
import org.sonar.plugins.python.api.project.configuration.ProjectConfiguration;
2122
import org.sonar.plugins.python.api.tree.FunctionDef;
2223
import org.sonar.plugins.python.api.types.v2.FunctionType;
@@ -29,10 +30,18 @@ private AwsLambdaChecksUtils() {
2930
}
3031

3132
public static boolean isLambdaHandler(PythonVisitorContext ctx, FunctionDef functionDef) {
33+
return isLambdaHandler(ctx.projectConfiguration(), ctx.callGraph(), functionDef);
34+
}
35+
36+
public static boolean isLambdaHandler(SubscriptionContext ctx, FunctionDef functionDef) {
37+
return isLambdaHandler(ctx.projectConfiguration(), ctx.callGraph(), functionDef);
38+
}
39+
40+
public static boolean isLambdaHandler(ProjectConfiguration config, CallGraph cg, FunctionDef functionDef) {
3241
if (functionDef.name().typeV2() instanceof FunctionType functionType) {
3342
String fqn = functionType.fullyQualifiedName();
34-
return isLambdaHandlerFqn(ctx.projectConfiguration(), fqn)
35-
|| isFqnCalledFromLambdaHandler(ctx.callGraph(), ctx.projectConfiguration(), fqn);
43+
return isLambdaHandlerFqn(config, fqn)
44+
|| isFqnCalledFromLambdaHandler(cg, config, fqn);
3645
}
3746
return false;
3847
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<p>Reserved environment variable names should not be overridden in Lambda functions</p>
2+
<h2>Why is this an issue?</h2>
3+
<p>AWS Lambda reserves certain environment variable names for its internal operations and runtime management. These reserved variables, such as
4+
'_HANDLER', '_X_AMZN_TRACE_ID', 'AWS_REGION', and others, are automatically set by the Lambda service and contain critical information about the
5+
function’s execution context. When application code overrides these reserved environment variables by assigning new values to them, it can disrupt the
6+
Lambda runtime’s ability to function correctly. The Lambda service relies on these variables to manage function execution, implement tracing, handle
7+
authentication, and maintain proper communication with other AWS services.</p>
8+
<h3>What is the potential impact?</h3>
9+
<p>Overriding reserved environment variables can lead to unpredictable Lambda function behavior, runtime failures, broken tracing and monitoring
10+
capabilities, authentication issues with AWS services, and difficulty in debugging production problems. This can result in service outages and
11+
degraded system reliability.</p>
12+
<h2>How to fix it</h2>
13+
<p>Avoid modifying any environment variable names that are reserved by AWS Lambda. Use custom environment variable names that do not conflict with AWS
14+
Lambda’s reserved names. Always prefix your custom environment variables with a unique identifier or use descriptive names that clearly indicate they
15+
are application-specific.</p>
16+
<h4>Noncompliant code example</h4>
17+
<pre data-diff-id="1" data-diff-type="noncompliant">
18+
import os
19+
20+
def lambda_handler(event, context):
21+
os.environ['AWS_REGION'] = "us-west-2" # Noncompliant: overriding AWS Lambda reserved environment variable
22+
return {"statusCode": 200}
23+
</pre>
24+
<h4>Compliant solution</h4>
25+
<pre data-diff-id="1" data-diff-type="compliant">
26+
import os
27+
28+
def lambda_handler(event, context):
29+
os.environ['APP_REGION'] = "us-west-2" # Compliant: using custom environment variable names
30+
return {"statusCode": 200}
31+
</pre>
32+
<h2>Resources</h2>
33+
<h3>Documentation</h3>
34+
<ul>
35+
<li> AWS documentation - <a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html">AWS Lambda environment variables
36+
documentation</a> </li>
37+
<li> AWS documentation - <a href="https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html">AWS Lambda Python handler documentation</a>
38+
</li>
39+
</ul>
40+
<h3>Standards</h3>
41+
<p>CWE-20: Improper Input Validation</p>
42+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"title": "Reserved environment variable names should not be overridden in Lambda functions",
3+
"type": "CODE_SMELL",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "5min"
8+
},
9+
"tags": [
10+
"aws"
11+
],
12+
"defaultSeverity": "Major",
13+
"ruleSpecification": "RSPEC-7617",
14+
"sqKey": "S7617",
15+
"scope": "All",
16+
"quickfix": "unknown",
17+
"code": {
18+
"impacts": {
19+
"MAINTAINABILITY": "HIGH",
20+
"RELIABILITY": "MEDIUM"
21+
},
22+
"attribute": "CONVENTIONAL"
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
@@ -288,6 +288,7 @@
288288
"S7516",
289289
"S7517",
290290
"S7519",
291+
"S7617",
291292
"S7632"
292293
]
293294
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 AWSLambdaReservedEnvironmentVariableCheckTest {
23+
@Test
24+
void test() {
25+
PythonCheckVerifier.verify("src/test/resources/checks/awsLambdaReservedEnvironmentVariable.py", new AWSLambdaReservedEnvironmentVariableCheck());
26+
}
27+
28+
}
29+
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import os
2+
3+
def os_environ_lambda_handler(event, context):
4+
5+
os.environ['_HANDLER'] = "lambda_function.lambda_handler" # Noncompliant
6+
os.environ['_X_AMZN_TRACE_ID'] = "Root=1-67891233-abcdef012345678912345678;Sampled=1" # Noncompliant
7+
os.environ['AWS_DEFAULT_REGION'] = "us-east-1" # Noncompliant
8+
os.environ['AWS_REGION'] = "us-west-2" # Noncompliant
9+
os.environ['AWS_EXECUTION_ENV'] = "AWS_Lambda_python3.8" # Noncompliant
10+
os.environ['AWS_LAMBDA_FUNCTION_NAME'] = "my_lambda_function" # Noncompliant
11+
os.environ['AWS_LAMBDA_FUNCTION_MEMORY_SIZE'] = "128" # Noncompliant
12+
os.environ['AWS_LAMBDA_FUNCTION_VERSION'] = "$LATEST" # Noncompliant
13+
os.environ['AWS_LAMBDA_INITIALIZATION_TYPE'] = "on-demand" # Noncompliant
14+
os.environ['AWS_LAMBDA_LOG_GROUP_NAME'] = "/aws/lambda/my_lambda_function" # Noncompliant
15+
os.environ['AWS_LAMBDA_LOG_STREAM_NAME'] = "2023/10/01/[$LATEST]abcdef1234567890" # Noncompliant
16+
os.environ['AWS_ACCESS_KEY'] = "example_access_key" # Noncompliant
17+
os.environ['AWS_ACCESS_KEY_ID'] = "AKIAEXAMPLE" # Noncompliant
18+
os.environ['AWS_SECRET_ACCESS_KEY'] = "example_secret_key" # Noncompliant
19+
os.environ['AWS_SESSION_TOKEN'] = "example_session_token" # Noncompliant
20+
os.environ['AWS_LAMBDA_RUNTIME_API'] = "127.0.0.1:9001" # Noncompliant
21+
os.environ['LAMBDA_TASK_ROOT'] = "/var/task" # Noncompliant
22+
os.environ['LAMBDA_RUNTIME_DIR'] = "/var/runtime" # Noncompliant
23+
24+
os.environ['PATH'] = "/path"
25+
os.another_array['AWS_REGION'] = "us-east-1"
26+
27+
28+
def multi_assignment_lambda_handler(event, context):
29+
smth, os.environ['AWS_REGION'] = 1, "us-west-2" # Noncompliant
30+
return {"statusCode": 200}
31+
32+
def without_string_literal_lambda_handler(event, context):
33+
from a_module import importred_region_str
34+
region_str = "AWS_REGION"
35+
int_var = 42
36+
os.environ[region_str] = "us-west-2" # Noncompliant
37+
os.environ[importred_region_str] = "us-west-2"
38+
os.environ[42] = "smth"
39+
os.environ[int_var] = "smth"
40+
41+
def environ_assignment_lambda_handler(event, context):
42+
from os import environ
43+
environ['AWS_REGION'] = 1, "us-west-2" # Noncompliant
44+
smth, environ['AWS_REGION'] = 1, "us-west-2" # Noncompliant
45+
return {"statusCode": 200}
46+
47+
def not_a_lambda_handler():
48+
# Outside of a Lambda handler
49+
os.environ['AWS_REGION'] = "us-west-2" # Compliant
50+
51+
os.environ['AWS_REGION'] = "us-west-2" # Compliant

0 commit comments

Comments
 (0)