Skip to content

Commit 96a5527

Browse files
Seppli11sonartech
authored andcommitted
SONARPY-3117 Rule S7625: Using long-term access keys is security-sensitive (#429)
GitOrigin-RevId: 8ac60f4d76b5d0431a36b06afa11ab77f74f42aa
1 parent 46ff3eb commit 96a5527

42 files changed

Lines changed: 1373 additions & 1035 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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.tree.CallExpression;
24+
import org.sonar.plugins.python.api.tree.Expression;
25+
import org.sonar.plugins.python.api.tree.Name;
26+
import org.sonar.plugins.python.api.tree.RegularArgument;
27+
import org.sonar.plugins.python.api.tree.Tree;
28+
import org.sonar.python.checks.utils.Expressions;
29+
import org.sonar.python.tree.TreeUtils;
30+
import org.sonar.python.types.v2.TypeCheckMap;
31+
32+
@Rule(key = "S7625")
33+
public class AwsLongTermAccessKeysCheck extends PythonSubscriptionCheck {
34+
35+
private static final String ACCESS_KEY_MESSAGE = "Make sure using long-term access keys is safe here.";
36+
private static final String SECRET_KEY_MESSAGE = "Make sure using long-term secret keys is safe here.";
37+
38+
record ClientMethod(String fqn, int awsAccessKeyIdArgIndex) {
39+
public int awsSecretAccessKeyArgIndex() {
40+
return awsAccessKeyIdArgIndex + 1;
41+
}
42+
}
43+
44+
private static final Set<ClientMethod> AWS_CLIENT_METHODS = Set.of(
45+
new ClientMethod("boto3.client", 6),
46+
new ClientMethod("boto3.resource", 6),
47+
new ClientMethod("boto3.session.Session.client", 6),
48+
new ClientMethod("boto3.session.Session.resource", 6),
49+
new ClientMethod("boto3.session.Session", 0)
50+
);
51+
52+
private TypeCheckMap<ClientMethod> awsClientTypeChecks;
53+
54+
@Override
55+
public void initialize(Context context) {
56+
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, this::initializeTypeCheckMap);
57+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::checkCallExpression);
58+
}
59+
60+
private void initializeTypeCheckMap(SubscriptionContext ctx) {
61+
awsClientTypeChecks = new TypeCheckMap<>();
62+
AWS_CLIENT_METHODS.forEach(clientMethod ->
63+
awsClientTypeChecks.put(ctx.typeChecker().typeCheckBuilder().isTypeOrInstanceWithName(clientMethod.fqn()), clientMethod));
64+
65+
}
66+
67+
private void checkCallExpression(SubscriptionContext ctx) {
68+
var call = (CallExpression) ctx.syntaxNode();
69+
ClientMethod clientMethod = awsClientTypeChecks.getForType(call.callee().typeV2());
70+
if (clientMethod != null) {
71+
RegularArgument accessKeyArgument = getAccessKeyArgument(clientMethod, call);
72+
RegularArgument secretKeyArgument = getSecretKeyArgument(clientMethod, call);
73+
if(accessKeyArgument != null && isLongTermKeyPassedAsArgument(accessKeyArgument)) {
74+
ctx.addIssue(accessKeyArgument, ACCESS_KEY_MESSAGE);
75+
} else if (secretKeyArgument != null && isLongTermKeyPassedAsArgument(secretKeyArgument)) {
76+
ctx.addIssue(secretKeyArgument, SECRET_KEY_MESSAGE);
77+
}
78+
}
79+
}
80+
81+
private static RegularArgument getSecretKeyArgument(ClientMethod clientMethod, CallExpression call) {
82+
return TreeUtils.nthArgumentOrKeyword(clientMethod.awsSecretAccessKeyArgIndex(), "aws_secret_access_key", call.arguments());
83+
}
84+
85+
private static RegularArgument getAccessKeyArgument(ClientMethod clientMethod, CallExpression call) {
86+
return TreeUtils.nthArgumentOrKeyword(clientMethod.awsAccessKeyIdArgIndex(), "aws_access_key_id", call.arguments());
87+
}
88+
89+
private static boolean isLongTermKeyPassedAsArgument(RegularArgument argument) {
90+
// For the purpose of this rule, any string literal is considered a long-term key.
91+
// Short-term keys seem to usually use a different mechanism. Furthermore, there is no way to distinguish between the two statically
92+
Expression expression = argument.expression();
93+
return isStringLiteral(expression) || isAssignedValueString(expression);
94+
}
95+
96+
private static boolean isAssignedValueString(Expression expression) {
97+
if(expression instanceof Name name) {
98+
Expression assignedValue = Expressions.singleAssignedValue(name);
99+
return assignedValue != null && isStringLiteral(assignedValue);
100+
}
101+
return false;
102+
}
103+
104+
private static boolean isStringLiteral(Expression expr) {
105+
return expr.is(Tree.Kind.STRING_LITERAL);
106+
}
107+
}

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
@@ -135,6 +135,7 @@ public Stream<Class<?>> getChecks() {
135135
AwsCustomMetricNamespaceCheck.class,
136136
AwsLambdaReturnValueAreSerializableCheck.class,
137137
AwsLambdaTmpCleanupCheck.class,
138+
AwsLongTermAccessKeysCheck.class,
138139
AwsMissingPaginationCheck.class,
139140
AwsWaitersInsteadOfCustomPollingCheck.class,
140141
BackslashInStringCheck.class,
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<p>This rule raises an issue when long-term AWS access keys are used directly in code.</p>
2+
<h2>Why is this an issue?</h2>
3+
<p>Long-term AWS access keys remain valid until manually revoked, making them a significant security risk. Unlike temporary credentials, these keys
4+
don’t expire automatically and provide persistent access to your AWS resources. When hardcoded in applications, stored in configuration files, or used
5+
in environments where temporary credentials are available, they create unnecessary security exposure. AWS provides several alternatives like IAM
6+
roles, temporary credentials through AWS STS, and instance profiles that offer better security practices.</p>
7+
<h3>What is the potential impact?</h3>
8+
<p>If long-term access keys are compromised, attackers gain persistent access to your AWS resources until the keys are manually revoked. This can lead
9+
to unauthorized data access, resource manipulation, unexpected charges, and potential data breaches. The risk is particularly high when keys are
10+
embedded in mobile applications, used on EC2 instances, or stored in version control systems.</p>
11+
<h2>How to fix it</h2>
12+
<p>Use IAM roles or temporary credentials instead of hardcoded access keys to improve security and follow AWS best practices.</p>
13+
<h3>Code examples</h3>
14+
<h4>Noncompliant code example</h4>
15+
<pre data-diff-id="1" data-diff-type="noncompliant">
16+
import boto3
17+
18+
s3_client = boto3.client(
19+
's3',
20+
aws_access_key_id='AKIAIOSFODNN7EXAMPLE',
21+
aws_secret_access_key='wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLE'
22+
) # Noncompliant: hardcoded access keys are used
23+
</pre>
24+
<h4>Compliant solution</h4>
25+
<pre data-diff-id="1" data-diff-type="compliant">
26+
import boto3
27+
import os
28+
29+
sts_client = boto3.client('sts')
30+
assumable_role_arn = os.environ.get('ASSUMABLE_ROLE_ARN', 'arn:aws:iam::account-of-role-to-assume:role/name-of-role')
31+
assumed_role_object = sts_client.assume_role(
32+
RoleArn=assumable_role_arn,
33+
RoleSessionName="AssumeRoleSession1"
34+
)
35+
credentials = assumed_role_object['Credentials']
36+
s3_client = boto3.client('s3')
37+
38+
s3_client = boto3.client(
39+
's3',
40+
aws_access_key_id=credentials['AccessKeyId'],
41+
aws_secret_access_key=credentials['SecretAccessKey'],
42+
aws_session_token=credentials['SessionToken']
43+
)
44+
</pre>
45+
<h2>Resources</h2>
46+
<h3>Documentation</h3>
47+
<ul>
48+
<li> AWS Documentation - <a href="https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html">Best practices for managing AWS
49+
access keys</a> </li>
50+
<li> AWS Documentation - <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html">Managing access keys for IAM
51+
users</a> </li>
52+
<li> AWS Documentation - <a href="https://docs.aws.amazon.com/STS/latest/APIReference/Welcome.html">AWS Security Token Service API Reference</a>
53+
</li>
54+
</ul>
55+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"title": "Long-term AWS access keys should not be used directly in code",
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-7625",
14+
"sqKey": "S7625",
15+
"scope": "All",
16+
"quickfix": "unknown",
17+
"code": {
18+
"impacts": {
19+
"MAINTAINABILITY": "HIGH",
20+
"SECURITY": "HIGH"
21+
},
22+
"attribute": "CONVENTIONAL"
23+
}
24+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,9 @@
295295
"S7617",
296296
"S7618",
297297
"S7621",
298-
"S7622",
299298
"S7620",
299+
"S7622",
300+
"S7625",
300301
"S7632"
301302
]
302303
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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 AwsLongTermAccessKeysCheckTest {
23+
24+
@Test
25+
void test() {
26+
PythonCheckVerifier.verify("src/test/resources/checks/awsLongTermAccessKeys.py", new AwsLongTermAccessKeysCheck());
27+
}
28+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import boto3
2+
import os
3+
4+
def get_temporary_credentials() -> str:
5+
"""Used for mocking the IAM role assuming part"""
6+
7+
return "AKIAIOSFODNN7EXAMPLE"
8+
9+
def noncompliant_hardcoded_credentials():
10+
s3_client = boto3.client(
11+
's3',
12+
aws_access_key_id='AKIAIOSFODNN7EXAMPLE', # Noncompliant
13+
aws_secret_access_key='wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLE'
14+
)
15+
16+
s3_resource = boto3.resource(
17+
's3',
18+
aws_access_key_id='AKIAIOSFODNN7EXAMPLE', # Noncompliant
19+
aws_secret_access_key='wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLE'
20+
)
21+
22+
s3_client = boto3.client(
23+
's3',
24+
None, # region_name
25+
None, # api_version
26+
None, # use_ssl
27+
None, # verify
28+
None, # endpoint_url
29+
'AKIAIOSFODNN7EXAMPLE', # Noncompliant
30+
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLE'
31+
)
32+
33+
s3_resource = boto3.resource(
34+
's3',
35+
None, # region_name
36+
None, # api_version
37+
None, # use_ssl
38+
None, # verify
39+
None, # endpoint_url
40+
'AKIAIOSFODNN7EXAMPLE', # Noncompliant
41+
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLE'
42+
)
43+
44+
45+
def noncompliant_session_with_credentials():
46+
session = boto3.Session()
47+
ec2_from_session = session.client('ec2',
48+
aws_access_key_id='AKIAIOSFODNN7EXAMPLE', # Noncompliant
49+
aws_secret_access_key='wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLE'
50+
)
51+
s3_from_session = session.resource('s3',
52+
aws_access_key_id='AKIAIOSFODNN7EXAMPLE', # Noncompliant
53+
aws_secret_access_key='wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLE'
54+
)
55+
56+
session = boto3.session.Session()
57+
ec2_from_session = session.client('ec2',
58+
aws_access_key_id='AKIAIOSFODNN7EXAMPLE', # Noncompliant
59+
aws_secret_access_key='wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLE'
60+
)
61+
62+
session = boto3.Session(
63+
aws_access_key_id='AKIAIOSFODNN7EXAMPLE', # Noncompliant
64+
aws_secret_access_key='wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLE'
65+
)
66+
67+
session = boto3.Session(
68+
'AKIAIOSFODNN7EXAMPLE', # Noncompliant
69+
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLE'
70+
)
71+
ec2_from_session = session.client('ec2',
72+
None, # region_name
73+
None, # api_version
74+
None, # use_ssl
75+
None, # verify
76+
None, # endpoint_url
77+
'AKIAIOSFODNN7EXAMPLE', # Noncompliant
78+
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLE'
79+
)
80+
s3_from_session = session.resource('s3',
81+
None, # region_name
82+
None, # api_version
83+
None, # use_ssl
84+
None, # verify
85+
None, # endpoint_url
86+
'AKIAIOSFODNN7EXAMPLE', # Noncompliant
87+
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLE'
88+
)
89+
90+
def noncompliant_variable_assignment():
91+
access_key = 'AKIAIOSFODNN7EXAMPLE'
92+
secret_key = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLE'
93+
lambda_client = boto3.client('lambda',
94+
aws_access_key_id=access_key, # Noncompliant
95+
aws_secret_access_key=secret_key
96+
)
97+
98+
def noncompliant_only_one_parameter():
99+
s3_client = boto3.client(
100+
's3',
101+
aws_access_key_id='AKIAIOSFODNN7EXAMPLE', # Noncompliant {{Make sure using long-term access keys is safe here.}}
102+
)
103+
s3_client = boto3.client(
104+
's3',
105+
aws_secret_access_key='wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLE' # Noncompliant {{Make sure using long-term secret keys is safe here.}}
106+
)
107+
108+
109+
def compliant_environment_credentials():
110+
s3_resource = boto3.resource(
111+
's3',
112+
aws_access_key_id=os.getenv('AWS_ACCESS_KEY_ID'),
113+
aws_secret_access_key=os.getenv('AWS_SECRET_ACCESS_KEY')
114+
)
115+
116+
def compliant_iam_roles():
117+
s3_client_compliant = boto3.client('s3')
118+
s3_resource_compliant = boto3.resource('s3')
119+
ec2_client_compliant = boto3.client('ec2', region_name='us-east-1')
120+
121+
session = boto3.Session()
122+
ec2_compliant = session.client('ec2')
123+
s3_compliant = session.resource('s3')
124+
125+
def compliant_temporary_credentials():
126+
sts_client = boto3.client('sts')
127+
assumed_role_object = sts_client.assume_role(
128+
RoleArn='arn:aws:iam::account-of-role-to-assume:role/name-of-role',
129+
RoleSessionName="AssumeRoleSession1"
130+
)
131+
credentials = assumed_role_object['Credentials']
132+
133+
s3_client_temp = boto3.client(
134+
's3',
135+
aws_access_key_id=credentials['AccessKeyId'],
136+
aws_secret_access_key=credentials['SecretAccessKey'],
137+
aws_session_token=credentials['SessionToken']
138+
)
139+
140+
def compliant_with_non_string_literals():
141+
key = get_temporary_credentials()
142+
s3_client = boto3.client(
143+
's3',
144+
aws_access_key_id=key,
145+
aws_secret_access_key=key
146+
)
147+
148+
def compliant_unknown_variable(access_key, secret_key):
149+
s3_client = boto3.client(
150+
's3',
151+
aws_access_key_id=access_key,
152+
aws_secret_access_key=secret_key
153+
)
154+

0 commit comments

Comments
 (0)