Merge branch 'cassandra-4.1' into cassandra-5.0

This commit is contained in:
Stefan Miklosovic 2026-06-19 15:17:18 +02:00
commit 1a87bf5142
No known key found for this signature in database
GPG Key ID: 32F35CB2F546D93E
3 changed files with 133 additions and 6 deletions

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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.
* <p>
* 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.
* <p>
* 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<String> failures = new ArrayList<>();
for (char c = ASCII_START; c <= ASCII_END; c++)
{
// Skip alphanumeric characters
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))
continue;
// Test character at start, middle, and end of password
String[] passwords = {
c + "password", // at start
"pass" + c + "word", // in middle
"password" + c // at end
};
for (String password : passwords)
{
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);
RoleOptions testOpts = new RoleOptions();
testOpts.setOption(IRoleManager.Option.PASSWORD, password);
try
{
String result = obfuscate(query, testOpts);
if (!expected.equals(result))
{
if (result.contains(escapedPassword) || result.contains(password))
{
failures.add(format("Password '%s': PASSWORD LEAKED - Result: %s",
password, result));
}
else
{
failures.add(format("Password '%s': Unexpected result - Expected: %s, Got: %s",
password, expected, result));
}
}
}
catch (Exception e)
{
failures.add(format("Password '%s': Exception thrown - %s: %s",
password, e.getClass().getSimpleName(), e.getMessage()));
}
}
}
if (!failures.isEmpty())
{
StringBuilder sb = new StringBuilder();
sb.append(format("%d password(s) with special characters failed obfuscation:\n", failures.size()));
for (String failure : failures)
{
sb.append(" - ").append(failure).append("\n");
}
fail(sb.toString());
}
}
}