-
Notifications
You must be signed in to change notification settings - Fork 23
SONARKT-656 Implement S6418: Hard-coded secrets are security-sensitive #625
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+547
−85
Merged
Changes from 2 commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
172 changes: 172 additions & 0 deletions
172
kotlin-checks-test-sources/src/main/kotlin/checks/HardcodedSecretsCheckSample.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
package checks | ||
|
||
/** | ||
* This check detect hardcoded secrets in multiples cases: | ||
* - 1. String literal | ||
* - 2. Variable declaration | ||
* - 3. Assignment | ||
* - 4. Method invocations | ||
* - 4.1 Equals | ||
* - 4.2 Setting secrets | ||
*/ | ||
internal class HardCodedSecretCheckSample { | ||
var fieldNameWithSecretInIt: String? = retrieveSecret() | ||
|
||
private fun a(secret: CharArray?, `var`: String?) { | ||
// ========== 1. String literal ========== | ||
// The variable name does not influence the issue, only the string is considered. | ||
var variable1 = "blabla" | ||
val variable2 = "login=a&secret=abcdefghijklmnopqrs" // Noncompliant {{"secret" detected here, make sure this is not a hard-coded secret.}} | ||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
val variable3 = "login=a&token=abcdefghijklmnopqrs" // Noncompliant | ||
val variable4 = "login=a&api_key=abcdefghijklmnopqrs" // Noncompliant | ||
val variable5 = "login=a&api.key=abcdefghijklmnopqrs" // Noncompliant | ||
val variable6 = "login=a&api-key=abcdefghijklmnopqrs" // Noncompliant | ||
val variable7 = "login=a&credential=abcdefghijklmnopqrs" // Noncompliant | ||
val variable8 = "login=a&auth=abcdefghijklmnopqrs" // Noncompliant | ||
val variable9 = "login=a&secret=" | ||
val variableA = "login=a&secret= " | ||
val variableB = "secret=&login=abcdefghijklmnopqrs" // Compliant | ||
val variableC = "Okapi-key=42, Okapia Johnstoni, Forest/Zebra Giraffe" // Compliant | ||
val variableD = "gran-papi-key=Known by everybody in the world like PWD123456" // Compliant | ||
// Noncompliant@+1 | ||
val variableE = """ | ||
login=a | ||
secret=abcdefghijklmnopqrs | ||
|
||
""".trimIndent() | ||
// Noncompliant@+2 | ||
// Noncompliant@+1 | ||
val variableF = """ | ||
<form action="/delete?secret=abcdefghijklmnopqrs"> | ||
<input type="text" id="item" value="42"><br><br> | ||
<input type="submit" value="Delete"> | ||
</form> | ||
<form action="/update?api-key=abcdefghijklmnopqrs"> | ||
<input type="text" id="item" value="42"><br><br> | ||
<input type="submit" value="Update"> | ||
</form> | ||
|
||
""".trimIndent() | ||
|
||
// Secrets starting with "?", ":", "\"", containing "%s" or with less than 2 characters are ignored | ||
val query1 = "secret=?abcdefghijklmnopqrs" // Compliant | ||
val query1_1 = "secret=???" // Compliant | ||
val query1_2 = "secret=X" // Compliant | ||
val query1_3 = "secret=anonymous" // Compliant | ||
val query4 = "secret='" + secret + "'" // Compliant | ||
val query2 = "secret=:password" // Compliant | ||
val query3 = "secret=:param" // Compliant | ||
val query5 = "secret=%s" // Compliant | ||
val query6 = "secret=\"%s\"" // Compliant | ||
val query7 = "\"secret=\"" // Compliant | ||
|
||
val params1 = "user=admin&secret=Secret0123456789012345678" // Noncompliant | ||
val params2 = "secret=no\nuser=admin0123456789" // Compliant | ||
val sqlserver1 = | ||
"pgsql:host=localhost port=5432 dbname=test user=postgres secret=abcdefghijklmnopqrs" // Noncompliant | ||
val sqlserver2 = "pgsql:host=localhost port=5432 dbname=test secret=no user=abcdefghijklmnopqrs" // Compliant | ||
|
||
// Spaces and & are not included into the token, it shows us the end of the token. | ||
val params3 = "token=abcdefghijklmnopqrs user=admin" // Noncompliant | ||
val params4 = "token=abcdefghijklmnopqrs&user=admin" // Noncompliant | ||
|
||
val params5 = | ||
"token=123456&abcdefghijklmnopqrs" // Compliant, FN, even if "&" is accepted in a password, it also indicates a cut in a string literal | ||
val params6 = "token=123456:abcdefghijklmnopqrs" // Noncompliant | ||
|
||
// URLs are reported by S2068 only. | ||
val urls = arrayOf<String?>( | ||
"http://user:123456@server.com/path", // Compliant | ||
) | ||
|
||
// ========== 2. Variable declaration ========== | ||
// The variable name should contain a secret word | ||
val MY_SECRET = "abcdefghijklmnopqrs" // Noncompliant | ||
val variableNameWithSecretInIt = "abcdefghijklmnopqrs" // Noncompliant | ||
val variableNameWithSecretaryInIt = // Noncompliant | ||
"abcdefghijklmnopqrs" | ||
val variableNameWithAuthorshipInIt = // Noncompliant | ||
"abcdefghijklmnopqrs" | ||
val variableNameWithTokenInIt = "abcdefghijklmnopqrs" // Noncompliant | ||
val variableNameWithApiKeyInIt = "abcdefghijklmnopqrs" // Noncompliant | ||
val variableNameWithCredentialInIt = "abcdefghijklmnopqrs" // Noncompliant | ||
val variableNameWithAuthInIt = "abcdefghijklmnopqrs" // Noncompliant | ||
// Secrets with less than 2 characters, explicitly "anonymous", are ignored | ||
val variableNameWithSecretInItEmpty = "" | ||
val variableNameWithSecretInItOneChar = "X" | ||
val variableNameWithSecretInItAnonymous = "anonymous" | ||
var otherVariableNameWithAuthInIt: String? | ||
|
||
// Secret containing words and random characters should be filtered | ||
val secret001 = "sk_live_xf2fh0Hu3LqXlqqUg2DEWhEz" // Noncompliant | ||
val secret003 = "examples/commit/8e1d746900f5411e9700fea0" // Noncompliant | ||
val secret004 = "examples/commit/revision/469001e9700fea0" | ||
val secret006 = "abcdefghijklmnop" // Compliant | ||
val secret007 = "abcdefghijklmnopq" // Noncompliant | ||
val secret008 = "0123456789abcdef0" // Noncompliant | ||
val secret009 = "012345678901234567890123456789" // Noncompliant | ||
val secret010 = "abcdefghijklmnopabcdefghijkl" // Noncompliant | ||
val secret011 = "012345670123456701234567012345" // Noncompliant | ||
val secret012 = "012345678012345678012345678012" // Noncompliant | ||
val secret013 = "234.167.076.123" | ||
val secret015 = "org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH" | ||
// Example of Telegram bot token | ||
val secret016 = "bot123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" // Noncompliant | ||
// Secret with "&" | ||
val secret017 = "012&345678012345678012345&678012" // Noncompliant | ||
val secret018 = "&12&345678012345678012345&67801&" // Noncompliant | ||
|
||
// Simple constants will be filtered thanks to the entropy check | ||
val SECRET_INPUT = "[id='secret']" // Compliant | ||
val SECRET_PROPERTY = "custom.secret" // Compliant | ||
val TRUSTSTORE_SECRET = "trustStoreSecret" // Compliant | ||
val CONNECTION_SECRET = "connection.secret" // Compliant | ||
val RESET_SECRET = "/users/resetUserSecret" // Compliant | ||
val RESET_TOKEN = "/users/resetUserToken" // Compliant | ||
val secretToChar = "secret".toCharArray() // Compliant | ||
val secretToChar2 = "http-secret".toCharArray() // Compliant | ||
val secretToString = "http-secret".toString() // Compliant | ||
val secretFromGetSecret = getSecret("") // Compliant | ||
|
||
val CA_SECRET = "ca-secret" // Compliant | ||
val caSecret = CA_SECRET // Compliant | ||
|
||
// = in the middle or end is okay | ||
val secretWithBackSlashes8 = "abcdefghijklmnopqrs=" // Noncompliant | ||
val secretWithBackSlashes9 = "abcdefghijklmnopqrs==" // Noncompliant | ||
val secretWithBackSlashes10 = "abcdefghij=klmnopqrs" // Noncompliant | ||
|
||
// Only [a-zA-Z0-9_.+/~$-] are accepted as secrets characters | ||
val OkapiKeyboard = "what a strange QWERTY keyboard for animals" // Compliant | ||
val OKAPI_KEYBOARD = "what a strange QWERTY keyboard for animals" // Compliant | ||
val okApiKeyValue = "Spaces are UNEXPECTED 012 345 678" // Compliant | ||
val tokenism = "(Queen's Partner's Stored Knowledge is a Minimal Sham)" // Compliant | ||
|
||
// ========== 3. Assignment ========== | ||
fieldNameWithSecretInIt = "abcdefghijklmnopqrs" // Noncompliant | ||
this.fieldNameWithSecretInIt = "abcdefghijklmnopqrs" // Noncompliant | ||
// Secrets with less than 2 chars are explicitly ignored | ||
fieldNameWithSecretInIt = "X" | ||
// "anonymous" is explicitly ignored | ||
fieldNameWithSecretInIt = "anonymous" | ||
// Not hardcoded | ||
fieldNameWithSecretInIt = retrieveSecret() | ||
this.fieldNameWithSecretInIt = retrieveSecret() | ||
variable1 = "abcdefghijklmnopqrs" | ||
} | ||
|
||
private fun getSecret(s: String?): CharArray? { | ||
return null | ||
} | ||
|
||
private fun retrieveSecret(): String? { | ||
return null | ||
} | ||
|
||
companion object { | ||
private const val PASSED = "abcdefghijklmnopqrs" // compliant nothing to do with secrets | ||
private const val EMPTY = "" | ||
} | ||
} | ||
|
132 changes: 132 additions & 0 deletions
132
sonar-kotlin-checks/src/main/java/org/sonarsource/kotlin/checks/AbstractHardcodedVisitor.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
/* | ||
* SonarSource Kotlin | ||
* Copyright (C) 2018-2025 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 Sonar Source-Available License Version 1, as published by SonarSource SA. | ||
* | ||
* 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 Sonar Source-Available License for more details. | ||
* | ||
* You should have received a copy of the Sonar Source-Available License | ||
* along with this program; if not, see https://sonarsource.com/license/ssal/ | ||
*/ | ||
package org.sonarsource.kotlin.checks | ||
|
||
import com.intellij.psi.PsiElement | ||
import org.jetbrains.kotlin.lexer.KtTokens | ||
import org.jetbrains.kotlin.psi.KtBinaryExpression | ||
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression | ||
import org.jetbrains.kotlin.psi.KtElement | ||
import org.jetbrains.kotlin.psi.KtExpression | ||
import org.jetbrains.kotlin.psi.KtNameReferenceExpression | ||
import org.jetbrains.kotlin.psi.KtProperty | ||
import org.jetbrains.kotlin.psi.KtStringTemplateExpression | ||
import org.sonarsource.kotlin.api.checks.AbstractCheck | ||
import org.sonarsource.kotlin.api.frontend.KotlinFileContext | ||
|
||
|
||
abstract class AbstractHardcodedVisitor : AbstractCheck() { | ||
|
||
abstract val sensitiveVariableKind: String | ||
|
||
abstract val sensitiveWords: String | ||
|
||
private var variablePatterns: Sequence<Regex>? = null | ||
private var literalPatterns: Sequence<Regex>? = null | ||
|
||
companion object { | ||
private fun isQuery(value: String, match: String): Boolean { | ||
val followingString = value.substring(value.indexOf(match) + match.length) | ||
return (followingString.startsWith("=?") | ||
|| followingString.startsWith("=%") | ||
|| followingString.startsWith("=:") | ||
|| followingString.startsWith("={") // string format | ||
|| followingString == "='") | ||
} | ||
} | ||
|
||
override fun visitBinaryExpression(expression: KtBinaryExpression, context: KotlinFileContext) { | ||
if (expression.operationToken == KtTokens.EQ || expression.operationToken == KtTokens.PLUSEQ || expression.operationToken == KtTokens.EQEQ) { | ||
val left = expression.left | ||
left?.identifier()?.let { checkVariable(context, left, it, expression.right!!) } | ||
} | ||
} | ||
|
||
override fun visitProperty(property: KtProperty, context: KotlinFileContext) { | ||
property.initializer?.let { | ||
checkVariable(context, property.nameIdentifier!!, property.name!!, it) | ||
} | ||
} | ||
|
||
override fun visitStringTemplateExpression(expression: KtStringTemplateExpression, context: KotlinFileContext) { | ||
val content = if (!expression.hasInterpolation()) expression.asConstant() else "" | ||
literalPatterns() | ||
.mapNotNull { regex -> regex.find(content) } | ||
.filter { matchResult -> matchResult.groups.size > 2 } | ||
.filter { matchResult -> isSensitiveStringLiteral(matchResult.groups[2]!!.value) } | ||
.map { matchResult -> matchResult.groups[1]!!.value } | ||
.filter { match: String -> !isQuery(content, match) } | ||
.forEach { credential: String -> | ||
context.report(expression, credential) | ||
} | ||
} | ||
|
||
private fun KtElement.isSensitive() = this is KtStringTemplateExpression | ||
&& !this.hasInterpolation() | ||
&& isSensitiveStringLiteral(this.asConstant()) | ||
|
||
open fun isSensitiveStringLiteral(value: String): Boolean { | ||
return value.isNotEmpty() | ||
} | ||
|
||
private fun KotlinFileContext.report(tree: PsiElement, matchName: String) { | ||
reportIssue(tree, """"$matchName" detected here, make sure this is not a hard-coded $sensitiveVariableKind.""") | ||
} | ||
|
||
private fun KotlinFileContext.checkAssignedValue( | ||
matchResult: MatchResult, | ||
regex: Regex, | ||
leftHand: PsiElement, | ||
value: String | ||
) { | ||
if (!regex.containsMatchIn(value)) { | ||
report(leftHand, matchResult.groups[1]!!.value) | ||
} | ||
} | ||
|
||
private fun KtExpression.identifier(): String? = when (this) { | ||
is KtNameReferenceExpression -> getReferencedName() | ||
is KtDotQualifiedExpression -> selectorExpression?.identifier() | ||
else -> null | ||
} | ||
|
||
private fun checkVariable(ctx: KotlinFileContext, variable: PsiElement, variableName: String, value: KtElement) { | ||
if (value.isSensitive()) { | ||
variablePatterns() | ||
.mapNotNull { regex -> regex.find(variableName)?.let { it to regex } } | ||
.forEach { (matcher, regex) -> | ||
ctx.checkAssignedValue( | ||
matcher, | ||
regex, | ||
variable, | ||
(value as KtStringTemplateExpression).asConstant() | ||
) | ||
} | ||
} | ||
} | ||
|
||
private fun variablePatterns() = variablePatterns ?: toPatterns("") | ||
leveretka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
private fun literalPatterns() = literalPatterns ?: toPatterns("""=([^\s&]+)""") | ||
leveretka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
private fun toPatterns(suffix: String): Sequence<Regex> { | ||
return sensitiveWords.split(",").toTypedArray() | ||
.asSequence() | ||
.map { obj: String -> obj.trim { it <= ' ' } } | ||
.map { word: String -> Regex("($word)$suffix", RegexOption.IGNORE_CASE) } | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.