Secret Tecniques To Improve Dependency Resolution On App Class Loader

Written by camposer | Published 2020/11/20
Tech Story Tags: spring-boot | java | java-classloaders | hacks | spring | backend | software-development | programming | web-monetization

TLDR Rodolfo Campos has developed a way to tweak the Java Class Loading System. 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. The original BasicTextEncryptor used a PBEWithMD5AndDES algorithm and the ‘new’ implementation looks like this:.* Overrides the original implementation with a Caesar Cipher Algorithm.via the TL;DR App

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.

Written by camposer | Family man and tech geek - passionate about Software & Data Engineering, and Entrepreneurship
Published by HackerNoon on 2020/11/20