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&uuml;lc&uuml;
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}