diff --git a/CHANGES.txt b/CHANGES.txt index f39b7ff06a..fbc2301eac 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 4.0.21 + * Fix PasswordObfuscator failing to obfuscate certain passwords (CASSANDRA-21113) * Fix negative memtable allocator ownership when an update is shadowed by an existing row deletion (CASSANDRA-21469) * Consider first token of SSTable when calculating SSTable intersection in LeveledScanner (CASSANDRA-21369) * Remove inFlightEcho entry on ECHO_REQ failure (CASSANDRA-21428) diff --git a/src/java/org/apache/cassandra/cql3/PasswordObfuscator.java b/src/java/org/apache/cassandra/cql3/PasswordObfuscator.java index 89962f90d7..f648ccf87f 100644 --- a/src/java/org/apache/cassandra/cql3/PasswordObfuscator.java +++ b/src/java/org/apache/cassandra/cql3/PasswordObfuscator.java @@ -18,6 +18,8 @@ package org.apache.cassandra.cql3; +import java.util.regex.Pattern; + import com.google.common.base.Optional; import org.apache.cassandra.auth.PasswordAuthenticator; @@ -65,7 +67,11 @@ public class PasswordObfuscator if (!pass.isPresent() || pass.get().isEmpty()) return query; - // match new line, case insensitive (?si), and PASSWORD_TOKEN up to the actual password greedy. Group that and replace the password - return query.replaceAll("((?si)"+ PASSWORD_TOKEN + ".+?)" + pass.get(), "$1" + PasswordObfuscator.OBFUSCATION_TOKEN); + // CQL-escape the password, the password in RoleOptions is unescaped, but the query contains the escaped form. + String escapedPassword = pass.get().replace("'", "''"); + + // Use Pattern.quote() to safely handle all special regex characters. + String pattern = "((?si)" + PASSWORD_TOKEN + ".+?)" + Pattern.quote(escapedPassword); + return query.replaceAll(pattern, "$1" + OBFUSCATION_TOKEN); } } diff --git a/test/unit/org/apache/cassandra/cql3/PasswordObfuscatorTest.java b/test/unit/org/apache/cassandra/cql3/PasswordObfuscatorTest.java index 09a366e49c..f1bebae139 100644 --- a/test/unit/org/apache/cassandra/cql3/PasswordObfuscatorTest.java +++ b/test/unit/org/apache/cassandra/cql3/PasswordObfuscatorTest.java @@ -18,24 +18,31 @@ package org.apache.cassandra.cql3; +import java.util.ArrayList; +import java.util.List; + import org.junit.BeforeClass; import org.junit.Test; +import org.apache.cassandra.auth.IRoleManager; import org.apache.cassandra.auth.RoleOptions; import static org.apache.cassandra.cql3.PasswordObfuscator.*; import static java.lang.String.format; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; public class PasswordObfuscatorTest { private static final RoleOptions opts = new RoleOptions(); private static final String optsPassword = "testpassword"; + public static final int ASCII_START = 32; + public static final int ASCII_END = 126; @BeforeClass public static void startup() { - opts.setOption(org.apache.cassandra.auth.IRoleManager.Option.PASSWORD, "testpassword"); + opts.setOption(org.apache.cassandra.auth.IRoleManager.Option.PASSWORD, optsPassword); } @Test @@ -237,4 +244,121 @@ public class PasswordObfuscatorTest "APPLY BATCH;", optsPassword, optsPassword), opts)); } + + /** + * Tests that passwords containing the literal string \E are properly obfuscated. + *
+ * The \E sequence is special in Java regex as it ends a \Q...\E quoted block. + * If inline \Q...\E quoting is used instead of Pattern.quote(), passwords containing + * \E will break the regex pattern and cause either an exception or failed obfuscation. + */ + @Test + public void testPasswordWithRegexEndQuote() + { + // \E in the middle + assertPasswordObfuscated("secret\\Epassword"); + + // \E at the start + assertPasswordObfuscated("\\Esecretpassword"); + + // \E at the end + assertPasswordObfuscated("secretpassword\\E"); + + // \E followed by regex special char + + assertPasswordObfuscated("secret\\E+password"); + + // Multiple \E sequences + assertPasswordObfuscated("sec\\Eret\\Epass"); + } + + /** + * Helper method to test that a password is properly obfuscated in a CREATE ROLE statement. + * + * @param password the password to test (unescaped, as stored in RoleOptions) + */ + private void assertPasswordObfuscated(String password) + { + RoleOptions roleOpts = new RoleOptions(); + roleOpts.setOption(IRoleManager.Option.PASSWORD, password); + + // Escape single quotes for CQL query string + String escapedPassword = password.replace("'", "''"); + String query = format("CREATE ROLE role1 WITH PASSWORD = '%s'", escapedPassword); + String expected = format("CREATE ROLE role1 WITH PASSWORD = '%s'", OBFUSCATION_TOKEN); + + assertEquals("Password should be obfuscated: " + password, expected, obfuscate(query, roleOpts)); + } + + /** + * Tests that passwords containing each printable special character (outside a-z, A-Z, 0-9) + * are properly obfuscated by PasswordObfuscator. + *
+ * This test iterates through all printable ASCII special characters (32-126, excluding
+ * alphanumeric) and tests each character at three positions: start, middle, and end of
+ * the password. Any failures are collected and reported at the end.
+ */
+ @Test
+ public void testAllPrintableSpecialCharactersObfuscation()
+ {
+ List