Tuesday, February 21, 2012

Log4j filter to mask Payment Card numbers (PCI DSS)

According to PCI DSS (Payment Card Industry Data Security Standard) your application must not store payment card numbers. This requirement includes database, files and logs. The following filter will allow you to mask card numbers in your logs on the fly, so even if you accidentally turned debug mode on for network communication, you can be confident that your data is PCI compliant.

Log4j allows you to configure PatternLayout that processes your log records. The idea is simple, out filter would match payment card numbers and replace them with masked values. Card number is usually a number of 15-19 digits.

I am going to use regular expression to match possible card numbers and replace them with masked values. I leave unmasked the beginning (6 digits) and the ending (4 digits), replacing the middle part with <HIDDEN> text. So, instead of 123456789012345678, I will get 123456<HIDDEN>5678 in my logs.

The following class implements PatternLayout with overriden format() method that does filtering:

package vozis.logger;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.spi.LoggingEvent;

/**
 * Credit Card Filtering Layout
 * @author sergej.sizov
 */
public class CreditCardFilteringLayout extends PatternLayout {
    
 private static final String MASKCARD = "$1<HIDDEN>$2";
 private static final Pattern PATTERNCARD = 
    Pattern.compile("([0-9]{6})[0-9]{0,9}([0-9]{4})");        
    
 @Override
 public String format(LoggingEvent event) {
   if (event.getMessage() instanceof String) {
      String message = event.getRenderedMessage();

      Matcher matcher = PATTERNCARD.matcher(message);
           
      if (matcher.find()) {
         String maskedMessage = matcher.replaceAll(MASKCARD);

         Throwable throwable = 
             event.getThrowableInformation() != null ?
             event.getThrowableInformation().getThrowable() : null;
                
         LoggingEvent maskedEvent = new LoggingEvent(
                 event.fqnOfCategoryClass,
                 Logger.getLogger(event.getLoggerName()), 
                 event.timeStamp, 
                 event.getLevel(), 
                 maskedMessage, 
                 throwable);
                
         return super.format(maskedEvent);
      } 
   }

   return super.format(event);

 }
}



Then we need to configure Log4j to use CreditCardFilteringLayout. You need to override layout property for every appender in log4j.properties as it is shown below:

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=vozis.logger.CreditCardFilteringLayout
log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss} %-5p %c{1} - %m%n
log4j.appender.stdout.Threshold=info

log4j.appender.TEMP=org.apache.log4j.RollingFileAppender
log4j.appender.TEMP.File=temp.log 
log4j.appender.TEMP.MaxFileSize=5MB
log4j.appender.TEMP.MaxBackupIndex=1
log4j.appender.TEMP.layout=vozis.logger.CreditCardFilteringLayout
log4j.appender.TEMP.layout.ConversionPattern=%-5p %d{yyyy-MM-dd HH:mm:ss,SSS} %C{1}:%M(line %L) - %m%n


This idea can be used not only for credit card numbers, but also for Social Security number (SSN) or any other data that you consider sensitive. The benefit of this solution is that it is a one place change and it is easier than checking every logger.log() invocation in your application.


3 comments:

  1. Thank you Sizov for posting the nice article. I was searching for the similar this actually..

    Appreciated your efforts..!!

    Regards
    Siv

    ReplyDelete
  2. Very clear & concise post. Spot on for what I need!
    Thanks!

    ReplyDelete