28

Our web app needs to be made PCI compliant, i.e. it must not store any credit card numbers. The app is a frontend to a mainframe system which handles the CC numbers internally and - as we have just found out - occasionally still spits out a full CC number on one of its response screens. By default, the whole content of these responses are logged at debug level, and also the content parsed from these can be logged in lots of different places. So I can't hunt down the source of such data leaks. I must make sure that CC numbers are masked in our log files.

The regex part is not an issue, I will reuse the regex we already use in several other places. However I just can't find any good source on how to alter a part of a log message with Log4J. Filters seem to be much more limited, only able to decide whether to log a particular event or not, but can't alter the content of the message. I also found the ESAPI security wrapper API for Log4J which at first sight promises to do what I want. However, apparently I would need to replace all the loggers in the code with the ESAPI logger class - a pain in the butt. I would prefer a more transparent solution.

Any idea how to mask out credit card numbers from Log4J output?

Update: Based on @pgras's original idea, here is a working solution:

public class CardNumberFilteringLayout extends PatternLayout {
    private static final String MASK = "$1++++++++++++";
    private static final Pattern PATTERN = Pattern.compile("([0-9]{4})([0-9]{9,15})");

    @Override
    public String format(LoggingEvent event) {
        if (event.getMessage() instanceof String) {
            String message = event.getRenderedMessage();
            Matcher matcher = PATTERN.matcher(message);

            if (matcher.find()) {
                String maskedMessage = matcher.replaceAll(MASK);
                @SuppressWarnings({ "ThrowableResultOfMethodCallIgnored" })
                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);
    }
}

Notes:

  • I mask with + rather than *, because I want to tell apart cases when the CID was masked by this logger, from cases when it was done by the backend server, or whoever else
  • I use a simplistic regex because I am not worried about false positives

The code is unit tested so I am fairly convinced it works properly. Of course, if you spot any possibility to improve it, please let me know :-)

Reddy
  • 8,737
  • 11
  • 55
  • 73
Péter Török
  • 114,404
  • 31
  • 268
  • 329
  • 2
    Not sure why you don't let the super parse the event to string first and then filter it. You would spare yourself a lot of coding. – Boris Hamanov Feb 10 '11 at 15:09
  • @avok00, because LoggingEvent is immutable. – Péter Török Aug 31 '11 at 10:29
  • PCI DSS 3.0 Allow to show the BIN and the Last four at maximum. When you have large traffic is better to mask using those maximum allowed parameters. This can be done by changing this two lines. `private static final String MASK = "$1++++++$3";` `private static final Pattern PATTERN = Pattern.compile("([0-9]{6})([0-9]{6,10})([0-9]{4})");` – Neoecos Oct 22 '14 at 17:26

2 Answers2

16

You could write your own layout and configure it for all appenders...

Layout has a format method which makes a String from a loggingEvent that contains the logging message...

pgras
  • 12,614
  • 4
  • 38
  • 46
  • You could make your new layout a composite of the existing one you are using: Delegate anything that doesn't match your regular expression to it, and then remove or star out the line containing the credit card number – James B Mar 17 '10 at 11:44
  • Thanks, this seems to be viable. Although I would prefer subclassing over aggregation in this case. Let's experiment with it a bit... – Péter Török Mar 17 '10 at 13:55
5

A better implementation of credit card number masking is at http://adamcaudill.com/2011/10/20/masking-credit-cards-for-pci/ . You want to log the issuer and the checksum, but not the PAN (Primary Account Number).

Concrete Gannet
  • 551
  • 4
  • 11
  • 2
    Well, better is relative :-) In this case I don't want to log the issuer neither the checksum. I am not really interested in the concrete card number at all - we don't need to search for them in the logs or anything. That's why I don't care about false positives either. The only point is that no full card number shall get into the logs ever. But I agree that in other cases, Adam's solution may be better. – Péter Török Mar 23 '12 at 08:47