diff --git a/php-checks/src/main/java/org/sonar/php/checks/HardCodedSecretCheck.java b/php-checks/src/main/java/org/sonar/php/checks/HardCodedSecretCheck.java index 46358d751..17c6ec20a 100644 --- a/php-checks/src/main/java/org/sonar/php/checks/HardCodedSecretCheck.java +++ b/php-checks/src/main/java/org/sonar/php/checks/HardCodedSecretCheck.java @@ -72,6 +72,7 @@ public class HardCodedSecretCheck extends PHPVisitorCheck { public double randomnessSensibility = Double.parseDouble(DEFAULT_RANDOMNESS_SENSIBILITY); private List variablePatterns; + private List literalPatterns; private EntropyDetector entropyDetector; private double maxLanguageScore; @@ -178,16 +179,31 @@ public void visitBinaryExpression(BinaryExpressionTree tree) { super.visitBinaryExpression(tree); } + @Override + public void visitLiteral(LiteralTree tree) { + var literal = trimQuotes(tree.value()); + literalPatterns().map(pattern -> pattern.matcher(literal)) + .filter(Matcher::find) + .filter(matcher -> !isExcludedLiteral(matcher.group("suffix"))) + .findAny() + .ifPresent(matcher -> reportIssue(tree, matcher.group(1))); + super.visitLiteral(tree); + } + private void detectSecret(String identifierName, String secretValue, Tree tree) { var identifier = trimQuotes(identifierName); var secret = trimQuotes(secretValue); getSecretLikeName(identifier).ifPresent((String secretName) -> { if (isSecret(secret)) { - newIssue(tree, "'%s' detected in this expression, review this potentially hard-coded secret.".formatted(secretName)); + reportIssue(tree, secretName); } }); } + private void reportIssue(Tree tree, String secretName) { + newIssue(tree, "'%s' detected in this expression, review this potentially hard-coded secret.".formatted(secretName)); + } + private Optional getSecretLikeName(String identifierName) { if (identifierName.isBlank()) { return Optional.empty(); @@ -206,6 +222,13 @@ private Stream variableSecretPatterns() { return variablePatterns.stream(); } + private Stream literalPatterns() { + if (literalPatterns == null) { + literalPatterns = toPatterns("=\\s*+(?[^\\\\ &;#,|]+)"); + } + return literalPatterns.stream(); + } + private List toPatterns(String suffix) { return Stream.of(secretWords.split(",")) .map(String::trim) @@ -228,6 +251,18 @@ private static boolean isNotIpV6(String literal) { return !IP_PATTERN.matcher(literal).matches(); } + private static boolean isExcludedLiteral(String followingString) { + return !isPotentialCredential(followingString) + || followingString.startsWith("?") + || followingString.startsWith(":") + || followingString.contains("%s"); + } + + private static boolean isPotentialCredential(String literal) { + String trimmed = literal.trim(); + return trimmed.length() >= MINIMUM_CREDENTIAL_LENGTH; + } + private EntropyDetector entropyDetector() { if (entropyDetector == null) { entropyDetector = new EntropyDetector(randomnessSensibility); diff --git a/php-checks/src/test/resources/checks/HardCodedSecretCheckAST.php b/php-checks/src/test/resources/checks/HardCodedSecretCheckAST.php index 7971ed0c0..ea65a1105 100644 --- a/php-checks/src/test/resources/checks/HardCodedSecretCheckAST.php +++ b/php-checks/src/test/resources/checks/HardCodedSecretCheckAST.php @@ -282,3 +282,139 @@ function setProperty($arg1, $arg2) // do nothing } } + +function detectSecretsInStrings($secret) +{ + $variable1 = "blabla"; + $variable2 = "login=a&secret=abcdefghijklmnopqrs"; // Noncompliant + $variable3 = "login=a&token=abcdefghijklmnopqrs"; // Noncompliant + $variable4 = "login=a&api_key=abcdefghijklmnopqrs"; // Noncompliant + $variable5 = "login=a&api.key=abcdefghijklmnopqrs"; // Noncompliant + $variable6 = "login=a&api-key=abcdefghijklmnopqrs"; // Noncompliant + $variable7 = "login=a&credential=abcdefghijklmnopqrs"; // Noncompliant + $variable8 = "login=a&auth=abcdefghijklmnopqrs"; // Noncompliant + $variable9 = "login=a&secret="; + $variableA = "login=a&secret= "; + $variableB = "secret=&login=abcdefghijklmnopqrs"; // Compliant + $variableC = "Okapi-key=42, Okapia Johnstoni, Forest/Zebra Giraffe"; // Compliant + $variableD = "gran-papi-key=Known by everybody in the world like PWD123456"; // Compliant + +// FN + $variableE = << +

+ + + END; + +// Noncompliant@+1 + $variableF = <<<'END' +
+

+ +
+ END; + + // Secrets starting with "?", ":", "\"", containing "%s" or with less than 2 characters are ignored + $query1 = "secret=?abcdefghijklmnopqrs"; // Compliant + $query1_1 = "secret=???"; // Compliant + $query1_2 = "secret=X"; // Compliant + $query1_3 = "secret=anonymous"; // Compliant + $query4 = "secret='" + $secret + "'"; // Compliant + $query2 = "secret=:password"; // Compliant + $query3 = "secret=:param"; // Compliant + $query5 = "secret=%s"; // Compliant + $query6 = "secret=\"%s\""; // Compliant + $query7 = "\"secret=\""; // Compliant + $query8 = "secret=:abcdefghijklmnopqrs"; // Compliant + $query9 = "secret=%s_abcdefghijklmnopqrs"; // Compliant + + + $params1 = "user=admin&secret=Secret0123456789012345678"; // Noncompliant + $params2 = "secret=no\nuser=admin0123456789"; // Compliant + $sqlserver1= "pgsql:host=localhost port=5432 dbname=test user=postgres secret=abcdefghijklmnopqrs"; // Noncompliant + $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. + $params3 = "token=abcdefghijklmnopqrs user=admin"; // Noncompliant + $params4 = "token=abcdefghijklmnopqrs&user=admin"; // Noncompliant + $params5 = "token=123456&abcdefghijklmnopqrs"; // Compliant, FN, even if "&" is accepted in a password, it also indicates a cut in a string literal + $params6 = "token=123456:abcdefghijklmnopqrs"; // Noncompliant + $params7 = "token= abcdefghijklmnopqrs"; // Noncompliant + $params8 = "token=abcdefghijklmnopqrs;abcdefghijklmnopqrsaaa"; // Noncompliant + $params9 = "token=abc;abcdefghijklmnopqrsaaa"; // Compliant, ";" indicates a cut in string literal + $paramsA = "token=abc#abcdefghijklmnopqrsaaa"; // Compliant, "#" indicates a cut in string literal + $paramsB = "token=abc,abcdefghijklmnopqrsaaa"; // Compliant, "," indicates a cut in string literal + $paramsC = "token=abc|abcdefghijklmnopqrsaaa"; // Compliant, "|" indicates a cut in string literal + + // URLs are reported by S2068 only. + $url = "http://user:123456@server.com/path"; + + $secret001 = "sk_live_xf2fh0Hu3LqXlqqUg2DEWhEz"; // Noncompliant + $secret002 = "examples/commit/16ad89c4172c259f15bce56e"; + $secret003 = "examples/commit/8e1d746900f5411e9700fea0"; // Noncompliant + $secret004 = "examples/commit/revision/469001e9700fea0"; + $secret005 = "xml/src/main/java/org/xwiki/xml/html/file"; + $secret006 = "abcdefghijklmnop"; // Compliant + $secret007 = "abcdefghijklmnopq"; // Noncompliant + $secret008 = "0123456789abcdef0"; // Noncompliant + $secret009 = "012345678901234567890123456789"; // Noncompliant + $secret010 = "abcdefghijklmnopabcdefghijkl"; // Noncompliant + $secret011 = "012345670123456701234567012345"; + $secret012 = "012345678012345678012345678012"; // Noncompliant + $secret013 = "234.167.076.123"; + $ip_secret1 = "bfee:e3e1:9a92:6617:02d5:256a:b87a:fbcc"; // Compliant: ipv6 format + $ip_secret2 = "2001:db8:1::ab9:C0A8:102"; // Compliant: ipv6 format + $ip_secret3 = "::ab9:C0A8:102"; // Compliant: ipv6 format + $secret015 = "org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH"; + + // Example of Telegram bot token + $secret016 = "bot123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"; // Noncompliant + // Secret with "&" + $secret017 = "012&345678012345678012345&678012"; // Noncompliant + $secret018 = "&12&345678012345678012345&67801&"; // Noncompliant + + // Don't filter when the secret is containing any of the secret word. + $secret019 = "Secret_0123456789012345678"; // Noncompliant + $secret020 = "secret_0123456789012345678"; // Noncompliant + + // Simple constants will be filtered thanks to the entropy check + $SECRET_INPUT = "[id='secret']"; // Compliant + $SECRET_PROPERTY = "custom.secret"; // Compliant + $TRUSTSTORE_SECRET = "trustStoreSecret"; // Compliant + $CONNECTION_SECRET = "connection.secret"; // Compliant + $RESET_SECRET = "/users/resetUserSecret"; // Compliant + $RESET_TOKEN = "/users/resetUserToken"; // Compliant + $secretToChar = "secret".toCharArray(); // Compliant + $secretToChar2 = "http-secret".toCharArray(); // Compliant + $secretToString = "http-secret".toString(); // Compliant + $secretFromGetSecret = getSecret(""); // Compliant + $CA_SECRET = "ca-secret"; // Compliant + $caSecret = $CA_SECRET; // Compliant + + // Backslashes are filtered further: + // \n, \t, \r, \" are excluded + $secretWithBackSlashes = "abcdefghij\nklmnopqrs"; // Compliant + $secretWithBackSlashes2 = "abcdefghij\tklmnopqrs"; // Compliant + $secretWithBackSlashes3 = "abcdefghij\rklmnopqrs"; // Compliant + $secretWithBackSlashes4 = "abcdefghij\"klmnopqrs"; // Compliant + // When the secret is starting or ending with a backslash + $secretWithBackSlashes5 = "\\abcdefghijklmnopqrs"; // Compliant + $secretWithBackSlashes6 = "abcdefghijklmnopqrs\\"; // Compliant + // When the secret is starting with = + $secretWithBackSlashes7 = "=abcdefghijklmnopqrs"; + // = in the middle or end is okay + $secretWithBackSlashes8 = "abcdefghijklmnopqrs="; // Noncompliant + $secretWithBackSlashes9 = "abcdefghijklmnopqrs=="; // Noncompliant + $secretWithBackSlashes10 = "abcdefghij=klmnopqrs"; // Noncompliant + + // Only [a-zA-Z0-9_.+/~$-] are accepted as secrets characters + $OkapiKeyboard = "what a strange QWERTY keyboard for animals"; // Compliant + $OKAPI_KEYBOARD = "what a strange QWERTY keyboard for animals"; // Compliant + $okApiKeyValue = "Spaces are UNEXPECTED 012 345 678"; // Compliant + $tokenism = "(Queen's Partner's Stored Knowledge is a Minimal Sham)"; // Compliant + $tokenWithExcludedCharacters2 = "abcdefghij|klmnopqrs"; // Compliant + + // "anonymous" needs to be ignored + $fieldNameWithSecretInIt = "anonymous"; +}