Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/main/java/com/zaxxer/hikari/util/UtilityElf.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,29 @@ public final class UtilityElf
private static final Logger LOGGER = LoggerFactory.getLogger(UtilityElf.class);

/**
* A pattern to match and mask passwords in JDBC URLs.
* It looks for the "password" parameter in the URL and replaces its value with "<masked>".
* A pattern to mask the password query parameter in JDBC URLs (e.g. {@code ?password=secret}).
*/
private static final Pattern PASSWORD_MASKING_PATTERN = Pattern.compile("([?&;][^&#;=]*[pP]assword=)[^&#;]*");

/**
* A pattern to mask the password embedded in the authority of JDBC URLs
* (e.g. {@code jdbc:postgresql://user:password@host}). Only the secret that follows the
* first colon of the userinfo is masked; the username is preserved for log readability.
* The password component is delimited by the URI reserved delimiters {@code / ? # @} per
* RFC 3986, so a query value containing an at-sign (e.g. {@code ?user=admin@corp.com})
* is never swallowed; passwords containing those delimiters unencoded are left as-is.
*/
private static final Pattern AUTHORITY_PASSWORD_MASKING_PATTERN = Pattern.compile("(://[^/?#@:]*:)[^/?#@]*(@)");

private UtilityElf()
{
// non-constructable
}

public static String maskPasswordInJdbcUrl(String jdbcUrl)
{
return PASSWORD_MASKING_PATTERN.matcher(jdbcUrl).replaceAll("$1<masked>");
String masked = PASSWORD_MASKING_PATTERN.matcher(jdbcUrl).replaceAll("$1<masked>");
return AUTHORITY_PASSWORD_MASKING_PATTERN.matcher(masked).replaceAll("$1<masked>$2");
}

/**
Expand Down
124 changes: 124 additions & 0 deletions src/test/java/com/zaxxer/hikari/util/UtilityElfTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,130 @@ public void shouldCreateInstanceOfClassWithConstructorThatAcceptsSuperClassAndIn
new ClassD());
}

@Test
public void shouldMaskPasswordQueryParameter()
{
//Arrange
String url = "jdbc:mysql://host:3306/db?password=secret&user=admin";

//Act
String masked = UtilityElf.maskPasswordInJdbcUrl(url);

//Assert
assertEquals("jdbc:mysql://host:3306/db?password=<masked>&user=admin", masked);
}

@Test
public void shouldMaskPasswordEmbeddedInUrlAuthority()
{
//Arrange
String url = "jdbc:postgresql://admin:s3cret@db.internal:5432/prod";

//Act
String masked = UtilityElf.maskPasswordInJdbcUrl(url);

//Assert
assertEquals("jdbc:postgresql://admin:<masked>@db.internal:5432/prod", masked);
}

@Test
public void shouldMaskBothAuthorityAndQueryParameterPasswords()
{
//Arrange
String url = "jdbc:postgresql://user:pass@host/db?password=other";

//Act
String masked = UtilityElf.maskPasswordInJdbcUrl(url);

//Assert
assertEquals("jdbc:postgresql://user:<masked>@host/db?password=<masked>", masked);
}

@Test
public void shouldPreservePortWhenMaskingAuthorityPassword()
{
//Arrange
String url = "jdbc:mysql://user:pass@host:3306/db";

//Act
String masked = UtilityElf.maskPasswordInJdbcUrl(url);

//Assert
assertEquals("jdbc:mysql://user:<masked>@host:3306/db", masked);
}

@Test
public void shouldNotMaskUrlWithoutAuthorityPassword()
{
//Arrange
String url = "jdbc:postgresql://host:5432/db?user=admin";

//Act
String masked = UtilityElf.maskPasswordInJdbcUrl(url);

//Assert
assertEquals(url, masked);
}

@Test
public void shouldMaskEverythingAfterFirstColonInAuthority()
{
//Arrange
String url = "jdbc:postgresql://user:p:a:ss@host/db";

//Act
String masked = UtilityElf.maskPasswordInJdbcUrl(url);

//Assert
assertEquals("jdbc:postgresql://user:<masked>@host/db", masked);
}

@Test
public void shouldNotMaskUrlWithAtSignInQueryParameter()
{
//Arrange
String url = "jdbc:mysql://host:3306/db?user=admin@corp.com";

//Act
String masked = UtilityElf.maskPasswordInJdbcUrl(url);

//Assert
assertEquals(url, masked);
}

@Test
public void shouldMaskAuthorityPasswordWithEmptyUsername()
{
//Arrange
String url = "jdbc:postgresql://:secret@host:5432/db";

//Act
String masked = UtilityElf.maskPasswordInJdbcUrl(url);

//Assert
assertEquals("jdbc:postgresql://:<masked>@host:5432/db", masked);
}

@Test
public void shouldNotMaskMalformedPasswordsWithReservedDelimiters()
{
//Arrange: RFC 3986 reserves / ? # as delimiters inside the authority, so these
//passwords cannot be told apart from path/query content by a regex over the URL.
String urlWithSlash = "jdbc:mysql://app:Sup3r/Secur3@dbhost/db";
String urlWithQuestion = "jdbc:mysql://app:Secret?x@dbhost/db";
String urlWithHash = "jdbc:mysql://app:Secret#x@dbhost/db";

//Act
String maskedSlash = UtilityElf.maskPasswordInJdbcUrl(urlWithSlash);
String maskedQuestion = UtilityElf.maskPasswordInJdbcUrl(urlWithQuestion);
String maskedHash = UtilityElf.maskPasswordInJdbcUrl(urlWithHash);

//Assert
assertEquals(urlWithSlash, maskedSlash);
assertEquals(urlWithQuestion, maskedQuestion);
assertEquals(urlWithHash, maskedHash);
}

public static class ClassA {}

public static final class ClassB extends ClassA {}
Expand Down