diff --git a/CHANGES.txt b/CHANGES.txt index 27a812a11a..e9d575f6b0 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -8,6 +8,7 @@ Merged from 4.1: * Add Paxos v2 option and informatin in cassandra.yaml (CASSANDRA-21316) Merged from 4.0: + * 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 8e18f34611..0f96815363 100644 --- a/src/java/org/apache/cassandra/cql3/PasswordObfuscator.java +++ b/src/java/org/apache/cassandra/cql3/PasswordObfuscator.java @@ -19,6 +19,7 @@ package org.apache.cassandra.cql3; import java.util.Optional; +import java.util.regex.Pattern; import org.apache.cassandra.auth.PasswordAuthenticator; import org.apache.cassandra.auth.RoleOptions; @@ -67,11 +68,11 @@ public class PasswordObfuscator if (!pass.isPresent() || pass.get().isEmpty()) return query; - // Regular expression: - // - Match new line and case insensitive (?si), and PASSWORD_TOKEN with greedy mode up to the start of the actual password and group it. - // - Quote the password between \Q and \E so any potential special characters are ignored - // - Replace the match with the grouped data + the obfuscated token - return query.replaceAll("((?si)"+ PASSWORD_TOKEN + ".+?)\\Q" + pass.get() + "\\E", - "$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 96f97b626d..e131fe42c3 100644 --- a/test/unit/org/apache/cassandra/cql3/PasswordObfuscatorTest.java +++ b/test/unit/org/apache/cassandra/cql3/PasswordObfuscatorTest.java @@ -18,14 +18,19 @@ 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 { @@ -34,6 +39,9 @@ public class PasswordObfuscatorTest private static final String plainPwd = "testpassword"; private static final String hashedPwd = "$2a$10$1fI9MDCe13ZmEYW4XXZibuASNKyqOY828ELGUtml/t.0Mk/6Kqnsq"; + public static final int ASCII_START = 32; + public static final int ASCII_END = 126; + @BeforeClass public static void startup() { @@ -362,4 +370,121 @@ public class PasswordObfuscatorTest "APPLY BATCH;", hashedPwd, hashedPwd), hashedPwdOpts)); } + + /** + * 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