Recently I had the need to change certain classes -from external dependencies- loaded on a Spring Boot application. All this happened in a very restrictive environment, where I was not allowed to use other libraries or tweak the JRE, it was only possible to modify the fat JAR and environment variables or system properties.
I managed to change the dependency behaviour taking advantage of the way the Java Class Loading System works. In my case, because the class was part of a dependency and not the JRE, I decided to just rely on the App Class Loader. Of course, this is not Spring specific, this is more "any JVM app" specific :-)
NOTE: For cases where the classes are part of the JRE, you could use the “-Xbootclasspath/a:path” option (Java < 9) or Jigsaw for recent versions.
The App Class Loader would pick any class available in your classpath before going to check your dependencies. For that reason, if you add a class with the same qualified name -that respects the original contract- you could “hack” the dependency behaviour.
For demoing the technique, I chose the Jasypt library and changed the BasicTextEncryptor with a custom implementation of the Caesar cipher algorithm.
The original BasicTextEncryptor used a PBEWithMD5AndDES algorithm and the “new” implementation looks like this:
package org.jasypt.util.text;
/**
* Overrides the original implementation with a Caesar Cipher Algorithm implementation
*/
public final class BasicTextEncryptor implements TextEncryptor {
private String password;
public void setPassword(final String password) {
this.password = password;
}
public void setPasswordCharArray(final char[] password) {
this.password = new String(password);
}
@Override
public String encrypt(final String message) {
return cipher(message, password.length());
}
@Override
public String decrypt(final String encryptedMessage) {
return cipher(encryptedMessage, 26 - password.length());
}
private String cipher(String message, int shift) {
StringBuffer result= new StringBuffer();
for (int i = 0; i < message.length(); i++) {
if (Character.isUpperCase(message.charAt(i))) {
int ch = ((int)message.charAt(i) + shift - 65) % 26 + 65;
result.append((char)ch);
} else {
int ch = ((int)message.charAt(i) + shift - 97) % 26 + 97;
result.append((char)ch);
}
}
return result.toString();
}
}
My tests prove the point, the application now uses my custom implementation:
public class JasyptTest {
private BasicTextEncryptor textEncryptor;
@BeforeEach
public void setUp() {
textEncryptor = new BasicTextEncryptor();
textEncryptor.setPasswordCharArray("123".toCharArray());
}
@Test
public void hackJasyptBasicEncryptor_shouldUseAppClassLoader() {
ClassLoader actualClassLoader = textEncryptor.getClass().getClassLoader();
assertTrue(actualClassLoader.toString().contains("AppClassLoader"));
}
@Test
public void hackJasyptBasicEncryptor_encrypt() {
String actual = textEncryptor.encrypt("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
assertEquals("DEFGHIJKLMNOPQRSTUVWXYZABC", actual);
}
@Test
public void hackJasyptBasicEncryptor_decrypt() {
String actual = textEncryptor.decrypt("DEFGHIJKLMNOPQRSTUVWXYZABC");
assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZ", actual);
}
}
The AppClassLoader loads the custom BasicTextEncryptor and as you can see the encrypt/decrypt methods now use the Caesar algorithm.
In a “normal” project you should not need to override the dependency or JRE classes behaviour, but in case you need to, I hope this technique would do the trick for you.
After our project I kept thinking about all the different possibilities enabled by tweaking the class loading process... Certainly, something to consider and with special care when wearing the security hat.