Masking log output in Logback

It is important to make sure that sensitive information does not get logged by your application. Logging is an important tool for troubleshooting your app, however, it can be easy to accidentally log passwords, emails, or other sensitive information. For such scenarios, I created Logmasker, an open-source library that can be used with both Logback and Log4j2 for masking all sorts of sensitive information. Please look at the dedicated page for LogMasker – Log4j and Logback Masking Library.

However, because of the success of my Log4J Masking article, I decided to create a similar one for Logback. So, in this article, I will be showing how to easily mask sensitive information using Logback. I strongly recommend you use my dedicated library, but if you just want to know how to create a custom MessageConverter in Logback, read on.

Importing Logback

First, we need to import Logback in our Java project. This can be done with easy by including the following lines of code in your build.gradle file

dependencies {
    implementation 'ch.qos.logback:logback-core:1.4.7'
    implementation 'ch.qos.logback:logback-classic:1.4.7'
    implementation 'org.slf4j:slf4j-api:2.0.7'
}

Now we can simply declare a logger in our Java files and use them. For simplicity, I will only have one Main.java file where I will be logging two messages, with one of them containing a password.

public class Main {
    private static final Logger logger = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args) {
        logger.info("This is a normal message");
        logger.info("This will contain the password: testpass123");
    }
}

The last thing we need to do is include a logback.xml file in our classpath. If you are using IntelliJ, you just need to add it to your resources folder.

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

When you run the application, you should see two messages printed in the console:

A Logback Masking Converter class

Now, we want to replace the password with ****** so that we don’t have it printed in the logs. To do this, we must intercept the messages that are being logged and replace the password with our text. Let’s create a simple message converter that just searches for testpass123 and replaces it. This is done by creating a Java class that extends ch.qos.logback.classic.pattern.MessageConverter. Now, we need to override the convert() method and do the “magic”. Here is a simple example:

public class LogbackMaskingConverter extends MessageConverter {

    public static final String PASSWORD = "testpass123";

    @Override
    public String convert(ILoggingEvent event) {
        StringBuilder messageBuffer = new StringBuilder(event.getFormattedMessage());
        int startOfPassword = messageBuffer.indexOf(PASSWORD);
        if (startOfPassword >= 0) {
            messageBuffer.replace(startOfPassword, startOfPassword + PASSWORD.length(), "******");
        }
        return messageBuffer.toString();
    }

}

The last thing we need to do is include this message converter in our logback.xml file. Otherwise, Logback won’t know about this converter and won’t be able to use it. So, we need to import it and assign a pattern. I chose %mask.

<configuration>
    <conversionRule conversionWord="mask" converterClass="tech.petrepopescu.LogbackMaskingConverter" />

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

Masking emails in Logback

We created a Masking message converter, but it is quite rudimentary. It can only mask one word. We need to make it better so that we can mask emails and other passwords. Let’s start with the email. First, let’s create an interface named LogMasker that has one method which receives StringBuilder. All markers will alter this builder and mask any sensitive data. I will be using RegExUtils from Apache Commons lang3

We still need SOME information, so we won’t be masking the entire email address. The first and last letters of the address and domain name will remain (since it makes it easier to analyze the log) as well as full domain. As an example, the email test.email@domain.com will be shown as t********l@d****n.com. Here I would like to give special thanks to Wiktor Stribiżew for his excellent answer on how to mask the email. You can find more masking patterns in his answer on StackOverflow.

public class EmailMasker implements LogMasker {
    private final Pattern emailFindPattern = Pattern.compile("([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)");
    private final Pattern emailMaskPattern = Pattern.compile("(?<=.)[^@](?=[^@]*?[^@]@)|(?:(?<=@.)|(?!^)\G(?=[^@]*$)).(?=.*[^@]\.)");

    public void mask(StringBuilder stringBuilder) {
        Matcher matcher =  emailFindPattern.matcher(stringBuilder);
        if (matcher.find()) {
            String email = matcher.group(1);
            String masked = RegExUtils.replaceAll(email, emailMaskPattern, "*");
            int idx = stringBuilder.indexOf(email);
            stringBuilder.replace(idx, idx + email.length(), masked);
        }
    }
}

Masking passwords in Logback

Using a similar approach, we need to mask any passwords. The code is slightly different here for two reasons. First, we have multiple keywords we need to search for (password or pwd for example). Secondly, we need to always mask with the exact same string so that we don’t reveal how many characters the password has. Still, the fundamentals are the same.

public class PasswordMasker implements LogMasker {
    private final Pattern passwordFindPattern = Pattern.compile("(?i)((?:password|pwd)(?::|=)(?:\s*)[A-Za-z0-9@$!%*?&.;<>]+)");

    @Override
    public void mask(StringBuilder stringBuilder) {
        Matcher matcher =  passwordFindPattern.matcher(stringBuilder);
        if (matcher.find()) {
            String password = matcher.group(1);
            int idx = stringBuilder.indexOf(password);
            stringBuilder.replace(idx, idx + password.length(), "password: ******");
        }
    }
}

Tieing everything together

Now, let’s enhance our message converter so that we use the newly-created makers. We simply create a set of makers and iterate over them. Each will update the StringBuilder and at the end we will have a sanitized message.

public class LogbackMaskingConverter extends MessageConverter {
    private static final Set<LogMasker> MASKERS = Set.of(new EmailMasker(), new PasswordMasker());
    
    @Override
    public String convert(ILoggingEvent event) {
        StringBuilder messageBuffer = new StringBuilder(event.getFormattedMessage());
        for (LogMasker masker: MASKERS) {
            masker.mask(messageBuffer);
        }
        return messageBuffer.toString();
    }
}

Now, let’s add more log messages in our Main file

public class Main {
    private static final Logger logger = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args) {
        logger.info("This is a normal message");
        logger.info("This will contain the password: testpass123");
        logger.info("This will contain an email testmail@email.com and will be masked");
        logger.info("The password: pass12345 will be masked");
    }
}

Other Considerations

First, keep in mind that this will add more processing for the log. The more LogMaskers you have, the more it will take to process each logged line. I tried to make things quite fast by using as few objects as possible and by using mutable objects wherever possible. Also, I used StringUtils and RegExUtils from Apache Commons Lang, since it is faster than the standard Java implementation.

Lastly, keep in mind that these are only examples. Maybe the regular expressions used could be improved and more maskers can be added based on your needs. As an example, an IP masker may be useful for some. Feel free to add one.

Or even better, us my open-source masking library that has more markers and far better performance.


Source link