diff --git a/its/sources b/its/sources index fd74061ee..ff75c5df9 160000 --- a/its/sources +++ b/its/sources @@ -1 +1 @@ -Subproject commit fd74061ee2669fe8ff4a0f5f96c9a2ec8d9087a7 +Subproject commit ff75c5df9b98cd60850e5622970d195a85709a0f diff --git a/kotlin-checks-test-sources/src/main/kotlin/checks/ObjectOutputStreamCheckSample.kt b/kotlin-checks-test-sources/src/main/kotlin/checks/ObjectOutputStreamCheckSample.kt new file mode 100644 index 000000000..4afbf98f5 --- /dev/null +++ b/kotlin-checks-test-sources/src/main/kotlin/checks/ObjectOutputStreamCheckSample.kt @@ -0,0 +1,81 @@ +package checks + +import java.io.File +import java.io.FileOutputStream +import java.io.ObjectOutputStream +import java.nio.file.Files +import java.nio.file.OpenOption +import java.nio.file.Paths +import java.nio.file.StandardOpenOption + +class ObjectOutputStreamCheckSample { + + fun noncompliant_1(fileName: String?) { + val fos = FileOutputStream(fileName, true) // fos opened in append mode + val out = ObjectOutputStream(fos) // Noncompliant {{Do not use a FileOutputStream in append mode.}} + } + + + fun noncompliant_2(fileName: String?, appendMode: Boolean) { + if (!appendMode) return + val fos = FileOutputStream(fileName, appendMode) // fos opened in append mode + val out = ObjectOutputStream(fos) // FN + } + + + fun noncompliant_3(file: File?) { + val fos = FileOutputStream(file, true) // fos opened in append mode + val out = ObjectOutputStream(fos) // Noncompliant + } + + + fun noncompliant_10() { + val fos = Files.newOutputStream( + Paths.get("a"), + StandardOpenOption.APPEND + ) + val out = ObjectOutputStream(fos) // Noncompliant [[flows=f1]] + } + + + fun noncompliant_11() { + val fos = Files.newOutputStream(Paths.get("a"), StandardOpenOption.DELETE_ON_CLOSE, StandardOpenOption.APPEND) + val out = ObjectOutputStream(fos) // Noncompliant + } + + + fun noncompliant_12() { + val openOption: OpenOption = StandardOpenOption.APPEND + val fos = Files.newOutputStream(Paths.get("a"), StandardOpenOption.DELETE_ON_CLOSE, openOption) + val out = ObjectOutputStream(fos) // Noncompliant + } + + + fun noncompliant_13() { + val fos = Files.newOutputStream(Paths.get("a"), StandardOpenOption.APPEND) + val out = ObjectOutputStream(fos) // Noncompliant + } + + + fun compliant_1(fileName: String?) { + val fos = FileOutputStream(fileName, false) + val out = ObjectOutputStream(fos) + } + + + fun compliant_2(fileName: String?) { + val fos = FileOutputStream(fileName) + val out = ObjectOutputStream(fos) + } + + + fun compliant_10() { + val fos = Files.newOutputStream(Paths.get("a"), StandardOpenOption.TRUNCATE_EXISTING) + val out = ObjectOutputStream(fos) + } + + + fun coverage() { + val out = ObjectOutputStream(null) + } +} diff --git a/sonar-kotlin-plugin/src/main/java/org/sonarsource/kotlin/checks/ObjectOutputStreamCheck.kt b/sonar-kotlin-plugin/src/main/java/org/sonarsource/kotlin/checks/ObjectOutputStreamCheck.kt new file mode 100644 index 000000000..cd4afdbc6 --- /dev/null +++ b/sonar-kotlin-plugin/src/main/java/org/sonarsource/kotlin/checks/ObjectOutputStreamCheck.kt @@ -0,0 +1,95 @@ +/* + * SonarSource Kotlin + * Copyright (C) 2018-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonarsource.kotlin.checks + +import org.jetbrains.kotlin.js.descriptorUtils.getJetTypeFqName +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtDotQualifiedExpression +import org.jetbrains.kotlin.resolve.calls.model.ExpressionValueArgument +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall +import org.jetbrains.kotlin.resolve.calls.model.VarargValueArgument +import org.jetbrains.kotlin.resolve.calls.util.getCall +import org.jetbrains.kotlin.resolve.calls.util.getFirstArgumentExpression +import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall +import org.jetbrains.kotlin.utils.addToStdlib.ifTrue +import org.sonar.check.Rule +import org.sonarsource.kotlin.api.* +import org.sonarsource.kotlin.plugin.KotlinFileContext + +val fileOutputStreamConstructorMatcher = ConstructorMatcher(typeName = "java.io.FileOutputStream") { + withArguments("kotlin.String", "kotlin.Boolean") + withArguments("java.io.File", "kotlin.Boolean") +} + +val filesNewOutputStreamMatcher = FunMatcher(qualifier = "java.nio.file.Files") { + withArguments( + ArgumentMatcher(typeName = "java.nio.file.Path"), + ArgumentMatcher(typeName = "java.nio.file.OpenOption", isVararg = true), + + ) +} + +const val APPEND = "java.nio.file.StandardOpenOption.APPEND" + +@Rule(key = "S2689") +class ObjectOutputStreamCheck : CallAbstractCheck() { + override val functionsToVisit: Iterable = + listOf(ConstructorMatcher(typeName = "java.io.ObjectOutputStream") { + withArguments("java.io.OutputStream") + }) + + override fun visitFunctionCall( + callExpression: KtCallExpression, + resolvedCall: ResolvedCall<*>, + kotlinFileContext: KotlinFileContext + ) { + val bindingContext = kotlinFileContext.bindingContext + callExpression.getResolvedCall(bindingContext)?.getFirstArgumentExpression() + ?.let { arg -> + arg.predictRuntimeValueExpression(bindingContext).getCall(bindingContext) + ?.also { call -> + if (fileOutputStreamConstructorMatcher.matches(call, bindingContext)) { + (call.getResolvedCall(bindingContext)?.valueArgumentsByIndex?.get(1) as? ExpressionValueArgument) + ?.valueArgument?.getArgumentExpression()?.predictRuntimeBooleanValue(bindingContext) + ?.ifTrue { + kotlinFileContext.reportIssue( + callExpression.calleeExpression!!, + "Do not use a FileOutputStream in append mode." + ) + } + } else if (filesNewOutputStreamMatcher.matches(call, bindingContext)) { + + val varargValueArgument = + call.getResolvedCall(bindingContext)?.valueArgumentsByIndex?.get(1) as? VarargValueArgument ?: return + varargValueArgument.arguments.any { + (it.getArgumentExpression()?.predictRuntimeValueExpression(bindingContext) as? KtDotQualifiedExpression) + ?.resolveReferenceTarget(bindingContext).determineType()?.getJetTypeFqName(false) == APPEND + }.ifTrue { + kotlinFileContext.reportIssue( + callExpression.calleeExpression!!, + "Do not use a FileOutputStream in append mode." + ) + } + } + } + } + } + +} diff --git a/sonar-kotlin-plugin/src/main/java/org/sonarsource/kotlin/plugin/KotlinCheckList.kt b/sonar-kotlin-plugin/src/main/java/org/sonarsource/kotlin/plugin/KotlinCheckList.kt index f29e9b385..1839659be 100644 --- a/sonar-kotlin-plugin/src/main/java/org/sonarsource/kotlin/plugin/KotlinCheckList.kt +++ b/sonar-kotlin-plugin/src/main/java/org/sonarsource/kotlin/plugin/KotlinCheckList.kt @@ -76,6 +76,7 @@ import org.sonarsource.kotlin.checks.MainSafeCoroutinesCheck import org.sonarsource.kotlin.checks.MatchCaseTooBigCheck import org.sonarsource.kotlin.checks.MobileDatabaseEncryptionKeysCheck import org.sonarsource.kotlin.checks.NestedMatchCheck +import org.sonarsource.kotlin.checks.ObjectOutputStreamCheck import org.sonarsource.kotlin.checks.OneStatementPerLineCheck import org.sonarsource.kotlin.checks.ParsingErrorCheck import org.sonarsource.kotlin.checks.PseudoRandomCheck @@ -183,6 +184,7 @@ val KOTLIN_CHECKS = listOf( MatchCaseTooBigCheck::class.java, MobileDatabaseEncryptionKeysCheck::class.java, NestedMatchCheck::class.java, + ObjectOutputStreamCheck::class.java, OneStatementPerLineCheck::class.java, ParsingErrorCheck::class.java, PseudoRandomCheck::class.java, diff --git a/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/S2689.html b/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/S2689.html new file mode 100644 index 000000000..139aef328 --- /dev/null +++ b/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/S2689.html @@ -0,0 +1,16 @@ +

ObjectOutputStreams are used with serialization, and the first thing an ObjectOutputStream writes is the serialization +stream header. This header should appear once per file, at the beginning. Pass a file opened in append mode into an ObjectOutputStream +constructor and the serialization stream header will be added to the end of the file before your object is then also appended.

+

When you’re trying to read your object(s) back from the file, only the first one will be read successfully, and a +StreamCorruptedException will be thrown after that.

+

Noncompliant Code Example

+
+val fos = FileOutputStream(fileName, true) // fos opened in append mode
+val out = ObjectOutputStream(fos) // Noncompliant
+
+

Compliant Solution

+
+val fos = FileOutputStream(fileName)
+val out = ObjectOutputStream(fos)
+
+ diff --git a/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/S2689.json b/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/S2689.json new file mode 100644 index 000000000..5c5ace5cf --- /dev/null +++ b/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/S2689.json @@ -0,0 +1,17 @@ +{ + "title": "Files opened in append mode should not be used with ObjectOutputStream", + "type": "BUG", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "1h" + }, + "tags": [ + "serialization" + ], + "defaultSeverity": "Blocker", + "ruleSpecification": "RSPEC-2689", + "sqKey": "S2689", + "scope": "Main", + "quickfix": "unknown" +} diff --git a/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/Sonar_way_profile.json b/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/Sonar_way_profile.json index 9bbd41031..d57592963 100644 --- a/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/Sonar_way_profile.json +++ b/sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/Sonar_way_profile.json @@ -41,6 +41,7 @@ "S2151", "S2175", "S2245", + "S2689", "S2757", "S3329", "S3776", diff --git a/sonar-kotlin-plugin/src/test/java/org/sonarsource/kotlin/checks/ObjectOutputStreamCheckTest.kt b/sonar-kotlin-plugin/src/test/java/org/sonarsource/kotlin/checks/ObjectOutputStreamCheckTest.kt new file mode 100644 index 000000000..ba841e80d --- /dev/null +++ b/sonar-kotlin-plugin/src/test/java/org/sonarsource/kotlin/checks/ObjectOutputStreamCheckTest.kt @@ -0,0 +1,22 @@ +/* + * SonarSource Kotlin + * Copyright (C) 2018-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonarsource.kotlin.checks + +internal class ObjectOutputStreamCheckTest : CheckTest(ObjectOutputStreamCheck())