Skip to content

Commit 1094514

Browse files
SONARPY-1013 Rule S6252: Disabling versioning of S3 buckets is security-sensitive (#1123)
1 parent ae10e05 commit 1094514

File tree

7 files changed

+226
-2
lines changed

7 files changed

+226
-2
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Arrays;
2323
import java.util.Collections;
2424
import java.util.HashSet;
25+
import org.sonar.python.checks.cdk.S3BucketVersioningCheck;
2526
import org.sonar.python.checks.hotspots.ClearTextProtocolsCheck;
2627
import org.sonar.python.checks.hotspots.CommandLineArgsCheck;
2728
import org.sonar.python.checks.hotspots.CorsCheck;
@@ -51,8 +52,8 @@
5152
import org.sonar.python.checks.hotspots.UnverifiedHostnameCheck;
5253
import org.sonar.python.checks.regex.AnchorPrecedenceCheck;
5354
import org.sonar.python.checks.regex.DuplicatesInCharacterClassCheck;
54-
import org.sonar.python.checks.regex.EmptyGroupCheck;
5555
import org.sonar.python.checks.regex.EmptyAlternativeCheck;
56+
import org.sonar.python.checks.regex.EmptyGroupCheck;
5657
import org.sonar.python.checks.regex.EmptyStringRepetitionCheck;
5758
import org.sonar.python.checks.regex.GraphemeClustersInClassesCheck;
5859
import org.sonar.python.checks.regex.GroupReplacementCheck;
@@ -67,9 +68,9 @@
6768
import org.sonar.python.checks.regex.SingleCharCharacterClassCheck;
6869
import org.sonar.python.checks.regex.SingleCharacterAlternationCheck;
6970
import org.sonar.python.checks.regex.StringReplaceCheck;
71+
import org.sonar.python.checks.regex.SuperfluousCurlyBraceCheck;
7072
import org.sonar.python.checks.regex.UnquantifiedNonCapturingGroupCheck;
7173
import org.sonar.python.checks.regex.VerboseRegexCheck;
72-
import org.sonar.python.checks.regex.SuperfluousCurlyBraceCheck;
7374

7475
public final class CheckList {
7576

@@ -224,6 +225,7 @@ public static Iterable<Class> getChecks() {
224225
ReturnAndYieldInOneFunctionCheck.class,
225226
ReturnYieldOutsideFunctionCheck.class,
226227
RobustCipherAlgorithmCheck.class,
228+
S3BucketVersioningCheck.class,
227229
SameBranchCheck.class,
228230
SameConditionCheck.class,
229231
SecureCookieCheck.class,
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2022 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 GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.python.checks.cdk;
21+
22+
import java.util.ArrayList;
23+
import java.util.Collections;
24+
import java.util.List;
25+
import java.util.Optional;
26+
import org.sonar.check.Rule;
27+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
28+
import org.sonar.plugins.python.api.SubscriptionContext;
29+
import org.sonar.plugins.python.api.tree.Argument;
30+
import org.sonar.plugins.python.api.tree.CallExpression;
31+
import org.sonar.plugins.python.api.tree.Expression;
32+
import org.sonar.plugins.python.api.tree.Name;
33+
import org.sonar.plugins.python.api.tree.RegularArgument;
34+
import org.sonar.plugins.python.api.tree.Token;
35+
import org.sonar.plugins.python.api.tree.Tree;
36+
import org.sonar.python.checks.Expressions;
37+
38+
@Rule(key = "S6252")
39+
public class S3BucketVersioningCheck extends PythonSubscriptionCheck {
40+
41+
public static final String MESSAGE = "Make sure an unversioned S3 bucket is safe here.";
42+
public static final String MESSAGE_OMITTING = "Omitting the \"versioned\" argument disables S3 bucket versioning. Make sure it is safe here.";
43+
public static final String MESSAGE_SECONDARY = "Propagated setting.";
44+
45+
@Override
46+
public void initialize(Context context) {
47+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::visitNode);
48+
}
49+
50+
public void visitNode(SubscriptionContext ctx) {
51+
CallExpression node = (CallExpression) ctx.syntaxNode();
52+
Optional.ofNullable(node.calleeSymbol())
53+
.filter(nodeSymbol -> ("aws_cdk.aws_s3.Bucket".equals(nodeSymbol.fullyQualifiedName())))
54+
.ifPresent(nodeSymbol -> {
55+
Optional<RegularArgument> version = getVersionArgument(node.arguments());
56+
if (version.isPresent()) {
57+
version.map(regArg -> propagatedSensitiveExpressions(regArg.expression()))
58+
.filter(trees -> !trees.isEmpty())
59+
.ifPresent(trees -> {
60+
PreciseIssue issue = ctx.addIssue(trees.get(0).parent(), MESSAGE);
61+
trees.stream().skip(1).forEach(expression -> issue.secondary(expression.parent(), MESSAGE_SECONDARY));
62+
});
63+
} else {
64+
ctx.addIssue(node.callee(), MESSAGE_OMITTING);
65+
}
66+
});
67+
}
68+
69+
private static Optional<RegularArgument> getVersionArgument(List<Argument> args) {
70+
return args.stream()
71+
.map(RegularArgument.class::cast)
72+
.filter(a -> a.keywordArgument() != null)
73+
.filter(a -> "versioned".equals(a.keywordArgument().name()))
74+
.findAny();
75+
}
76+
77+
private static List<Tree> propagatedSensitiveExpressions(Expression expression) {
78+
List<Tree> trees = new ArrayList<>();
79+
if (Optional.ofNullable(expression.firstToken()).filter(S3BucketVersioningCheck::isFalse).isPresent()) {
80+
trees.add(expression);
81+
} else if (expression.is(Tree.Kind.NAME)) {
82+
Expression singleAssignedValue = Expressions.singleAssignedValue(((Name) expression));
83+
if (singleAssignedValue != null) {
84+
trees.add(expression);
85+
List<Tree> subTrees = propagatedSensitiveExpressions(singleAssignedValue);
86+
if (subTrees.isEmpty()) {
87+
return Collections.emptyList();
88+
}
89+
trees.addAll(subTrees);
90+
}
91+
}
92+
return trees;
93+
}
94+
95+
private static boolean isFalse(Token token) {
96+
return "False".equals(token.value());
97+
}
98+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<p>S3 buckets can be versioned. When the S3 bucket is unversioned it means that a new version of an object overwrites an existing one in the S3
2+
bucket.</p>
3+
<p>It can lead to unintentional or intentional information loss.</p>
4+
<h2>Ask Yourself Whether</h2>
5+
<ul>
6+
<li> The bucket stores information that require high availability. </li>
7+
</ul>
8+
<p>There is a risk if you answered yes to any of those questions.</p>
9+
<h2>Recommended Secure Coding Practices</h2>
10+
<p>It’s recommended to enable S3 versioning and thus to have the possibility to retrieve and restore different versions of an object.</p>
11+
<h2>Sensitive Code Example</h2>
12+
<pre>
13+
bucket = s3.Bucket(self, "MyUnversionedBucket",
14+
versioned=False # Sensitive
15+
)
16+
</pre>
17+
<p>The default value of <code>versioned</code> is <code>False</code> so the absence of this parameter is also sensitive.</p>
18+
<h2>Compliant Solution</h2>
19+
<pre>
20+
bucket = s3.Bucket(self, "MyVersionedBucket",
21+
versioned=True # Compliant
22+
)
23+
</pre>
24+
<h2>See</h2>
25+
<ul>
26+
<li> <a href="https://owasp.org/Top10/A05_2021-Security_Misconfiguration/">OWASP Top 10 2021 Category A5</a> - Security Misconfiguration </li>
27+
<li> <a href="https://www.owasp.org/index.php/Top_10-2017_A6-Security_Misconfiguration">OWASP Top 10 2017 Category A6</a> - Security
28+
Misconfiguration </li>
29+
<li> <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html">AWS documentation</a> - Using versioning in S3 buckets </li>
30+
<li> <a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3.Bucket.html#versioned">CDK documentation</a> - Using versioning in S3
31+
buckets </li>
32+
</ul>
33+
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"title": "Disabling versioning of S3 buckets is security-sensitive",
3+
"type": "SECURITY_HOTSPOT",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "5min"
8+
},
9+
"tags": [
10+
"aws",
11+
"owasp-a6"
12+
],
13+
"defaultSeverity": "Minor",
14+
"ruleSpecification": "RSPEC-6252",
15+
"sqKey": "S6252",
16+
"scope": "Main",
17+
"securityStandards": {
18+
"OWASP": [
19+
"A6"
20+
],
21+
"OWASP Top 10 2021": [
22+
"A5"
23+
]
24+
}
25+
}

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
@@ -152,6 +152,7 @@
152152
"S6002",
153153
"S6019",
154154
"S6035",
155+
"S6252",
155156
"S6323",
156157
"S6326",
157158
"S6328",
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2022 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 GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.python.checks.cdk;
21+
22+
import org.junit.Test;
23+
import org.sonar.python.checks.utils.PythonCheckVerifier;
24+
25+
public class S3BucketVersioningCheckTest {
26+
27+
@Test
28+
public void test() {
29+
PythonCheckVerifier.verify("src/test/resources/checks/cdk/s3BucketVersioning.py", new S3BucketVersioningCheck());
30+
}
31+
32+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from aws_cdk import aws_s3 as s3
2+
3+
# Noncompliant@+1 {{Omitting the "versioned" argument disables S3 bucket versioning. Make sure it is safe here.}}
4+
bucket = s3.Bucket(self, "MyUnversionedBucket")
5+
# ^^^^^^^^^
6+
7+
bucket = s3.Bucket(self, "MyUnversionedBucket", versioned=False) # Noncompliant {{Make sure an unversioned S3 bucket is safe here.}}
8+
# ^^^^^^^^^^^^^^^
9+
10+
bucket = s3.Bucket(self, "MyUnversionedBucket", versioned=True) # Compliant
11+
12+
bucket = s3.Bucket(self, "MyUnversionedBucket", versioned=unresolved_var) # Compliant
13+
14+
15+
versioning = True
16+
bucket = s3.Bucket(self, "MyUnversionedBucket", versioned=versioning) # Compliant
17+
18+
value = "FALSE"
19+
bucket = s3.Bucket(self, "MyUnversionedBucket", versioned=value) # Compliant
20+
21+
22+
a = function_call_for_coverage(10)
23+
24+
25+
def test():
26+
bucket_versioning = False
27+
# ^^^^^^^^^^^^^^^^^^^^^^^^^> {{Propagated setting.}}
28+
second_versioning = bucket_versioning
29+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^> {{Propagated setting.}}
30+
bucket = s3.Bucket(self, "MyUnversionedBucket",
31+
versioned=second_versioning # Noncompliant {{Make sure an unversioned S3 bucket is safe here.}}
32+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^
33+
)

0 commit comments

Comments
 (0)