mirror of https://github.com/apache/cassandra
Fix PasswordObfuscator failing to obfuscate certain passwords
patch by Stefan Miklosovic; reviewed by Dmitry Konstantinov for CASSANDRA-21113
This commit is contained in:
parent
d30ac083b8
commit
22bbac9d6a
|
|
@ -1,4 +1,5 @@
|
||||||
4.0.21
|
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)
|
* 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)
|
* Consider first token of SSTable when calculating SSTable intersection in LeveledScanner (CASSANDRA-21369)
|
||||||
* Remove inFlightEcho entry on ECHO_REQ failure (CASSANDRA-21428)
|
* Remove inFlightEcho entry on ECHO_REQ failure (CASSANDRA-21428)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@
|
||||||
|
|
||||||
package org.apache.cassandra.cql3;
|
package org.apache.cassandra.cql3;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
|
|
||||||
import org.apache.cassandra.auth.PasswordAuthenticator;
|
import org.apache.cassandra.auth.PasswordAuthenticator;
|
||||||
|
|
@ -65,7 +67,11 @@ public class PasswordObfuscator
|
||||||
if (!pass.isPresent() || pass.get().isEmpty())
|
if (!pass.isPresent() || pass.get().isEmpty())
|
||||||
return query;
|
return query;
|
||||||
|
|
||||||
// match new line, case insensitive (?si), and PASSWORD_TOKEN up to the actual password greedy. Group that and replace the password
|
// CQL-escape the password, the password in RoleOptions is unescaped, but the query contains the escaped form.
|
||||||
return query.replaceAll("((?si)"+ PASSWORD_TOKEN + ".+?)" + pass.get(), "$1" + PasswordObfuscator.OBFUSCATION_TOKEN);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,24 +18,31 @@
|
||||||
|
|
||||||
package org.apache.cassandra.cql3;
|
package org.apache.cassandra.cql3;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.apache.cassandra.auth.IRoleManager;
|
||||||
import org.apache.cassandra.auth.RoleOptions;
|
import org.apache.cassandra.auth.RoleOptions;
|
||||||
|
|
||||||
import static org.apache.cassandra.cql3.PasswordObfuscator.*;
|
import static org.apache.cassandra.cql3.PasswordObfuscator.*;
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
public class PasswordObfuscatorTest
|
public class PasswordObfuscatorTest
|
||||||
{
|
{
|
||||||
private static final RoleOptions opts = new RoleOptions();
|
private static final RoleOptions opts = new RoleOptions();
|
||||||
private static final String optsPassword = "testpassword";
|
private static final String optsPassword = "testpassword";
|
||||||
|
public static final int ASCII_START = 32;
|
||||||
|
public static final int ASCII_END = 126;
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void startup()
|
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
|
@Test
|
||||||
|
|
@ -237,4 +244,121 @@ public class PasswordObfuscatorTest
|
||||||
"APPLY BATCH;", optsPassword, optsPassword),
|
"APPLY BATCH;", optsPassword, optsPassword),
|
||||||
opts));
|
opts));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue