001/* 002 * Logback: the reliable, generic, fast and flexible logging framework. 003 * Copyright (C) 1999-2026, QOS.ch. All rights reserved. 004 * 005 * This program and the accompanying materials are dual-licensed under 006 * either the terms of the Eclipse Public License v2.0 as published by 007 * the Eclipse Foundation 008 * 009 * or (per the licensee's choosing) 010 * 011 * under the terms of the GNU Lesser General Public License version 2.1 012 * as published by the Free Software Foundation. 013 */ 014package ch.qos.logback.classic.sift; 015 016import ch.qos.logback.classic.spi.ILoggingEvent; 017import ch.qos.logback.core.sift.AbstractDiscriminator; 018import ch.qos.logback.core.util.BatchedFixedIntervalInvocationGate; 019import ch.qos.logback.core.util.Duration; 020import ch.qos.logback.core.util.OptionHelper; 021 022import java.util.Map; 023 024/** 025 * MDCBasedDiscriminator essentially returns the value mapped to an MDC key. If 026 * the said value is null, then a default value is returned. 027 * <p/> 028 * <p> 029 * Both Key and the DefaultValue are user specified properties. 030 * </p> 031 * <p> 032 * MDC values containing any of the characters {@code /}, {@code \}, {@code $}, 033 * <code>{</code>, <code>}</code>, <code>[</code>, <code>]</code>, <code>(</code>, 034 * <code>)</code> , <code>|</code>, <code>?</code>, <code>*</code>, <code>+</code>, 035 * <code>%</code>, <code>,</code>, <code>@</code> or the sequence {@code ..} are rejected, 036 * in which case the default value is returned. This prevents path separators, relative path components, 037 * variable substitutions such as <code>${file.separator}</code>, regex 038 * special characters as well as the anchor character '%' or the ',' and '@' characters from 039 * leaking into file names or email addresses. 040 * </p> 041 * 042 * <p>Moreover, the maximum length of an MDC value is limited to 64 characters.</p> 043 * 044 * @author Ceki Gülcü 045 */ 046public class MDCBasedDiscriminator extends AbstractDiscriminator<ILoggingEvent> { 047 048 static final String FORBIDDEN_CHARACTERS = "/\\${}[]()|?*+%@,"; 049 private static final int MAX_MDC_VALUE = 64; 050 051 private static final char DOT = '.'; 052 053 static final String EMPTY_VALUE_WARNING = "MDC value [%s] is empty, using default value [%s] instead"; 054 static final String REJECTED_VALUE_WARNING = "MDC value [%s] contains forbidden characters, using default value [%s] instead"; 055 static final String REJECTED_VALUE_LENGTH_WARNING = "MDC value of length %s is longer than the maximum allowed length of " + MAX_MDC_VALUE + " characters, using default value [%s] instead"; 056 057 058 private String key; 059 private String defaultValue; 060 /** 061 * Limits how often rejected-value warnings are emitted on the hot path. 062 */ 063 private final BatchedFixedIntervalInvocationGate invocationGate = 064 new BatchedFixedIntervalInvocationGate(4, Duration.buildByMinutes(10)); 065 066 @Override 067 public void start() { 068 int errors = 0; 069 if (OptionHelper.isNullOrEmptyOrAllSpaces(key)) { 070 errors++; 071 addError("The \"Key\" property must be set"); 072 } 073 if (OptionHelper.isNullOrEmptyOrAllSpaces(defaultValue)) { 074 errors++; 075 addError("The \"DefaultValue\" property must be set"); 076 } 077 if (errors == 0) { 078 started = true; 079 } 080 } 081 082 /** 083 * Return the value associated with an MDC entry designated by the Key property. 084 * If that value is null, then return the value assigned to the DefaultValue 085 * property. 086 * <p> 087 * If the MDC value contains any of the characters {@code /}, {@code \}, 088 * {@code $}, <code>{</code> or <code>}</code>, or the sequence {@code ..}, then 089 * the value assigned to the DefaultValue property is returned. 090 * </p> 091 */ 092 public String getDiscriminatingValue(ILoggingEvent event) { 093 // http://jira.qos.ch/browse/LBCLASSIC-213 094 Map<String, String> mdcMap = event.getMDCPropertyMap(); 095 if (mdcMap == null) { 096 return defaultValue; 097 } 098 String mdcValue = mdcMap.get(key); 099 if (mdcValue == null) { 100 return defaultValue; 101 } else { 102 String sanitized = sanitizePathCharacters(mdcValue, event.getTimeStamp()); 103 return sanitized == null ? defaultValue : sanitized; 104 } 105 } 106 107 108 /** 109 * Returns the value unchanged if it is safe as a file-name segment, and 110 * {@code null} otherwise. 111 * <p> 112 * A value is rejected if it contains any of the characters {@code /}, 113 * {@code \}, {@code $}, <code>{</code> or <code>}</code>, or the sequence 114 * {@code ..}. A (rate limited) warning is emitted for each rejected value. 115 * </p> 116 * <p> 117 * Performs a single scan and no allocation. 118 * </p> 119 */ 120 String sanitizePathCharacters(String value, long timestamp) { 121 if (value == null) { 122 return null; 123 } 124 final int len = value.length(); 125 if(len == 0) { 126 if (!invocationGate.isTooSoon(timestamp)) { 127 addWarn(String.format(EMPTY_VALUE_WARNING, value, defaultValue)); 128 } 129 return null; 130 } 131 132 if (len > MAX_MDC_VALUE) { 133 if (!invocationGate.isTooSoon(timestamp)) { 134 addWarn(String.format(REJECTED_VALUE_LENGTH_WARNING, len, defaultValue)); 135 } 136 return null; 137 } 138 139 char previous = 0; 140 for (int i = 0; i < len; i++) { 141 char c = value.charAt(i); 142 if (isForbiddenCharacter(c) || (c == DOT && previous == DOT)) { 143 if (!invocationGate.isTooSoon(timestamp)) { 144 addWarn(String.format(REJECTED_VALUE_WARNING, value, defaultValue)); 145 } 146 return null; 147 } 148 previous = c; 149 } 150 return value; 151 } 152 153 private static boolean isForbiddenCharacter(char c) { 154 return FORBIDDEN_CHARACTERS.indexOf(c) != -1; 155 } 156 157 158 public String getKey() { 159 return key; 160 } 161 162 public void setKey(String key) { 163 this.key = key; 164 } 165 166 /** 167 * @return 168 * @see #setDefaultValue(String) 169 */ 170 public String getDefaultValue() { 171 return defaultValue; 172 } 173 174 /** 175 * The default MDC value in case the MDC is not set for {@link #setKey(String) 176 * mdcKey}. 177 * <p/> 178 * <p> 179 * For example, if {@link #setKey(String) Key} is set to the value "someKey", 180 * and the MDC is not set for "someKey", then this appender will use the default 181 * value, which you can set with the help of this method. 182 * 183 * @param defaultValue 184 */ 185 public void setDefaultValue(String defaultValue) { 186 this.defaultValue = defaultValue; 187 } 188}