From cc74b2932b9d072350bcaa114a8f27839c26823e Mon Sep 17 00:00:00 2001 From: David Edwards Date: Thu, 27 Jun 2019 13:18:30 +0100 Subject: [PATCH] REPO-4305: jmx dump password redaction inconsistent (#499) Methods and tests have been added to handle removal of passwords from the InputCommands section of the JMX dump as this is not accessible by our repository-jmx-repository.xml. Added a static string[] that can be altered to provide control over arguments that are redacted. New method added to dynamically create the regex pattern, based on provided argument. (cherry picked from commit 2ce690a473e1f126525fcb84b926c95698370ce5) --- .../alfresco/repo/management/JmxDumpUtil.java | 128 ++++++++++++++++++ .../repo/management/JmxDumpUtilTest.java | 121 ++++++++++++++--- 2 files changed, 233 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/alfresco/repo/management/JmxDumpUtil.java b/src/main/java/org/alfresco/repo/management/JmxDumpUtil.java index 186c15ec67..750d171e09 100644 --- a/src/main/java/org/alfresco/repo/management/JmxDumpUtil.java +++ b/src/main/java/org/alfresco/repo/management/JmxDumpUtil.java @@ -30,13 +30,17 @@ import java.io.PrintWriter; import java.lang.reflect.Array; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.StringJoiner; import java.util.TreeMap; import java.util.TreeSet; +import java.util.regex.Pattern; import javax.management.JMException; import javax.management.MBeanAttributeInfo; @@ -71,6 +75,10 @@ public class JmxDumpUtil private static final String OS_NAME = "os.name"; + private static final String INPUT_ARGUMENTS = "InputArguments"; + + private static final String[] REDACTED_INPUTS = {"password","token","pwd"}; + /** * Dumps a local or remote MBeanServer's entire object tree for support purposes. Nested arrays and CompositeData * objects in MBean attribute values are handled. @@ -164,9 +172,129 @@ public class JmxDumpUtil attributes.put(OS_NAME, updateOSNameAttributeForLinux(osName)); } } + if (objectName.getCanonicalName().equals("java.lang:type=Runtime")) + { + String[] commandInputs = (String[]) attributes.get(INPUT_ARGUMENTS); + if(commandInputs != null) + { + try + { + attributes.put(INPUT_ARGUMENTS, cleanPasswordsFromInputArguments(commandInputs,REDACTED_INPUTS)); + } catch (IllegalArgumentException e) + { + attributes.put(INPUT_ARGUMENTS, commandInputs); + } + } + } tabulate(JmxDumpUtil.NAME_HEADER, JmxDumpUtil.VALUE_HEADER, attributes, out, 0); } + /** + * Replaces strings with JmxDumpUtil.PROTECTED_VALUE, + * if any of the string that contains a string from redactedInputs. + * + * @see #cleanPasswordFromInputArgument + * @param commandInputs one or more strings of input arguments + * @param redactedInputs one or more strings, that end input arguments that are to be redacted + * @return commandInputs with any arguments ending in redactedInputs with redacted values + */ + static String[] cleanPasswordsFromInputArguments(String[] commandInputs, String[] redactedInputs) + { + Pattern passwordRedactPattern = Pattern.compile(createPasswordFindRegexString(redactedInputs)); + List cleanInputs = new ArrayList(); + for (String input : commandInputs) + { + input = cleanPasswordFromInputArgument(input, passwordRedactPattern); + cleanInputs.add(input); + } + + return cleanInputs.toArray(new String[commandInputs.length]); + } + + /** + * Removes any characters the word/s provided in redactedInputs + * and replaces them with JmxDumpUtil.PROTECTED_VALUE + *

+ * Example: + *

+ * Input: -Ddb.password=alfresco + *

+ * Output: -Ddb.password=******** + *

+ * + * @param input String + * @param redactedInputs String[] + * @return password redacted string if input matches a string in redactedInputs, an un-altered string will be returned if it does not match. + */ + static String cleanPasswordFromInputArgument(String input, Pattern redactedInputPattern) + { + //Replace the whole string with just capture group 1 to remove the desired value and concat the protected value. + String output = redactedInputPattern.matcher(input).replaceAll("$1"+PROTECTED_VALUE); + return output; + } + + /** + * Creates a regular expression that will select a string that contains one of the values provided in argEndings, proceeding an "=" and defines two capture groups: + *
    + *
  • Group 1: An argEnding that is followed by an "=", including the "=" and all character prior to the argEnding. + *
  • Group 2: The characters that follow group 1, to the end of the string or new line. + *
+ *

+ * The argEnding can be the whole Input argument or the common characters proceeding the = sign. + * Example argEndings: + *

    + *
  • -Ddb.password This will select the values passed as -Ddb.password + *
  • password This will select any potential values that end in the word password + *
+ *

+ * Example usage: + *

+ * argEndings={"password", "pwd"} + *

+ * This will create a regex that will match a string that contains either argEndings. + * The following will be matched by the resulting regex: + *

+ * "-Ddb.password=my_password" + *

+ * For this example: group 1="-Ddb.password=" group 2="my_password" + * + * + * @param argEndings Strings that will end the input argument you wish to select + * @return Regex pattern for selecting the characters following the strings passed as argEndings + */ + static String createPasswordFindRegexString(String[] argEndings) throws IllegalArgumentException + { + if(argEndings.length<1) + { + IllegalArgumentException e = new IllegalArgumentException("Arguments are required"); + throw e; + } + + StringJoiner argJoiner = new StringJoiner("|"); + + for (String argEnding : argEndings) + { + argJoiner.add(escapeRegexMetaChars(argEnding)+"="); + } + + String regex = String.format("%s%s%s%s%s", + "(?i)(.*(", argJoiner.toString(),"))((?<=",argJoiner.toString(), ").*+)"); + return regex; + } + + /** + * Places an escape character in front of any regex meta charater: | ? * + . + * + * @param input + * @return + */ + static String escapeRegexMetaChars (String input) + { + String pattern = "(\\||\\?|\\*|\\+|\\.)"; + String output = input.replaceAll(pattern, "\\\\$1"); + return output; + } + /** * Adds a Linux version * diff --git a/src/test/java/org/alfresco/repo/management/JmxDumpUtilTest.java b/src/test/java/org/alfresco/repo/management/JmxDumpUtilTest.java index 05b9cffe5f..806b03c922 100644 --- a/src/test/java/org/alfresco/repo/management/JmxDumpUtilTest.java +++ b/src/test/java/org/alfresco/repo/management/JmxDumpUtilTest.java @@ -23,23 +23,112 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.repo.management; - +package org.alfresco.repo.management; + +import static org.junit.Assert.assertArrayEquals; + +import java.util.regex.Pattern; + import org.alfresco.util.ApplicationContextHelper; +import org.junit.Test; import org.springframework.context.ApplicationContext; -import junit.framework.TestCase; - -public class JmxDumpUtilTest extends TestCase -{ - public void testUpdateOSNameAttribute() throws Exception +import junit.framework.TestCase; + +public class JmxDumpUtilTest extends TestCase +{ + public void testUpdateOSNameAttribute() throws Exception { - ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); - String osName = System.getProperty("os.name"); - if (osName.toLowerCase().startsWith("linux")) - { - String attr = JmxDumpUtil.updateOSNameAttributeForLinux(osName); - assertTrue(attr.toLowerCase().startsWith("linux (")); - } - } -} + ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + String osName = System.getProperty("os.name"); + if (osName.toLowerCase().startsWith("linux")) + { + String attr = JmxDumpUtil.updateOSNameAttributeForLinux(osName); + assertTrue(attr.toLowerCase().startsWith("linux (")); + } + } + + @Test + public void testCleanPasswordsFromInputArgument() throws Exception + { + Pattern pattern = Pattern.compile("(?i)(.*(password=|pwd=|token=))((?<=password=|pwd=|token=).*+)"); + String passwordArg = "-Ddb.password=I should be stars \"£$%^&*()@"; + String expected = "-Ddb.password="+JmxDumpUtil.PROTECTED_VALUE; + String actual = JmxDumpUtil.cleanPasswordFromInputArgument(passwordArg,pattern); + assertEquals("Expectected output: "+ expected +" Actual output: "+actual, expected, actual); + + passwordArg = "-Ddb.paSsword=@"; + expected = "-Ddb.paSsword="+JmxDumpUtil.PROTECTED_VALUE; + actual = JmxDumpUtil.cleanPasswordFromInputArgument(passwordArg, pattern); + assertEquals("Expectected output: "+ expected +" Actual output: "+actual, expected, actual); + + passwordArg = "somePrefix.token=\"If i'm not replaced, something has gone very wrong\""; + expected = "somePrefix.token="+JmxDumpUtil.PROTECTED_VALUE; + actual = JmxDumpUtil.cleanPasswordFromInputArgument(passwordArg, pattern); + assertEquals("Expectected output: "+ expected +" Actual output: "+actual, expected, actual); + + passwordArg = "yetanotherpwd="; + expected = "yetanotherpwd="+JmxDumpUtil.PROTECTED_VALUE; + actual = JmxDumpUtil.cleanPasswordFromInputArgument(passwordArg, pattern); + assertEquals("Expectected output: "+ expected +" Actual output: "+actual, expected, actual); + + passwordArg = "AnyOtherArgument=\"I should still be here\""; + expected = "AnyOtherArgument=\"I should still be here\""; + actual = JmxDumpUtil.cleanPasswordFromInputArgument(passwordArg, pattern); + assertEquals("Expectected output :"+ expected +" Actual output :"+actual, expected, actual); + + } + @Test + public void testCleanPasswordsFromInputArguments() throws Exception + { + String[] argEndingsTypical = {"password", "token","pwd"}; + String[] args = {"-Ddb.password=alfresco", "-Ddb.user=alfresco", "-DtestToken=asdoij3ifiej22244ojpgkmkfpsi3j55643pojpdjoismvi4563625mposvsd"}; + String[] expectedArray = {"-Ddb.password="+JmxDumpUtil.PROTECTED_VALUE, "-Ddb.user=alfresco", "-DtestToken="+JmxDumpUtil.PROTECTED_VALUE}; + String[] actualArray = JmxDumpUtil.cleanPasswordsFromInputArguments(args, argEndingsTypical); + assertArrayEquals("Expectected output:"+expectedArray+" Actual output:"+actualArray,expectedArray,actualArray); + + args = new String[]{"-Ddb.port=1234", "-Ddb.user=alfresco", "-DtestArg=Test1234password"}; + expectedArray = new String[]{"-Ddb.port=1234", "-Ddb.user=alfresco", "-DtestArg=Test1234password"}; + actualArray = JmxDumpUtil.cleanPasswordsFromInputArguments(args, argEndingsTypical); + assertArrayEquals("Expectected output:"+expectedArray+" Actual output:"+actualArray, expectedArray, actualArray); + + + + } + @Test + public void testCreatePasswordFindRegexString() throws Exception + { + String[] argEndings = {"password", "any old ending :D", "token"}; + String expected = "(?i)(.*(password=|any old ending :D=|token=))((?<=password=|any old ending :D=|token=).*+)"; + String actual = JmxDumpUtil.createPasswordFindRegexString(argEndings); + assertEquals("Expectected output :"+expected+" Actual output :"+actual,expected, actual); + + String[] argEndings2 = {"?", "\"£$%^&*"}; + expected = "(?i)(.*(\\?=|\"£$%^&\\*=))((?<=\\?=|\"£$%^&\\*=).*+)"; + actual = JmxDumpUtil.createPasswordFindRegexString(argEndings2); + assertEquals("Expectected output :"+expected+" Actual output :"+actual,expected, actual); + + String[] emptyArgs = {}; + try + { + JmxDumpUtil.createPasswordFindRegexString(emptyArgs); + fail("expected exception was not occured."); + } catch(IllegalArgumentException e) + { + + } + } + @Test + public void testEscapeRegexMetaChars() + { + String input = "|?*+."; + String expected = "\\|\\?\\*\\+\\."; + String actual = JmxDumpUtil.escapeRegexMetaChars(input); + assertEquals("Expectected output :" + expected + " Actual output :" + actual, expected, actual); + + input = "Let's.Add++,*complexity?!\""; + expected = "Let's\\.Add\\+\\+,\\*complexity\\?!\""; + actual = JmxDumpUtil.escapeRegexMetaChars(input); + assertEquals("Expectected output :" + expected + " Actual output :" + actual, expected, actual); + } +}