Introduction to Log Masking
Log masking is a critical security practice that prevents sensitive information from being exposed in application logs. This comprehensive guide covers techniques, patterns, and best practices for implementing effective log masking in Java applications.
Core Masking Strategies
1. Pattern-Based Masking
2. Annotation-Driven Masking
3. Custom Masking Providers
4. Log Framework Integrations
Basic Pattern-Based Masking
Simple Regex Masking Utility
import java.util.regex.*; import java.util.*; public class BasicLogMasker { private static final Map<Pattern, String> MASK_PATTERNS = new LinkedHashMap<>(); static { // Credit card patterns MASK_PATTERNS.put(Pattern.compile("\\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14})\\b"), "****-****-****-####"); MASK_PATTERNS.put(Pattern.compile("\\b3[47][0-9]{13}\\b"), "****-******-####"); // SSN patterns MASK_PATTERNS.put(Pattern.compile("\\b[0-9]{3}-[0-9]{2}-[0-9]{4}\\b"), "***-**-####"); // Email patterns MASK_PATTERNS.put(Pattern.compile("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b"), "***@***.***"); // Phone numbers MASK_PATTERNS.put(Pattern.compile("\\b\\(?[0-9]{3}\\)?[-. ]?[0-9]{3}[-. ]?[0-9]{4}\\b"), "(***) ***-####"); // API keys (common patterns) MASK_PATTERNS.put(Pattern.compile("\\bsk_[a-zA-Z0-9]{20,}\\b"), "sk_************"); MASK_PATTERNS.put(Pattern.compile("\\bAKIA[0-9A-Z]{16}\\b"), "AKIA************"); } public static String maskSensitiveData(String input) { if (input == null || input.trim().isEmpty()) { return input; } String masked = input; for (Map.Entry<Pattern, String> entry : MASK_PATTERNS.entrySet()) { Pattern pattern = entry.getKey(); String replacement = entry.getValue(); Matcher matcher = pattern.matcher(masked); masked = matcher.replaceAll(replacement); } return masked; } // Specific masking methods for common data types public static String maskCreditCard(String creditCard) { if (creditCard == null || creditCard.length() < 12) { return creditCard; } return "****-****-****-" + creditCard.substring(creditCard.length() - 4); } public static String maskSSN(String ssn) { if (ssn == null || ssn.length() < 9) { return ssn; } return "***-**-" + ssn.substring(ssn.length() - 4); } public static String maskEmail(String email) { if (email == null || !email.contains("@")) { return email; } int atIndex = email.indexOf("@"); String localPart = email.substring(0, atIndex); String domain = email.substring(atIndex + 1); String maskedLocal = localPart.length() <= 2 ? "*".repeat(localPart.length()) : localPart.charAt(0) + "*".repeat(localPart.length() - 2) + localPart.charAt(localPart.length() - 1); int dotIndex = domain.lastIndexOf("."); String domainName = domain.substring(0, dotIndex); String tld = domain.substring(dotIndex + 1); String maskedDomain = domainName.length() <= 2 ? "*".repeat(domainName.length()) : domainName.charAt(0) + "*".repeat(domainName.length() - 2) + domainName.charAt(domainName.length() - 1); return maskedLocal + "@" + maskedDomain + "." + tld; } public static void main(String[] args) { // Test the masker String testData = "User with SSN 123-45-6789, email [email protected], " + "and credit card 4111-1111-1111-1111 called phone 555-123-4567."; System.out.println("Original: " + testData); System.out.println("Masked: " + maskSensitiveData(testData)); // Test individual methods System.out.println("\nIndividual masking:"); System.out.println("Credit Card: " + maskCreditCard("4111111111111111")); System.out.println("SSN: " + maskSSN("123456789")); System.out.println("Email: " + maskEmail("[email protected]")); } } Advanced Annotation-Based Masking
Custom Annotations for Sensitive Data
import java.lang.annotation.*; import java.lang.reflect.*; import java.util.*; // Marker annotation for sensitive fields @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface SensitiveData { MaskType type() default MaskType.GENERIC; int visiblePrefix() default 0; int visibleSuffix() default 0; } // Masking strategies enum MaskType { CREDIT_CARD, SSN, EMAIL, PHONE, API_KEY, PASSWORD, GENERIC } // Sample entity class with sensitive data class User { private String username; @SensitiveData(type = MaskType.EMAIL) private String email; @SensitiveData(type = MaskType.SSN, visibleSuffix = 4) private String socialSecurityNumber; @SensitiveData(type = MaskType.CREDIT_CARD, visibleSuffix = 4) private String creditCardNumber; @SensitiveData(type = MaskType.PASSWORD) private String password; @SensitiveData(type = MaskType.PHONE, visiblePrefix = 3, visibleSuffix = 2) private String phoneNumber; // Constructors, getters, and setters public User(String username, String email, String ssn, String creditCard, String password, String phone) { this.username = username; this.email = email; this.socialSecurityNumber = ssn; this.creditCardNumber = creditCard; this.password = password; this.phoneNumber = phone; } // Getters and setters public String getUsername() { return username; } public String getEmail() { return email; } public String getSocialSecurityNumber() { return socialSecurityNumber; } public String getCreditCardNumber() { return creditCardNumber; } public String getPassword() { return password; } public String getPhoneNumber() { return phoneNumber; } } // Reflection-based masking utility public class AnnotationBasedMasker { public static String maskObject(Object obj) { if (obj == null) { return "null"; } StringBuilder result = new StringBuilder(); result.append(obj.getClass().getSimpleName()).append("{"); Field[] fields = obj.getClass().getDeclaredFields(); boolean firstField = true; for (Field field : fields) { if (!firstField) { result.append(", "); } firstField = false; field.setAccessible(true); result.append(field.getName()).append("="); try { Object value = field.get(obj); if (field.isAnnotationPresent(SensitiveData.class)) { SensitiveData annotation = field.getAnnotation(SensitiveData.class); String maskedValue = maskValue(value != null ? value.toString() : null, annotation); result.append(maskedValue); } else { result.append(value); } } catch (IllegalAccessException e) { result.append("[ACCESS_ERROR]"); } } result.append("}"); return result.toString(); } private static String maskValue(String value, SensitiveData annotation) { if (value == null) { return "null"; } switch (annotation.type()) { case CREDIT_CARD: return maskCreditCard(value, annotation.visibleSuffix()); case SSN: return maskSSN(value, annotation.visibleSuffix()); case EMAIL: return maskEmail(value); case PHONE: return maskPhone(value, annotation.visiblePrefix(), annotation.visibleSuffix()); case PASSWORD: return "********"; case API_KEY: return maskApiKey(value); case GENERIC: default: return maskGeneric(value, annotation.visiblePrefix(), annotation.visibleSuffix()); } } private static String maskCreditCard(String cardNumber, int visibleSuffix) { if (cardNumber.length() < visibleSuffix) { return "*".repeat(cardNumber.length()); } return "*".repeat(cardNumber.length() - visibleSuffix) + cardNumber.substring(cardNumber.length() - visibleSuffix); } private static String maskSSN(String ssn, int visibleSuffix) { if (ssn.length() < visibleSuffix) { return "*".repeat(ssn.length()); } return "*".repeat(ssn.length() - visibleSuffix) + ssn.substring(ssn.length() - visibleSuffix); } private static String maskEmail(String email) { if (!email.contains("@")) { return "*".repeat(email.length()); } String[] parts = email.split("@"); String localPart = parts[0]; String domain = parts[1]; String maskedLocal = localPart.length() <= 2 ? "*".repeat(localPart.length()) : localPart.charAt(0) + "*".repeat(Math.max(0, localPart.length() - 2)) + (localPart.length() > 1 ? localPart.charAt(localPart.length() - 1) : ""); return maskedLocal + "@" + maskDomain(domain); } private static String maskDomain(String domain) { if (!domain.contains(".")) { return "*".repeat(domain.length()); } int lastDot = domain.lastIndexOf("."); String domainName = domain.substring(0, lastDot); String tld = domain.substring(lastDot + 1); String maskedDomain = domainName.length() <= 2 ? "*".repeat(domainName.length()) : domainName.charAt(0) + "*".repeat(Math.max(0, domainName.length() - 2)) + (domainName.length() > 1 ? domainName.charAt(domainName.length() - 1) : ""); return maskedDomain + "." + tld; } private static String maskPhone(String phone, int visiblePrefix, int visibleSuffix) { // Remove non-digit characters for masking String digits = phone.replaceAll("\\D", ""); if (digits.length() < visiblePrefix + visibleSuffix) { return "*".repeat(phone.length()); } String prefix = digits.substring(0, visiblePrefix); String suffix = digits.substring(digits.length() - visibleSuffix); String middle = "*".repeat(digits.length() - visiblePrefix - visibleSuffix); return prefix + middle + suffix; } private static String maskApiKey(String apiKey) { if (apiKey.length() <= 8) { return "*".repeat(apiKey.length()); } return apiKey.substring(0, 4) + "*".repeat(apiKey.length() - 8) + apiKey.substring(apiKey.length() - 4); } private static String maskGeneric(String value, int visiblePrefix, int visibleSuffix) { if (value.length() < visiblePrefix + visibleSuffix) { return "*".repeat(value.length()); } String prefix = value.substring(0, Math.min(visiblePrefix, value.length())); String suffix = visibleSuffix > 0 ? value.substring(value.length() - Math.min(visibleSuffix, value.length())) : ""; String middle = "*".repeat(Math.max(0, value.length() - visiblePrefix - visibleSuffix)); return prefix + middle + suffix; } public static void main(String[] args) { User user = new User( "johndoe", "[email protected]", "123-45-6789", "4111-1111-1111-1111", "secret123", "+1-555-123-4567" ); System.out.println("Original user: " + user); System.out.println("Masked user: " + maskObject(user)); } } Logback Configuration with Masking
Custom Logback Layout with Masking
<!-- logback.xml --> <configuration> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> <layout class="com.example.MaskingPatternLayout"> <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern> </layout> </encoder> </appender> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/application.log</file> <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> <layout class="com.example.MaskingPatternLayout"> <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern> </layout> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> </appender> <root level="INFO"> <appender-ref ref="CONSOLE" /> <appender-ref ref="FILE" /> </root> </configuration> import ch.qos.logback.classic.PatternLayout; import ch.qos.logback.classic.spi.ILoggingEvent; import java.util.regex.Pattern; import java.util.regex.Matcher; public class MaskingPatternLayout extends PatternLayout { private static final Pattern[] MASK_PATTERNS = { Pattern.compile("\\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14})\\b"), // Credit cards Pattern.compile("\\b[0-9]{3}-[0-9]{2}-[0-9]{4}\\b"), // SSN Pattern.compile("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b"), // Email Pattern.compile("\\bpassword[=:]['\"]?([^,'\"\\s]+)['\"]?", Pattern.CASE_INSENSITIVE), Pattern.compile("\\bapi[_-]?key[=:]['\"]?([^,'\"\\s]+)['\"]?", Pattern.CASE_INSENSITIVE), Pattern.compile("\\bsecret[=:]['\"]?([^,'\"\\s]+)['\"]?", Pattern.CASE_INSENSITIVE), Pattern.compile("\\btoken[=:]['\"]?([^,'\"\\s]+)['\"]?", Pattern.CASE_INSENSITIVE) }; @Override public String doLayout(ILoggingEvent event) { String message = super.doLayout(event); return maskSensitiveData(message); } private String maskSensitiveData(String message) { if (message == null || message.isEmpty()) { return message; } String masked = message; for (Pattern pattern : MASK_PATTERNS) { Matcher matcher = pattern.matcher(masked); StringBuffer buffer = new StringBuffer(); while (matcher.find()) { String matched = matcher.group(); String replacement; if (pattern.pattern().toLowerCase().contains("credit") || pattern.pattern().contains("4[0-9]")) { replacement = maskCreditCard(matched); } else if (pattern.pattern().contains("[0-9]{3}-[0-9]{2}-[0-9]{4}")) { replacement = "***-**-" + matched.substring(matched.length() - 4); } else if (pattern.pattern().contains("@")) { replacement = maskEmail(matched); } else { // For key=value patterns like password=secret if (matcher.groupCount() > 0 && matcher.group(1) != null) { String value = matcher.group(1); replacement = matcher.group().replace(value, "********"); } else { replacement = "********"; } } matcher.appendReplacement(buffer, Matcher.quoteReplacement(replacement)); } matcher.appendTail(buffer); masked = buffer.toString(); } return masked; } private String maskCreditCard(String cardNumber) { String digits = cardNumber.replaceAll("\\D", ""); if (digits.length() >= 4) { return "****-****-****-" + digits.substring(digits.length() - 4); } return "****"; } private String maskEmail(String email) { int atIndex = email.indexOf("@"); if (atIndex > 0) { String localPart = email.substring(0, atIndex); String domain = email.substring(atIndex); if (localPart.length() <= 2) { return "*".repeat(localPart.length()) + domain; } else { return localPart.charAt(0) + "*".repeat(localPart.length() - 2) + localPart.charAt(localPart.length() - 1) + domain; } } return email; } } Log4j2 Custom Plugin for Masking
Log4j2 Masking Plugin
import org.apache.logging.log4j.core.*; import org.apache.logging.log4j.core.config.plugins.*; import org.apache.logging.log4j.core.layout.*; import org.apache.logging.log4j.core.pattern.*; import java.util.regex.*; @Plugin(name = "MaskingPatternLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) public class MaskingPatternLayout extends PatternLayout { private final List<Pattern> maskPatterns; protected MaskingPatternLayout(String pattern, Configuration config, RegexReplacement replace, Charset charset, boolean alwaysWriteExceptions, boolean noConsoleNoAnsi) { super(config, replace, pattern, charset, alwaysWriteExceptions, noConsoleNoAnsi); this.maskPatterns = initializeMaskPatterns(); } @PluginFactory public static MaskingPatternLayout createLayout( @PluginAttribute(value = "pattern", defaultString = PatternLayout.DEFAULT_CONVERSION_PATTERN) String pattern, @PluginConfiguration Configuration config, @PluginElement("Replace") RegexReplacement replace, @PluginAttribute(value = "charset", defaultString = "UTF-8") Charset charset, @PluginAttribute(value = "alwaysWriteExceptions", defaultBoolean = true) boolean alwaysWriteExceptions, @PluginAttribute(value = "noConsoleNoAnsi", defaultBoolean = false) boolean noConsoleNoAnsi) { return new MaskingPatternLayout(pattern, config, replace, charset, alwaysWriteExceptions, noConsoleNoAnsi); } private List<Pattern> initializeMaskPatterns() { List<Pattern> patterns = new ArrayList<>(); // Credit cards patterns.add(Pattern.compile("\\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14})\\b")); // SSN patterns.add(Pattern.compile("\\b[0-9]{3}-[0-9]{2}-[0-9]{4}\\b")); // Email patterns.add(Pattern.compile("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b")); // Sensitive key-value pairs patterns.add(Pattern.compile("(?i)(password|pwd|pass)=['\"]?([^,'\"\\s]+)['\"]?")); patterns.add(Pattern.compile("(?i)(api[_-]?key|apikey)=['\"]?([^,'\"\\s]+)['\"]?")); patterns.add(Pattern.compile("(?i)(secret|token)=['\"]?([^,'\"\\s]+)['\"]?")); patterns.add(Pattern.compile("(?i)(authorization|auth):\\s*(bearer\\s+[^\\s]+)", Pattern.CASE_INSENSITIVE)); return patterns; } @Override public String toSerializable(LogEvent event) { String original = super.toSerializable(event); return maskSensitiveData(original); } private String maskSensitiveData(String message) { if (message == null || message.isEmpty()) { return message; } String masked = message; for (Pattern pattern : maskPatterns) { Matcher matcher = pattern.matcher(masked); StringBuffer buffer = new StringBuffer(); while (matcher.find()) { String replacement = getReplacement(matcher); matcher.appendReplacement(buffer, Matcher.quoteReplacement(replacement)); } matcher.appendTail(buffer); masked = buffer.toString(); } return masked; } private String getReplacement(Matcher matcher) { String matched = matcher.group(); // Handle key-value pairs if (matcher.pattern().pattern().toLowerCase().contains("password") || matcher.pattern().pattern().toLowerCase().contains("api") || matcher.pattern().pattern().toLowerCase().contains("secret") || matcher.pattern().pattern().toLowerCase().contains("token")) { if (matcher.groupCount() >= 2 && matcher.group(2) != null) { String key = matcher.group(1); String value = matcher.group(2); return key + "=********"; } } // Handle authorization headers if (matcher.pattern().pattern().toLowerCase().contains("authorization")) { if (matched.toLowerCase().startsWith("bearer")) { return "Bearer ********"; } return "********"; } // Handle specific data types if (matcher.pattern().pattern().contains("4[0-9]")) { return maskCreditCard(matched); } else if (matcher.pattern().pattern().contains("[0-9]{3}-[0-9]{2}-[0-9]{4}")) { return "***-**-" + (matched.length() >= 4 ? matched.substring(matched.length() - 4) : "****"); } else if (matcher.pattern().pattern().contains("@")) { return maskEmail(matched); } return "********"; } private String maskCreditCard(String cardNumber) { String digits = cardNumber.replaceAll("\\D", ""); if (digits.length() >= 4) { return "****-****-****-" + digits.substring(digits.length() - 4); } return "****"; } private String maskEmail(String email) { int atIndex = email.indexOf("@"); if (atIndex > 0) { String localPart = email.substring(0, atIndex); String domain = email.substring(atIndex); if (localPart.length() <= 2) { return "*".repeat(localPart.length()) + domain; } else { return localPart.charAt(0) + "*".repeat(Math.max(0, localPart.length() - 2)) + (localPart.length() > 1 ? localPart.charAt(localPart.length() - 1) : "") + domain; } } return email; } } <!-- log4j2.xml --> <?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN" packages="com.example.logging"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <MaskingPatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </Console> <File name="File" fileName="logs/app.log"> <MaskingPatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </File> </Appenders> <Loggers> <Root level="info"> <AppenderRef ref="Console"/> <AppenderRef ref="File"/> </Root> </Loggers> </Configuration> Spring Boot Integration
Spring Boot Configuration with Log Masking
import org.springframework.context.annotation.*; import org.springframework.boot.context.properties.*; import java.util.*; @Configuration @ConfigurationProperties(prefix = "logging.masking") public class LogMaskingConfig { private List<String> patterns = new ArrayList<>(); private boolean enabled = true; private MaskingStrategy strategy = MaskingStrategy.PARTIAL; public enum MaskingStrategy { FULL, // Replace entire value with **** PARTIAL, // Show partial information (first/last chars) HASH // Replace with hash of the value } // Getters and setters public List<String> getPatterns() { return patterns; } public void setPatterns(List<String> patterns) { this.patterns = patterns; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public MaskingStrategy getStrategy() { return strategy; } public void setStrategy(MaskingStrategy strategy) { this.strategy = strategy; } @Bean public LogMaskingService logMaskingService() { return new LogMaskingService(this); } } @Service public class LogMaskingService { private final LogMaskingConfig config; private final List<Pattern> compiledPatterns; public LogMaskingService(LogMaskingConfig config) { this.config = config; this.compiledPatterns = compilePatterns(); } private List<Pattern> compilePatterns() { List<Pattern> patterns = new ArrayList<>(); // Add default patterns patterns.add(Pattern.compile("\\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14})\\b")); patterns.add(Pattern.compile("\\b[0-9]{3}-[0-9]{2}-[0-9]{4}\\b")); patterns.add(Pattern.compile("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b")); // Add custom patterns from configuration for (String patternStr : config.getPatterns()) { try { patterns.add(Pattern.compile(patternStr)); } catch (Exception e) { // Log warning about invalid pattern System.err.println("Invalid regex pattern: " + patternStr); } } return patterns; } public String maskMessage(String message) { if (!config.isEnabled() || message == null) { return message; } String masked = message; for (Pattern pattern : compiledPatterns) { Matcher matcher = pattern.matcher(masked); StringBuffer buffer = new StringBuffer(); while (matcher.find()) { String replacement = getMaskedReplacement(matcher.group()); matcher.appendReplacement(buffer, Matcher.quoteReplacement(replacement)); } matcher.appendTail(buffer); masked = buffer.toString(); } return masked; } private String getMaskedReplacement(String value) { switch (config.getStrategy()) { case FULL: return "*".repeat(Math.min(value.length(), 8)); case HASH: return "hash_" + Integer.toHexString(value.hashCode()); case PARTIAL: default: return maskPartial(value); } } private String maskPartial(String value) { if (value.length() <= 2) { return "*".repeat(value.length()); } // For emails if (value.contains("@")) { return maskEmail(value); } // For credit cards, SSN, etc. if (value.matches(".*\\d.*")) { if (value.replaceAll("\\D", "").length() >= 4) { return "*".repeat(value.length() - 4) + value.substring(value.length() - 4); } } // Generic masking int visibleChars = Math.max(1, value.length() / 4); return value.substring(0, visibleChars) + "*".repeat(value.length() - visibleChars * 2) + value.substring(value.length() - visibleChars); } private String maskEmail(String email) { int atIndex = email.indexOf("@"); if (atIndex > 0) { String localPart = email.substring(0, atIndex); String domain = email.substring(atIndex); String maskedLocal = localPart.length() <= 2 ? "*".repeat(localPart.length()) : localPart.charAt(0) + "*".repeat(localPart.length() - 2) + localPart.charAt(localPart.length() - 1); return maskedLocal + domain; } return email; } } // Aspect for automatic method argument masking @Aspect @Component public class LoggingAspect { private final LogMaskingService maskingService; private final Logger logger = LoggerFactory.getLogger(LoggingAspect.class); public LoggingAspect(LogMaskingService maskingService) { this.maskingService = maskingService; } @Around("execution(* com.example.service.*.*(..))") public Object logMethodCall(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().getName(); String className = joinPoint.getTarget().getClass().getSimpleName(); // Mask arguments before logging Object[] maskedArgs = maskArguments(joinPoint.getArgs()); String maskedArguments = Arrays.toString(maskedArgs); logger.info("Entering {}.{} with arguments: {}", className, methodName, maskedArguments); try { Object result = joinPoint.proceed(); // Mask result before logging if needed String maskedResult = maskResult(result); logger.info("Exiting {}.{} with result: {}", className, methodName, maskedResult); return result; } catch (Exception e) { logger.error("Exception in {}.{}: {}", className, methodName, e.getMessage()); throw e; } } private Object[] maskArguments(Object[] args) { if (args == null) { return new Object[0]; } Object[] masked = new Object[args.length]; for (int i = 0; i < args.length; i++) { masked[i] = maskObject(args[i]); } return masked; } private Object maskObject(Object obj) { if (obj == null) { return null; } // If it's a string, apply masking if (obj instanceof String) { return maskingService.maskMessage((String) obj); } // If it's a custom object with sensitive data, use reflection if (isSensitiveObject(obj)) { return maskSensitiveObject(obj); } return obj; } private String maskResult(Object result) { if (result == null) { return "null"; } String resultStr = result.toString(); return maskingService.maskMessage(resultStr); } private boolean isSensitiveObject(Object obj) { // Check if object has fields with @SensitiveData annotation return Arrays.stream(obj.getClass().getDeclaredFields()) .anyMatch(field -> field.isAnnotationPresent(SensitiveData.class)); } private Object maskSensitiveObject(Object obj) { // Use reflection to create a masked version of the object // This would use the AnnotationBasedMasker from earlier return AnnotationBasedMasker.maskObject(obj); } } Application Properties Configuration
# application.yml logging: masking: enabled: true strategy: PARTIAL patterns: - "\\b[0-9]{16}\\b" # Additional 16-digit numbers - "\\b[A-Z]{2}[0-9]{6,}\\b" # License plates, etc. level: com.example: INFO org.springframework: WARN # Logback configuration (if using logback) logging: config: classpath:logback-masking.xml Testing the Masking Implementation
Unit Tests for Masking
import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; public class LogMaskingTest { private BasicLogMasker masker; private AnnotationBasedMasker annotationMasker; @BeforeEach void setUp() { masker = new BasicLogMasker(); annotationMasker = new AnnotationBasedMasker(); } @Test void testCreditCardMasking() { String input = "Card: 4111-1111-1111-1111"; String masked = masker.maskSensitiveData(input); assertFalse(masked.contains("4111-1111-1111-1111")); assertTrue(masked.contains("****-****-****-1111")); } @Test void testSSNMasking() { String input = "SSN: 123-45-6789"; String masked = masker.maskSensitiveData(input); assertFalse(masked.contains("123-45-6789")); assertTrue(masked.contains("***-**-6789")); } @Test void testEmailMasking() { String input = "Email: [email protected]"; String masked = masker.maskSensitiveData(input); assertFalse(masked.contains("[email protected]")); assertTrue(masked.contains("j****[email protected]")); } @Test void testAnnotationBasedMasking() { User user = new User( "johndoe", "[email protected]", "123-45-6789", "4111-1111-1111-1111", "secret123", "+1-555-123-4567" ); String masked = annotationMasker.maskObject(user); assertFalse(masked.contains("[email protected]")); assertFalse(masked.contains("123-45-6789")); assertFalse(masked.contains("4111-1111-1111-1111")); assertFalse(masked.contains("secret123")); assertTrue(masked.contains("j****e@e****e.com")); assertTrue(masked.contains("***-**-6789")); assertTrue(masked.contains("****-****-****-1111")); assertTrue(masked.contains("********")); // password } @Test void testLogMaskingService() { LogMaskingConfig config = new LogMaskingConfig(); config.setEnabled(true); config.setStrategy(LogMaskingConfig.MaskingStrategy.PARTIAL); LogMaskingService service = new LogMaskingService(config); String message = "User login: [email protected], card=4111111111111111"; String masked = service.maskMessage(message); assertFalse(masked.contains("[email protected]")); assertFalse(masked.contains("4111111111111111")); } @Test void testPerformance() { String largeMessage = "Large log message with multiple sensitive data: " + "[email protected], ssn=123-45-6789, " + "card=4111-1111-1111-1111, phone=555-123-4567. " + "Repeated multiple times for performance testing."; StringBuilder builder = new StringBuilder(); for (int i = 0; i < 1000; i++) { builder.append(largeMessage); } String largeInput = builder.toString(); long startTime = System.currentTimeMillis(); String masked = masker.maskSensitiveData(largeInput); long endTime = System.currentTimeMillis(); System.out.println("Masking performance: " + (endTime - startTime) + "ms"); // Ensure no sensitive data leaked assertFalse(masked.contains("[email protected]")); assertFalse(masked.contains("123-45-6789")); assertFalse(masked.contains("4111-1111-1111-1111")); } } Best Practices and Recommendations
1. Comprehensive Pattern Coverage
public class ComprehensiveMaskingPatterns { public static final List<Pattern> SENSITIVE_DATA_PATTERNS = Arrays.asList( // Financial information Pattern.compile("\\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|6(?:011|5[0-9]{2})[0-9]{12}|(?:2131|1800|35\\d{3})\\d{11})\\b"), // Social security numbers Pattern.compile("\\b[0-9]{3}-[0-9]{2}-[0-9]{4}\\b"), // Email addresses Pattern.compile("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b"), // Phone numbers (international formats) Pattern.compile("\\b(?:\\+?1[-. ]?)?\\(?[0-9]{3}\\)?[-. ]?[0-9]{3}[-. ]?[0-9]{4}\\b"), // API keys and tokens Pattern.compile("\\b(?:sk|pk)_[a-zA-Z0-9]{20,}\\b"), // Stripe-like Pattern.compile("\\bAKIA[0-9A-Z]{16}\\b"), // AWS Pattern.compile("\\bgh[pousr]_[A-Za-z0-9]{36}\\b"), // GitHub Pattern.compile("\\b[a-zA-Z0-9]{40}\\b"), // Generic 40-char tokens // Authorization headers Pattern.compile("(?i)authorization:\\s*(bearer|basic)\\s+[^\\s]+"), // Password in key-value pairs Pattern.compile("(?i)(password|pwd|pass|passphrase)[=:]['\"]?([^,'\"\\s]+)['\"]?"), // Secret keys Pattern.compile("(?i)(secret|secretkey|privatekey|apisecret)[=:]['\"]?([^,'\"\\s]+)['\"]?"), // Database connection strings Pattern.compile("(jdbc|mongodb|redis|postgresql)://[^:]+:[^@]+@[^\\s]+"), // JWT tokens Pattern.compile("\\beyJ[\\w-]+\\.[\\w-]+\\.[\\w-]+\\b"), // IP addresses (optional - based on requirement) Pattern.compile("\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b"), // MAC addresses Pattern.compile("\\b(?:[0-9A-Fa-f]{2}[:-]){5}(?:[0-9A-Fa-f]{2})\\b"), // Vehicle identification numbers Pattern.compile("\\b[A-HJ-NPR-Z0-9]{17}\\b") ); } 2. Layered Defense Strategy
public class LayeredLogMasking { // Layer 1: Pre-logging validation public void logSafely(String message, Object... args) { if (containsSensitiveData(message)) { throw new SecurityException("Attempt to log sensitive data: " + maskForValidation(message)); } // Apply masking before logging String maskedMessage = maskMessage(message); Object[] maskedArgs = maskArguments(args); logger.info(maskedMessage, maskedArgs); } // Layer 2: Runtime monitoring @Component public class LogMonitoringAspect { private final Set<String> sensitiveLogs = Collections.synchronizedSet(new HashSet<>()); @AfterReturning("execution(* org.slf4j.Logger.*(..))") public void monitorLogs(JoinPoint joinPoint) { Object[] args = joinPoint.getArgs(); if (args.length > 0 && args[0] instanceof String) { String message = (String) args[0]; if (containsUnmaskedSensitiveData(message)) { sensitiveLogs.add(message); alertSecurityTeam(message); } } } private boolean containsUnmaskedSensitiveData(String message) { // Check if message contains unmasked sensitive patterns return ComprehensiveMaskingPatterns.SENSITIVE_DATA_PATTERNS.stream() .anyMatch(pattern -> pattern.matcher(message).find()); } private void alertSecurityTeam(String message) { // Send alert to security team System.err.println("SECURITY ALERT: Unmasked sensitive data in logs: " + maskForValidation(message)); } } // Layer 3: Post-logging scanning @Component public class LogFileScanner { @Scheduled(fixedRate = 300000) // Scan every 5 minutes public void scanLogFiles() { scanLogFileForSensitiveData("logs/application.log"); scanLogFileForSensitiveData("logs/application.json"); } private void scanLogFileForSensitiveData(String filePath) { try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { String line; int lineNumber = 0; while ((line = reader.readLine()) != null) { lineNumber++; if (containsUnmaskedSensitiveData(line)) { handleSensitiveDataFound(filePath, lineNumber, line); } } } catch (IOException e) { System.err.println("Error scanning log file: " + e.getMessage()); } } private void handleSensitiveDataFound(String filePath, int lineNumber, String line) { // Log the incident System.err.printf("Sensitive data found in %s at line %d: %s%n", filePath, lineNumber, maskForValidation(line)); // Optionally: truncate or sanitize the log file // This should be done carefully in production } } } Conclusion
Implementing comprehensive log masking in Java applications is crucial for security and compliance. Key takeaways:
- Use Multiple Layers: Combine pattern-based, annotation-based, and framework-level masking
- Customize for Your Needs: Adapt masking patterns based on your specific sensitive data types
- Test Thoroughly: Ensure masking works correctly and doesn't impact performance
- Monitor Continuously: Implement monitoring to detect unmasked sensitive data
- Follow Compliance Requirements: Ensure your masking strategy meets regulatory requirements (GDPR, HIPAA, PCI-DSS)
By implementing these strategies, you can significantly reduce the risk of sensitive data exposure through application logs while maintaining useful logging for debugging and monitoring purposes.