This is an automated email from the ASF dual-hosted git repository.

ckozak pushed a commit to branch log4j-2.3.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


The following commit(s) were added to refs/heads/log4j-2.3.x by this push:
     new ce6b78d  Fix string substitution recursion (#641)
ce6b78d is described below

commit ce6b78d082aae89089cb3ad25cdd46e9ec70a70b
Author: Carter Kozak <[email protected]>
AuthorDate: Sun Dec 19 01:42:37 2021 -0500

    Fix string substitution recursion (#641)
    
    cherry-pick of 806023265f8c905b2dd1d81fd2458f64b2ea0b5e
---
 .../log4j/core/config/AbstractConfiguration.java   |  17 +-
 .../logging/log4j/core/config/AppenderControl.java |   6 +-
 .../log4j/core/config/ConfigurationFactory.java    |   3 +-
 .../log4j/core/config/json/JsonConfiguration.java  |   2 +-
 .../core/config/plugins/util/PluginBuilder.java    |  10 +-
 .../log4j/core/config/xml/XmlConfiguration.java    |   2 +-
 .../core/lookup/ConfigurationStrSubstitutor.java   |  63 ++++
 .../log4j/core/lookup/ContextMapLookup.java        |   2 +-
 .../logging/log4j/core/lookup/DateLookup.java      |   2 +-
 .../logging/log4j/core/lookup/Interpolator.java    |   2 +-
 .../log4j/core/lookup/RuntimeStrSubstitutor.java   |  61 ++++
 .../logging/log4j/core/lookup/StrSubstitutor.java  | 396 ++++++++++++++-------
 ...rnResolverDoesNotEvaluateThreadContextTest.java | 117 ++++++
 .../RoutingAppenderKeyLookupEvaluationTest.java    |  94 +++++
 .../log4j/core/lookup/StrSubstitutorTest.java      | 138 ++++++-
 ...{log4j-routing.xml => log4j-routing-lookup.xml} |  34 +-
 log4j-core/src/test/resources/log4j-routing.json   |   5 +-
 log4j-core/src/test/resources/log4j-routing.xml    |   5 +-
 log4j-core/src/test/resources/log4j-routing2.json  |   5 +-
 .../log4j2-pattern-layout-with-context.xml         |  34 ++
 .../logging/log4j/web/Log4jWebInitializerImpl.java |   3 +-
 src/changes/changes.xml                            |   3 +
 22 files changed, 826 insertions(+), 178 deletions(-)

diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java
index d87e4b9..c4c965a 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java
@@ -48,8 +48,10 @@ import 
org.apache.logging.log4j.core.config.plugins.util.PluginType;
 import org.apache.logging.log4j.core.filter.AbstractFilterable;
 import org.apache.logging.log4j.core.impl.Log4jContextFactory;
 import org.apache.logging.log4j.core.layout.PatternLayout;
+import org.apache.logging.log4j.core.lookup.ConfigurationStrSubstitutor;
 import org.apache.logging.log4j.core.lookup.Interpolator;
 import org.apache.logging.log4j.core.lookup.MapLookup;
+import org.apache.logging.log4j.core.lookup.RuntimeStrSubstitutor;
 import org.apache.logging.log4j.core.lookup.StrLookup;
 import org.apache.logging.log4j.core.lookup.StrSubstitutor;
 import org.apache.logging.log4j.core.net.Advertiser;
@@ -102,7 +104,8 @@ public abstract class AbstractConfiguration extends 
AbstractFilterable implement
     private List<CustomLevelConfig> customLevels = Collections.emptyList();
     private final ConcurrentMap<String, String> properties = new 
ConcurrentHashMap<String, String>();
     private final StrLookup tempLookup = new Interpolator(properties);
-    private final StrSubstitutor subst = new StrSubstitutor(tempLookup);
+    private final StrSubstitutor subst = new RuntimeStrSubstitutor(tempLookup);
+    private final StrSubstitutor configurationStrSubstitutor = new 
ConfigurationStrSubstitutor(subst);
     private LoggerConfig root = new LoggerConfig();
     private final ConcurrentMap<String, Object> componentMap = new 
ConcurrentHashMap<String, Object>();
     protected final List<String> pluginPackages = new ArrayList<String>();
@@ -338,12 +341,16 @@ public abstract class AbstractConfiguration extends 
AbstractFilterable implement
             final Node first = rootNode.getChildren().get(0);
             createConfiguration(first, null);
             if (first.getObject() != null) {
-                subst.setVariableResolver((StrLookup) first.getObject());
+                StrLookup lookup = (StrLookup) first.getObject();
+                subst.setVariableResolver(lookup);
+                configurationStrSubstitutor.setVariableResolver(lookup);
             }
         } else {
             final Map<String, String> map = 
this.getComponent(CONTEXT_PROPERTIES);
             final StrLookup lookup = map == null ? null : new MapLookup(map);
-            subst.setVariableResolver(new Interpolator(lookup, 
pluginPackages));
+            Interpolator interpolator = new Interpolator(lookup, 
pluginPackages);
+            subst.setVariableResolver(interpolator);
+            configurationStrSubstitutor.setVariableResolver(interpolator);
         }
 
         boolean setLoggers = false;
@@ -496,6 +503,10 @@ public abstract class AbstractConfiguration extends 
AbstractFilterable implement
         return subst;
     }
 
+    public StrSubstitutor getConfigurationStrSubstitutor() {
+        return configurationStrSubstitutor;
+    }
+
     @Override
     public void setConfigurationMonitor(final ConfigurationMonitor monitor) {
         this.monitor = monitor;
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AppenderControl.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AppenderControl.java
index ce4b9fb..5b04e7f 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AppenderControl.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AppenderControl.java
@@ -102,10 +102,10 @@ public class AppenderControl extends AbstractFilterable {
                 if (!appender.ignoreExceptions()) {
                     throw ex;
                 }
-            } catch (final Exception ex) {
-                appender.getHandler().error("An exception occurred processing 
Appender " + appender.getName(), ex);
+            } catch (final Throwable t) {
+                appender.getHandler().error("An exception occurred processing 
Appender " + appender.getName(), t);
                 if (!appender.ignoreExceptions()) {
-                    throw new AppenderLoggingException(ex);
+                    throw new AppenderLoggingException(t);
                 }
             }
         } finally {
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationFactory.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationFactory.java
index 29933d7..7897823 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationFactory.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationFactory.java
@@ -37,6 +37,7 @@ import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.core.config.plugins.util.PluginManager;
 import org.apache.logging.log4j.core.config.plugins.util.PluginType;
+import org.apache.logging.log4j.core.lookup.ConfigurationStrSubstitutor;
 import org.apache.logging.log4j.core.lookup.Interpolator;
 import org.apache.logging.log4j.core.lookup.StrSubstitutor;
 import org.apache.logging.log4j.core.util.FileUtils;
@@ -117,7 +118,7 @@ public abstract class ConfigurationFactory {
 
     private static ConfigurationFactory configFactory = new Factory();
 
-    protected final StrSubstitutor substitutor = new StrSubstitutor(new 
Interpolator());
+    protected final StrSubstitutor substitutor = new 
ConfigurationStrSubstitutor(new Interpolator());
 
     private static final Lock LOCK = new ReentrantLock();
 
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/json/JsonConfiguration.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/json/JsonConfiguration.java
index c9a8726..cb6b51f 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/json/JsonConfiguration.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/json/JsonConfiguration.java
@@ -73,7 +73,7 @@ public class JsonConfiguration extends AbstractConfiguration 
implements Reconfig
                     .withStatus(getDefaultStatus());
             for (final Map.Entry<String, String> entry : 
rootNode.getAttributes().entrySet()) {
                 final String key = entry.getKey();
-                final String value = 
getStrSubstitutor().replace(entry.getValue());
+                final String value = 
getConfigurationStrSubstitutor().replace(entry.getValue());
                 // TODO: this duplicates a lot of the XmlConfiguration 
constructor
                 if ("status".equalsIgnoreCase(key)) {
                     statusConfig.withStatus(value);
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginBuilder.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginBuilder.java
index 5730c45..c74ddc0 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginBuilder.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginBuilder.java
@@ -39,12 +39,12 @@ import 
org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidat
 import 
org.apache.logging.log4j.core.config.plugins.validation.ConstraintValidators;
 import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitor;
 import org.apache.logging.log4j.core.config.plugins.visitors.PluginVisitors;
+import org.apache.logging.log4j.core.lookup.ConfigurationStrSubstitutor;
 import org.apache.logging.log4j.core.util.Assert;
 import org.apache.logging.log4j.core.util.Builder;
 import org.apache.logging.log4j.core.util.ReflectionUtil;
 import org.apache.logging.log4j.core.util.TypeUtil;
 import org.apache.logging.log4j.status.StatusLogger;
-import org.apache.logging.log4j.util.Chars;
 import org.apache.logging.log4j.util.StringBuilders;
 
 /**
@@ -186,7 +186,9 @@ public class PluginBuilder implements Builder<Object> {
                     final Object value = visitor.setAliases(aliases)
                         .setAnnotation(a)
                         .setConversionType(field.getType())
-                        .setStrSubstitutor(configuration.getStrSubstitutor())
+                        .setStrSubstitutor(event == null
+                                ? new 
ConfigurationStrSubstitutor(configuration.getStrSubstitutor())
+                                : configuration.getStrSubstitutor())
                         .setMember(field)
                         .visit(configuration, node, event, log);
                     // don't overwrite default values if the visitor gives us 
no value to inject
@@ -247,7 +249,9 @@ public class PluginBuilder implements Builder<Object> {
                     final Object value = visitor.setAliases(aliases)
                         .setAnnotation(a)
                         .setConversionType(types[i])
-                        .setStrSubstitutor(configuration.getStrSubstitutor())
+                        .setStrSubstitutor(event == null
+                                ? new 
ConfigurationStrSubstitutor(configuration.getStrSubstitutor())
+                                : configuration.getStrSubstitutor())
                         .setMember(factory)
                         .visit(configuration, node, event, log);
                     // don't overwrite existing values if the visitor gives us 
no value to inject
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java
index 2425508..6099008 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java
@@ -144,7 +144,7 @@ public class XmlConfiguration extends AbstractConfiguration 
implements Reconfigu
                     .withStatus(getDefaultStatus());
             for (final Map.Entry<String, String> entry : attrs.entrySet()) {
                 final String key = entry.getKey();
-                final String value = 
getStrSubstitutor().replace(entry.getValue());
+                final String value = 
getConfigurationStrSubstitutor().replace(entry.getValue());
                 if ("status".equalsIgnoreCase(key)) {
                     statusConfig.withStatus(value);
                 } else if ("dest".equalsIgnoreCase(key)) {
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ConfigurationStrSubstitutor.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ConfigurationStrSubstitutor.java
new file mode 100644
index 0000000..1287721
--- /dev/null
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ConfigurationStrSubstitutor.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.core.lookup;
+
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * {@link RuntimeStrSubstitutor} is a {@link StrSubstitutor} which only 
supports recursive evaluation of lookups.
+ * This can be dangerous when combined with user-provided inputs, and should 
only be used on data directly from
+ * a configuration.
+ */
+public final class ConfigurationStrSubstitutor extends StrSubstitutor {
+
+    public ConfigurationStrSubstitutor() {
+    }
+
+    public ConfigurationStrSubstitutor(final Map<String, String> valueMap) {
+        super(valueMap);
+    }
+
+    public ConfigurationStrSubstitutor(final Properties properties) {
+        super(properties);
+    }
+
+    public ConfigurationStrSubstitutor(final StrLookup lookup) {
+        super(lookup);
+    }
+
+    public ConfigurationStrSubstitutor(final StrSubstitutor other) {
+        super(other);
+    }
+
+    @Override
+    boolean isRecursiveEvaluationAllowed() {
+        return true;
+    }
+
+    @Override
+    void setRecursiveEvaluationAllowed(final boolean 
recursiveEvaluationAllowed) {
+        throw new UnsupportedOperationException(
+                "recursiveEvaluationAllowed cannot be modified within 
ConfigurationStrSubstitutor");
+    }
+
+    @Override
+    public String toString() {
+        return "ConfigurationStrSubstitutor{" + super.toString() + "}";
+    }
+}
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java
index 9a39338..4c2e585 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java
@@ -44,6 +44,6 @@ public class ContextMapLookup implements StrLookup {
      */
     @Override
     public String lookup(final LogEvent event, final String key) {
-        return event.getContextMap().get(key);
+        return event == null ? null : event.getContextMap().get(key);
     }
 }
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/DateLookup.java 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/DateLookup.java
index 3e630b0..3bcecf7 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/DateLookup.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/DateLookup.java
@@ -54,7 +54,7 @@ public class DateLookup implements StrLookup {
      */
     @Override
     public String lookup(final LogEvent event, final String key) {
-        return formatDate(event.getTimeMillis(), key);
+        return event == null ? lookup(key) : formatDate(event.getTimeMillis(), 
key);
     }
 
     private String formatDate(final long date, final String format) {
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/Interpolator.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/Interpolator.java
index b0348e8..539597b 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/Interpolator.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/Interpolator.java
@@ -36,7 +36,7 @@ public class Interpolator extends AbstractLookup {
     private static final Logger LOGGER = StatusLogger.getLogger();
 
     /** Constant for the prefix separator. */
-    private static final char PREFIX_SEPARATOR = ':';
+    static final char PREFIX_SEPARATOR = ':';
 
     private final Map<String, StrLookup> lookups = new HashMap<String, 
StrLookup>();
 
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/RuntimeStrSubstitutor.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/RuntimeStrSubstitutor.java
new file mode 100644
index 0000000..f002c6e
--- /dev/null
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/RuntimeStrSubstitutor.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.core.lookup;
+
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * {@link RuntimeStrSubstitutor} is a {@link StrSubstitutor} which only 
supports evaluation of top-level lookups.
+ */
+public final class RuntimeStrSubstitutor extends StrSubstitutor {
+
+    public RuntimeStrSubstitutor() {
+    }
+
+    public RuntimeStrSubstitutor(final Map<String, String> valueMap) {
+        super(valueMap);
+    }
+
+    public RuntimeStrSubstitutor(final Properties properties) {
+        super(properties);
+    }
+
+    public RuntimeStrSubstitutor(final StrLookup lookup) {
+        super(lookup);
+    }
+
+    public RuntimeStrSubstitutor(final StrSubstitutor other) {
+        super(other);
+    }
+
+    @Override
+    boolean isRecursiveEvaluationAllowed() {
+        return false;
+    }
+
+    @Override
+    void setRecursiveEvaluationAllowed(final boolean 
recursiveEvaluationAllowed) {
+        throw new UnsupportedOperationException(
+                "recursiveEvaluationAllowed cannot be modified within 
RuntimeStrSubstitutor");
+    }
+
+    @Override
+    public String toString() {
+        return "RuntimeStrSubstitutor{" + super.toString() + "}";
+    }
+}
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/StrSubstitutor.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/StrSubstitutor.java
index 77362bc..ffa93a0 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/StrSubstitutor.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/StrSubstitutor.java
@@ -25,6 +25,8 @@ import java.util.Map;
 import java.util.Properties;
 
 import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.Strings;
 
 /**
@@ -142,21 +144,25 @@ public class StrSubstitutor {
      * Constant for the default escape character.
      */
     public static final char DEFAULT_ESCAPE = '$';
-    
+
     /**
      * Constant for the default variable prefix.
      */
     public static final StrMatcher DEFAULT_PREFIX = 
StrMatcher.stringMatcher(DEFAULT_ESCAPE + "{");
-    
+
     /**
      * Constant for the default variable suffix.
      */
     public static final StrMatcher DEFAULT_SUFFIX = 
StrMatcher.stringMatcher("}");
-    
+
     /**
      * Constant for the default value delimiter of a variable.
      */
-    public static final StrMatcher DEFAULT_VALUE_DELIMITER = 
StrMatcher.stringMatcher(":-");
+    public static final String DEFAULT_VALUE_DELIMITER_STRING = ":-";
+    public static final StrMatcher DEFAULT_VALUE_DELIMITER = 
StrMatcher.stringMatcher(DEFAULT_VALUE_DELIMITER_STRING);
+
+    public static final String ESCAPE_DELIMITER_STRING = ":\\-";
+    public static final StrMatcher DEFAULT_VALUE_ESCAPE_DELIMITER = 
StrMatcher.stringMatcher(ESCAPE_DELIMITER_STRING);
 
     private static final int BUF_SIZE = 256;
 
@@ -164,26 +170,44 @@ public class StrSubstitutor {
      * Stores the escape character.
      */
     private char escapeChar;
+
     /**
      * Stores the variable prefix.
      */
     private StrMatcher prefixMatcher;
+
     /**
      * Stores the variable suffix.
      */
     private StrMatcher suffixMatcher;
+
     /**
      * Stores the default variable value delimiter
      */
+    private String valueDelimiterString;
     private StrMatcher valueDelimiterMatcher;
+
+    /**
+     * Escape string to avoid matching the value delimiter matcher;
+     */
+    private StrMatcher valueEscapeDelimiterMatcher;
+
     /**
      * Variable resolution is delegated to an implementer of VariableResolver.
      */
     private StrLookup variableResolver;
+
     /**
      * The flag whether substitution in variable names is enabled.
      */
-    private boolean enableSubstitutionInVariables;
+    private boolean enableSubstitutionInVariables = false;
+
+    /**
+     * The currently active Configuration for use by ConfigurationAware 
StrLookup implementations.
+     */
+    private Configuration configuration;
+
+    private boolean recursiveEvaluationAllowed;
 
     //-----------------------------------------------------------------------
     /**
@@ -193,6 +217,7 @@ public class StrSubstitutor {
     public StrSubstitutor() {
         this(null, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
     }
+
     /**
      * Creates a new instance and initializes it. Uses defaults for variable
      * prefix and suffix and the escaping character.
@@ -245,6 +270,16 @@ public class StrSubstitutor {
     }
 
     /**
+     * Creates a new instance and initializes it. Uses defaults for variable
+     * prefix and suffix and the escaping character.
+     *
+     * @param properties  the map with the variables' values, may be null
+     */
+    public StrSubstitutor(final Properties properties) {
+        this(toTypeSafeMap(properties));
+    }
+
+    /**
      * Creates a new instance and initializes it.
      *
      * @param variableResolver  the variable resolver, may be null
@@ -300,7 +335,9 @@ public class StrSubstitutor {
     public StrSubstitutor(final StrLookup variableResolver, final StrMatcher 
prefixMatcher,
                           final StrMatcher suffixMatcher,
                           final char escape) {
-        this(variableResolver, prefixMatcher, suffixMatcher, escape, 
DEFAULT_VALUE_DELIMITER);
+        this(variableResolver, prefixMatcher, suffixMatcher, escape, 
DEFAULT_VALUE_DELIMITER,
+                DEFAULT_VALUE_ESCAPE_DELIMITER);
+        this.valueDelimiterString = DEFAULT_VALUE_DELIMITER_STRING;
     }
 
     /**
@@ -313,13 +350,48 @@ public class StrSubstitutor {
      * @param valueDelimiterMatcher  the variable default value delimiter 
matcher, may be null
      * @throws IllegalArgumentException if the prefix or suffix is null
      */
-    public StrSubstitutor(
-            final StrLookup variableResolver, final StrMatcher prefixMatcher, 
final StrMatcher suffixMatcher, final char escape, final StrMatcher 
valueDelimiterMatcher) {
+    public StrSubstitutor(final StrLookup variableResolver, final StrMatcher 
prefixMatcher,
+            final StrMatcher suffixMatcher, final char escape, final 
StrMatcher valueDelimiterMatcher) {
+        this.setVariableResolver(variableResolver);
+        this.setVariablePrefixMatcher(prefixMatcher);
+        this.setVariableSuffixMatcher(suffixMatcher);
+        this.setEscapeChar(escape);
+        this.setValueDelimiterMatcher(valueDelimiterMatcher);
+    }
+
+    /**
+     * Creates a new instance and initializes it.
+     *
+     * @param variableResolver  the variable resolver, may be null
+     * @param prefixMatcher  the prefix for variables, not null
+     * @param suffixMatcher  the suffix for variables, not null
+     * @param escape  the escape character
+     * @param valueDelimiterMatcher  the variable default value delimiter 
matcher, may be null
+     * @param valueEscapeMatcher the matcher to escape defaulting, may be null.
+     * @throws IllegalArgumentException if the prefix or suffix is null
+     */
+    public StrSubstitutor(final StrLookup variableResolver, final StrMatcher 
prefixMatcher,
+                          final StrMatcher suffixMatcher, final char escape, 
final StrMatcher valueDelimiterMatcher,
+                          final StrMatcher valueEscapeMatcher) {
         this.setVariableResolver(variableResolver);
         this.setVariablePrefixMatcher(prefixMatcher);
         this.setVariableSuffixMatcher(suffixMatcher);
         this.setEscapeChar(escape);
         this.setValueDelimiterMatcher(valueDelimiterMatcher);
+        valueEscapeDelimiterMatcher = valueEscapeMatcher;
+    }
+
+    StrSubstitutor(final StrSubstitutor other) {
+        this.setVariableResolver(other.getVariableResolver());
+        this.setVariablePrefixMatcher(other.getVariablePrefixMatcher());
+        this.setVariableSuffixMatcher(other.getVariableSuffixMatcher());
+        this.setEscapeChar(other.getEscapeChar());
+        this.setValueDelimiterMatcher(other.valueDelimiterMatcher);
+        this.valueEscapeDelimiterMatcher = other.valueEscapeDelimiterMatcher;
+        this.configuration = other.configuration;
+        this.recursiveEvaluationAllowed = other.isRecursiveEvaluationAllowed();
+        this.enableSubstitutionInVariables = 
other.isEnableSubstitutionInVariables();
+        this.valueDelimiterString = other.valueDelimiterString;
     }
 
     //-----------------------------------------------------------------------
@@ -374,6 +446,19 @@ public class StrSubstitutor {
         return StrSubstitutor.replace(source, valueMap);
     }
 
+    private static Map<String, String> toTypeSafeMap(final Properties 
properties) {
+        final Map<String, String> map = new HashMap<String, 
String>(properties.size());
+        for (final String name : properties.stringPropertyNames()) {
+            map.put(name, properties.getProperty(name));
+        }
+        return map;
+    }
+
+    private static String handleFailedReplacement(String input, Throwable 
throwable) {
+        StatusLogger.getLogger().error("Replacement failed on {}", input, 
throwable);
+        return input;
+    }
+
     //-----------------------------------------------------------------------
     /**
      * Replaces all the occurrences of variables with their matching values
@@ -399,8 +484,12 @@ public class StrSubstitutor {
             return null;
         }
         final StringBuilder buf = new StringBuilder(source);
-        if (!substitute(event, buf, 0, source.length())) {
-            return source;
+        try {
+            if (!substitute(event, buf, 0, source.length())) {
+                return source;
+            }
+        } catch (Throwable t) {
+            return handleFailedReplacement(source, t);
         }
         return buf.toString();
     }
@@ -441,8 +530,12 @@ public class StrSubstitutor {
             return null;
         }
         final StringBuilder buf = new StringBuilder(length).append(source, 
offset, length);
-        if (!substitute(event, buf, 0, length)) {
-            return source.substring(offset, offset + length);
+        try {
+            if (!substitute(event, buf, 0, length)) {
+                return source.substring(offset, offset + length);
+            }
+        } catch (Throwable t) {
+            return handleFailedReplacement(source, t);
         }
         return buf.toString();
     }
@@ -475,7 +568,11 @@ public class StrSubstitutor {
             return null;
         }
         final StringBuilder buf = new 
StringBuilder(source.length).append(source);
-        substitute(event, buf, 0, source.length);
+        try {
+            substitute(event, buf, 0, source.length);
+        } catch (Throwable t) {
+            return handleFailedReplacement(new String(source), t);
+        }
         return buf.toString();
     }
 
@@ -517,7 +614,11 @@ public class StrSubstitutor {
             return null;
         }
         final StringBuilder buf = new StringBuilder(length).append(source, 
offset, length);
-        substitute(event, buf, 0, length);
+        try {
+            substitute(event, buf, 0, length);
+        } catch (Throwable t) {
+            return handleFailedReplacement(new String(source, offset, length), 
t);
+        }
         return buf.toString();
     }
 
@@ -549,7 +650,11 @@ public class StrSubstitutor {
             return null;
         }
         final StringBuilder buf = new 
StringBuilder(source.length()).append(source);
-        substitute(event, buf, 0, buf.length());
+        try {
+            substitute(event, buf, 0, buf.length());
+        } catch (Throwable t) {
+            return handleFailedReplacement(source.toString(), t);
+        }
         return buf.toString();
     }
 
@@ -591,7 +696,11 @@ public class StrSubstitutor {
             return null;
         }
         final StringBuilder buf = new StringBuilder(length).append(source, 
offset, length);
-        substitute(event, buf, 0, length);
+        try {
+            substitute(event, buf, 0, length);
+        } catch (Throwable t) {
+            return handleFailedReplacement(source.substring(offset, offset + 
length), t);
+        }
         return buf.toString();
     }
 
@@ -623,7 +732,11 @@ public class StrSubstitutor {
             return null;
         }
         final StringBuilder buf = new 
StringBuilder(source.length()).append(source);
-        substitute(event, buf, 0, buf.length());
+        try {
+            substitute(event, buf, 0, buf.length());
+        } catch (Throwable t) {
+            return handleFailedReplacement(source.toString(), t);
+        }
         return buf.toString();
     }
     /**
@@ -664,7 +777,11 @@ public class StrSubstitutor {
             return null;
         }
         final StringBuilder buf = new StringBuilder(length).append(source, 
offset, length);
-        substitute(event, buf, 0, length);
+        try {
+            substitute(event, buf, 0, length);
+        } catch (Throwable t) {
+            return handleFailedReplacement(source.substring(offset, offset + 
length), t);
+        }
         return buf.toString();
     }
 
@@ -694,8 +811,13 @@ public class StrSubstitutor {
         if (source == null) {
             return null;
         }
-        final StringBuilder buf = new StringBuilder().append(source);
-        substitute(event, buf, 0, buf.length());
+        String stringValue = String.valueOf(source);
+        final StringBuilder buf = new 
StringBuilder(stringValue.length()).append(stringValue);
+        try {
+            substitute(event, buf, 0, buf.length());
+        } catch (Throwable t) {
+            return handleFailedReplacement(stringValue, t);
+        }
         return buf.toString();
     }
 
@@ -753,7 +875,12 @@ public class StrSubstitutor {
             return false;
         }
         final StringBuilder buf = new StringBuilder(length).append(source, 
offset, length);
-        if (!substitute(event, buf, 0, length)) {
+        try {
+            if (!substitute(event, buf, 0, length)) {
+                return false;
+            }
+        } catch (Throwable t) {
+            StatusLogger.getLogger().error("Replacement failed on {}", source, 
t);
             return false;
         }
         source.replace(offset, offset + length, buf.toString());
@@ -868,122 +995,133 @@ public class StrSubstitutor {
         final StrMatcher valueDelimiterMatcher = getValueDelimiterMatcher();
         final boolean substitutionInVariablesEnabled = 
isEnableSubstitutionInVariables();
 
-        final boolean top = (priorVariables == null);
+        final boolean top = priorVariables == null;
         boolean altered = false;
         int lengthChange = 0;
         char[] chars = getChars(buf);
         int bufEnd = offset + length;
         int pos = offset;
         while (pos < bufEnd) {
-            final int startMatchLen = prefixMatcher.isMatch(chars, pos, offset,
-                    bufEnd);
+            final int startMatchLen = prefixMatcher.isMatch(chars, pos, 
offset, bufEnd);
             if (startMatchLen == 0) {
                 pos++;
+            } else // found variable start marker
+            if (pos > offset && chars[pos - 1] == escape) {
+                // escaped
+                buf.deleteCharAt(pos - 1);
+                chars = getChars(buf);
+                lengthChange--;
+                altered = true;
+                bufEnd--;
             } else {
-                // found variable start marker
-                if (pos > offset && chars[pos - 1] == escape) {
-                    // escaped
-                    buf.deleteCharAt(pos - 1);
-                    chars = getChars(buf);
-                    lengthChange--;
-                    altered = true;
-                    bufEnd--;
-                } else {
-                    // find suffix
-                    final int startPos = pos;
-                    pos += startMatchLen;
-                    int endMatchLen = 0;
-                    int nestedVarCount = 0;
-                    while (pos < bufEnd) {
-                        if (substitutionInVariablesEnabled
-                                && (endMatchLen = prefixMatcher.isMatch(chars,
-                                        pos, offset, bufEnd)) != 0) {
-                            // found a nested variable start
-                            nestedVarCount++;
-                            pos += endMatchLen;
-                            continue;
-                        }
+                // find suffix
+                final int startPos = pos;
+                pos += startMatchLen;
+                int endMatchLen = 0;
+                int nestedVarCount = 0;
+                while (pos < bufEnd) {
+                    if (substitutionInVariablesEnabled
+                            && (endMatchLen = prefixMatcher.isMatch(chars, 
pos, offset, bufEnd)) != 0) {
+                        // found a nested variable start
+                        nestedVarCount++;
+                        pos += endMatchLen;
+                        continue;
+                    }
 
-                        endMatchLen = suffixMatcher.isMatch(chars, pos, offset,
-                                bufEnd);
-                        if (endMatchLen == 0) {
-                            pos++;
-                        } else {
-                            // found variable end marker
-                            if (nestedVarCount == 0) {
-                                String varNameExpr = new String(chars, startPos
-                                        + startMatchLen, pos - startPos
-                                        - startMatchLen);
-                                if (substitutionInVariablesEnabled) {
-                                    final StringBuilder bufName = new 
StringBuilder(varNameExpr);
-                                    substitute(event, bufName, 0, 
bufName.length());
-                                    varNameExpr = bufName.toString();
+                    endMatchLen = suffixMatcher.isMatch(chars, pos, offset, 
bufEnd);
+                    if (endMatchLen == 0) {
+                        pos++;
+                    } else {
+                        // found variable end marker
+                        if (nestedVarCount == 0) {
+                            String varNameExpr = new String(chars, startPos + 
startMatchLen, pos - startPos - startMatchLen);
+                            if (substitutionInVariablesEnabled) {
+                                // initialize priorVariables if they're not 
already set
+                                if (priorVariables == null) {
+                                    priorVariables = new ArrayList<String>();
                                 }
-                                pos += endMatchLen;
-                                final int endPos = pos;
-
-                                String varName = varNameExpr;
-                                String varDefaultValue = null;
-
-                                if (valueDelimiterMatcher != null) {
-                                    final char [] varNameExprChars = 
varNameExpr.toCharArray();
-                                    int valueDelimiterMatchLen = 0;
-                                    for (int i = 0; i < 
varNameExprChars.length; i++) {
-                                        // if there's any nested variable when 
nested variable substitution disabled, then stop resolving name and default 
value.
-                                        if (!substitutionInVariablesEnabled
-                                                && 
prefixMatcher.isMatch(varNameExprChars, i, i, varNameExprChars.length) != 0) {
+                                final StringBuilder bufName = new 
StringBuilder(varNameExpr);
+                                substitute(event, bufName, 0, 
bufName.length(), priorVariables);
+                                varNameExpr = bufName.toString();
+                            }
+                            pos += endMatchLen;
+                            final int endPos = pos;
+
+                            String varName = varNameExpr;
+                            String varDefaultValue = null;
+
+                            if (valueDelimiterMatcher != null) {
+                                final char [] varNameExprChars = 
varNameExpr.toCharArray();
+                                int valueDelimiterMatchLen = 0;
+                                for (int i = 0; i < varNameExprChars.length; 
i++) {
+                                    // if there's any nested variable when 
nested variable substitution disabled, then stop resolving name and default 
value.
+                                    if (!substitutionInVariablesEnabled
+                                            && 
prefixMatcher.isMatch(varNameExprChars, i, i, varNameExprChars.length) != 0) {
+                                        break;
+                                    }
+                                    if (valueEscapeDelimiterMatcher != null) {
+                                        int matchLen = 
valueEscapeDelimiterMatcher.isMatch(varNameExprChars, i);
+                                        if (matchLen != 0) {
+                                            String varNamePrefix = 
varNameExpr.substring(0, i) + Interpolator.PREFIX_SEPARATOR;
+                                            varName = varNamePrefix + 
varNameExpr.substring(i + matchLen - 1);
+                                            for (int j = i + matchLen; j < 
varNameExprChars.length; ++j){
+                                                if ((valueDelimiterMatchLen = 
valueDelimiterMatcher.isMatch(varNameExprChars, j)) != 0) {
+                                                    varName = varNamePrefix + 
varNameExpr.substring(i + matchLen, j);
+                                                    varDefaultValue = 
varNameExpr.substring(j + valueDelimiterMatchLen);
+                                                    break;
+                                                }
+                                            }
                                             break;
-                                        }
-                                        if ((valueDelimiterMatchLen = 
valueDelimiterMatcher.isMatch(varNameExprChars, i)) != 0) {
+                                        } else if ((valueDelimiterMatchLen = 
valueDelimiterMatcher.isMatch(varNameExprChars, i)) != 0) {
                                             varName = varNameExpr.substring(0, 
i);
                                             varDefaultValue = 
varNameExpr.substring(i + valueDelimiterMatchLen);
                                             break;
                                         }
+                                    } else if ((valueDelimiterMatchLen = 
valueDelimiterMatcher.isMatch(varNameExprChars, i)) != 0) {
+                                        varName = varNameExpr.substring(0, i);
+                                        varDefaultValue = 
varNameExpr.substring(i + valueDelimiterMatchLen);
+                                        break;
                                     }
                                 }
+                            }
 
-                                // on the first call initialize priorVariables
-                                if (priorVariables == null) {
-                                    priorVariables = new ArrayList<String>();
-                                    priorVariables.add(new String(chars,
-                                            offset, length + lengthChange));
-                                }
+                            // on the first call initialize priorVariables
+                            if (priorVariables == null) {
+                                priorVariables = new ArrayList<String>();
+                                priorVariables.add(new String(chars, offset, 
length + lengthChange));
+                            }
 
-                                // handle cyclic substitution
-                                checkCyclicSubstitution(varName, 
priorVariables);
-                                priorVariables.add(varName);
+                            // handle cyclic substitution
+                            boolean isCyclic = isCyclicSubstitution(varName, 
priorVariables);
 
-                                // resolve the variable
-                                String varValue = resolveVariable(event, 
varName, buf,
-                                        startPos, endPos);
-                                if (varValue == null) {
-                                    varValue = varDefaultValue;
-                                }
-                                if (varValue != null) {
-                                    // recursive replace
-                                    final int varLen = varValue.length();
-                                    buf.replace(startPos, endPos, varValue);
-                                    altered = true;
-                                    int change = substitute(event, buf, 
startPos,
-                                            varLen, priorVariables);
-                                    change = change
-                                            + (varLen - (endPos - startPos));
-                                    pos += change;
-                                    bufEnd += change;
-                                    lengthChange += change;
-                                    chars = getChars(buf); // in case buffer 
was
-                                                        // altered
-                                }
+                            // resolve the variable
+                            String varValue = isCyclic ? null : 
resolveVariable(event, varName, buf, startPos, endPos);
+                            if (varValue == null) {
+                                varValue = varDefaultValue;
+                            }
+                            if (varValue != null) {
+                                // recursive replace
+                                final int varLen = varValue.length();
+                                buf.replace(startPos, endPos, varValue);
+                                altered = true;
+                                int change = isRecursiveEvaluationAllowed()
+                                        ? substitute(event, buf, startPos, 
varLen, priorVariables)
+                                        : 0;
+                                change = change + (varLen - (endPos - 
startPos));
+                                pos += change;
+                                bufEnd += change;
+                                lengthChange += change;
+                                chars = getChars(buf); // in case buffer was 
altered
+                            }
 
-                                // remove variable from the cyclic stack
-                                priorVariables
-                                        .remove(priorVariables.size() - 1);
-                                break;
-                            } else {
-                                nestedVarCount--;
-                                pos += endMatchLen;
+                            // remove variable from the cyclic stack
+                            if (!isCyclic) {
+                                priorVariables.remove(priorVariables.size() - 
1);
                             }
+                            break;
                         }
+                        nestedVarCount--;
+                        pos += endMatchLen;
                     }
                 }
             }
@@ -995,21 +1133,23 @@ public class StrSubstitutor {
     }
 
     /**
-     * Checks if the specified variable is already in the stack (list) of 
variables.
+     * Checks if the specified variable is already in the stack (list) of 
variables, adding the value
+     * if it's not already present.
      *
      * @param varName  the variable name to check
      * @param priorVariables  the list of prior variables
+     * @return true if this is a cyclic substitution
      */
-    private void checkCyclicSubstitution(final String varName, final 
List<String> priorVariables) {
+    private boolean isCyclicSubstitution(final String varName, final 
List<String> priorVariables) {
         if (!priorVariables.contains(varName)) {
-            return;
+            priorVariables.add(varName);
+            return false;
         }
         final StringBuilder buf = new StringBuilder(BUF_SIZE);
         buf.append("Infinite loop in property interpolation of ");
-        buf.append(priorVariables.remove(0));
-        buf.append(": ");
         appendWithSeparators(buf, priorVariables, "->");
-        throw new IllegalStateException(buf.toString());
+        StatusLogger.getLogger().warn(buf);
+        return true;
     }
 
     /**
@@ -1038,7 +1178,12 @@ public class StrSubstitutor {
         if (resolver == null) {
             return null;
         }
-        return resolver.lookup(event, variableName);
+        try {
+            return resolver.lookup(event, variableName);
+        } catch (Throwable t) {
+            StatusLogger.getLogger().error("Resolver failed to lookup {}", 
variableName, t);
+            return null;
+        }
     }
 
     // Escape
@@ -1275,6 +1420,9 @@ public class StrSubstitutor {
             setValueDelimiterMatcher(null);
             return this;
         }
+        String escapeValue = valueDelimiter.substring(0, 
valueDelimiter.length() - 1) + "\\"
+                + valueDelimiter.substring(valueDelimiter.length() - 1);
+        valueEscapeDelimiterMatcher = StrMatcher.stringMatcher(escapeValue);
         return 
setValueDelimiterMatcher(StrMatcher.stringMatcher(valueDelimiter));
     }
 
@@ -1321,6 +1469,14 @@ public class StrSubstitutor {
         this.enableSubstitutionInVariables = enableSubstitutionInVariables;
     }
 
+    boolean isRecursiveEvaluationAllowed() {
+        return recursiveEvaluationAllowed;
+    }
+
+    void setRecursiveEvaluationAllowed(final boolean 
recursiveEvaluationAllowed) {
+        this.recursiveEvaluationAllowed = recursiveEvaluationAllowed;
+    }
+
     private char[] getChars(final StringBuilder sb) {
         final char[] chars = new char[sb.length()];
         sb.getChars(0, sb.length(), chars, 0);
diff --git 
a/log4j-core/src/test/java/org/apache/logging/log4j/core/PatternResolverDoesNotEvaluateThreadContextTest.java
 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/PatternResolverDoesNotEvaluateThreadContextTest.java
new file mode 100644
index 0000000..3515e70
--- /dev/null
+++ 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/PatternResolverDoesNotEvaluateThreadContextTest.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.core;
+
+import org.apache.logging.log4j.MarkerManager;
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.junit.InitialLoggerContext;
+import org.apache.logging.log4j.test.appender.ListAppender;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+
+public class PatternResolverDoesNotEvaluateThreadContextTest {
+
+
+    private static final String CONFIG = 
"log4j2-pattern-layout-with-context.xml";
+    private static final String PARAMETER = "user";
+    private ListAppender listAppender;
+
+    @ClassRule
+    public static InitialLoggerContext context = new 
InitialLoggerContext(CONFIG);
+
+    @Before
+    public void before() {
+        listAppender = context.getListAppender("list");
+        listAppender.clear();
+    }
+
+    @Test
+    public void testNoUserSet() {
+        Logger logger = context.getLogger(getClass().getName());
+        logger.info("This is a test");
+        List<String> messages = listAppender.getMessages();
+        assertTrue("No messages returned", messages != null && messages.size() 
> 0);
+        String message = messages.get(0);
+        assertEquals("INFO org.apache.logging.log4j.core." +
+                "PatternResolverDoesNotEvaluateThreadContextTest ${ctx:user} 
This is a test", message);
+    }
+
+    @Test
+    public void testMessageIsNotLookedUp() {
+        Logger logger = context.getLogger(getClass().getName());
+        logger.info("This is a ${upper:test}");
+        List<String> messages = listAppender.getMessages();
+        assertTrue("No messages returned", messages != null && messages.size() 
> 0);
+        String message = messages.get(0);
+        assertEquals("INFO org.apache.logging.log4j.core." +
+                "PatternResolverDoesNotEvaluateThreadContextTest ${ctx:user} 
This is a ${upper:test}", message);
+    }
+
+    @Test
+    public void testUser() {
+        Logger logger = context.getLogger(getClass().getName());
+        ThreadContext.put(PARAMETER, "123");
+        try {
+            logger.info("This is a test");
+        } finally {
+            ThreadContext.remove(PARAMETER);
+        }
+        List<String> messages = listAppender.getMessages();
+        assertTrue("No messages returned", messages != null && messages.size() 
> 0);
+        String message = messages.get(0);
+        assertEquals("INFO org.apache.logging.log4j.core." +
+                "PatternResolverDoesNotEvaluateThreadContextTest 123 This is a 
test", message);
+    }
+
+    @Test
+    public void testUserIsLookup() {
+        Logger logger = context.getLogger(getClass().getName());
+        ThreadContext.put(PARAMETER, "${java:version}");
+        try {
+            logger.info("This is a test");
+        } finally {
+            ThreadContext.remove(PARAMETER);
+        }
+        List<String> messages = listAppender.getMessages();
+        assertTrue("No messages returned", messages != null && messages.size() 
> 0);
+        String message = messages.get(0);
+        assertEquals("INFO org.apache.logging.log4j.core." +
+                "PatternResolverDoesNotEvaluateThreadContextTest 
${java:version} This is a test", message);
+    }
+
+    @Test
+    public void testUserHasLookup() {
+        Logger logger = context.getLogger(getClass().getName());
+        ThreadContext.put(PARAMETER, "user${java:version}name");
+        try {
+            logger.info("This is a test");
+        } finally {
+            ThreadContext.remove(PARAMETER);
+        }
+        List<String> messages = listAppender.getMessages();
+        assertTrue("No messages returned",messages != null && messages.size() 
> 0);
+        String message = messages.get(0);
+        assertEquals("INFO org.apache.logging.log4j.core." +
+                "PatternResolverDoesNotEvaluateThreadContextTest 
user${java:version}name This is a test", message);
+    }
+}
diff --git 
a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/routing/RoutingAppenderKeyLookupEvaluationTest.java
 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/routing/RoutingAppenderKeyLookupEvaluationTest.java
new file mode 100644
index 0000000..b7ab1e0
--- /dev/null
+++ 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/routing/RoutingAppenderKeyLookupEvaluationTest.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.core.appender.routing;
+
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.junit.InitialLoggerContext;
+import org.apache.logging.log4j.test.appender.ListAppender;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class RoutingAppenderKeyLookupEvaluationTest {
+    private static final String CONFIG = "log4j-routing-lookup.xml";
+
+    private static final String KEY = "user";
+    private ListAppender app;
+
+    @Rule
+    public InitialLoggerContext init = new InitialLoggerContext(CONFIG);
+
+    @Before
+    public void setUp() throws Exception {
+        ThreadContext.remove(KEY);
+        this.app = this.init.getListAppender("List");
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        this.app.clear();
+        this.init.getContext().stop();
+        ThreadContext.remove(KEY);
+    }
+
+    @Test
+    public void testRoutingNoUser() {
+        Logger logger = init.getLogger(getClass().getName());
+        logger.warn("no user");
+        String message = app.getMessages().get(0);
+        assertEquals("WARN ${ctx:user} no user", message);
+    }
+
+    @Test
+    public void testRoutingDoesNotMatchRoute() {
+        Logger logger = init.getLogger(getClass().getName());
+        ThreadContext.put(KEY, "noRouteExists");
+        logger.warn("unmatched user");
+        assertTrue(app.getMessages().isEmpty());
+    }
+
+    @Test
+    public void testRoutingContainsLookup() {
+        Logger logger = init.getLogger(getClass().getName());
+        ThreadContext.put(KEY, "${java:version}");
+        logger.warn("naughty user");
+        String message = app.getMessages().get(0);
+        assertEquals("WARN ${java:version} naughty user", message);
+    }
+
+    @Test
+    public void testRoutingMatchesEscapedLookup() {
+        Logger logger = init.getLogger(getClass().getName());
+        ThreadContext.put(KEY, "${upper:name}");
+        logger.warn("naughty user");
+        String message = app.getMessages().get(0);
+        assertEquals("WARN ${upper:name} naughty user", message);
+    }
+
+    @Test
+    public void testRoutesThemselvesNotEvaluated() {
+        Logger logger = init.getLogger(getClass().getName());
+        ThreadContext.put(KEY, "NAME");
+        logger.warn("unmatched user");
+        assertTrue(app.getMessages().isEmpty());
+    }
+}
diff --git 
a/log4j-core/src/test/java/org/apache/logging/log4j/core/lookup/StrSubstitutorTest.java
 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/lookup/StrSubstitutorTest.java
index 0f525e9..331f9b0 100644
--- 
a/log4j-core/src/test/java/org/apache/logging/log4j/core/lookup/StrSubstitutorTest.java
+++ 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/lookup/StrSubstitutorTest.java
@@ -20,15 +20,13 @@ import java.util.HashMap;
 import java.util.Map;
 
 import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.core.LogEvent;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
 import static org.junit.Assert.*;
 
-/**
- *
- */
 public class StrSubstitutorTest {
 
     private static final String TESTKEY = "TestKey";
@@ -65,4 +63,138 @@ public class StrSubstitutorTest {
         value = 
subst.replace("${BadKey:-Unknown}-${ctx:BadKey:-}-${sys:BadKey:-Unknown}");
         assertEquals("Unknown--Unknown", value);
     }
+
+    @Test
+    public void testDefault() {
+        final Map<String, String> map = new HashMap<String, String>();
+        map.put(TESTKEY, TESTVAL);
+        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrSubstitutor subst = new StrSubstitutor(lookup);
+        ThreadContext.put(TESTKEY, TESTVAL);
+        //String value = subst.replace("${sys:TestKey1:-${ctx:TestKey}}");
+        final String value = subst.replace("${sys:TestKey1:-${ctx:TestKey}}");
+        assertEquals("${ctx:TestKey}", value);
+    }
+
+    @Test
+    public void testDefaultReferencesLookupValue() {
+        final Map<String, String> map = new HashMap<String, String>();
+        map.put(TESTKEY, "${java:version}");
+        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrSubstitutor subst = new StrSubstitutor(lookup);
+        subst.setRecursiveEvaluationAllowed(false);
+        final String value = subst.replace("${sys:TestKey1:-${ctx:TestKey}}");
+        assertEquals("${ctx:TestKey}", value);
+    }
+
+    @Test
+    public void testInfiniteSubstitutionOnString() {
+        final StrLookup lookup = new Interpolator(new MapLookup(new 
HashMap<String, String>()));
+        final StrSubstitutor subst = new StrSubstitutor(lookup);
+        subst.setRecursiveEvaluationAllowed(true);
+        String infiniteSubstitution = "${${::-${::-$${::-j}}}}";
+        assertEquals(infiniteSubstitution, 
subst.replace(infiniteSubstitution));
+    }
+
+    @Test
+    public void testInfiniteSubstitutionOnStringBuilder() {
+        final StrLookup lookup = new Interpolator(new MapLookup(new 
HashMap<String, String>()));
+        final StrSubstitutor subst = new StrSubstitutor(lookup);
+        subst.setRecursiveEvaluationAllowed(true);
+        String infiniteSubstitution = "${${::-${::-$${::-j}}}}";
+        assertEquals(infiniteSubstitution, subst.replace(null, new 
StringBuilder(infiniteSubstitution)));
+    }
+
+    @Test
+    public void testRecursiveSubstitution() {
+        final Map<String, String> map = new HashMap<String, String>();
+        map.put("first", "${ctx:first}");
+        map.put("second", "secondValue");
+        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrSubstitutor subst = new StrSubstitutor(lookup);
+        subst.setRecursiveEvaluationAllowed(true);
+        assertEquals("${ctx:first} and secondValue", 
subst.replace("${ctx:first} and ${ctx:second}"));
+    }
+
+    @Test
+    public void testRecursiveWithDefault() {
+        final Map<String, String> map = new HashMap<String, String>();
+        map.put("first", "${ctx:first:-default}");
+        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrSubstitutor subst = new StrSubstitutor(lookup);
+        subst.setRecursiveEvaluationAllowed(true);
+        assertEquals("default", subst.replace("${ctx:first}"));
+    }
+
+    @Test
+    public void testRecursiveWithRecursiveDefault() {
+        final Map<String, String> map = new HashMap<String, String>();
+        map.put("first", "${ctx:first:-${ctx:first}}");
+        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrSubstitutor subst = new StrSubstitutor(lookup);
+        subst.setRecursiveEvaluationAllowed(true);
+        assertEquals("${ctx:first}", subst.replace("${ctx:first}"));
+    }
+
+    @Test
+    public void testNestedSelfReferenceWithRecursiveEvaluation() {
+        final Map<String, String> map = new HashMap<String, String>();
+        map.put("first", "${${ctx:first}}");
+        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrSubstitutor subst = new StrSubstitutor(lookup);
+        subst.setRecursiveEvaluationAllowed(true);
+        assertEquals("${${ctx:first}}}", subst.replace("${ctx:first}"));
+    }
+
+    @Test
+    public void testRandomWithRecursiveDefault() {
+        final Map<String, String> map = new HashMap<String, String>();
+        map.put("first", "${env:RANDOM:-${ctx:first}}");
+        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrSubstitutor subst = new StrSubstitutor(lookup);
+        subst.setRecursiveEvaluationAllowed(true);
+        assertEquals("${ctx:first}", subst.replace("${ctx:first}"));
+    }
+
+    @Test
+    public void testNoRecursiveEvaluationWithDefault() {
+        final Map<String, String> map = new HashMap<String, String>();
+        map.put("first", "${java:version}");
+        map.put("second", "${java:runtime}");
+        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrSubstitutor subst = new StrSubstitutor(lookup);
+        subst.setRecursiveEvaluationAllowed(false);
+        assertEquals("${java:version}}", 
subst.replace("${ctx:first:-${ctx:second}}"));
+    }
+
+    @Test
+    public void testNoRecursiveEvaluationWithDepthOne() {
+        final Map<String, String> map = new HashMap<String, String>();
+        map.put("first", "${java:version}");
+        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrSubstitutor subst = new StrSubstitutor(lookup);
+        subst.setRecursiveEvaluationAllowed(false);
+        assertEquals("${java:version}", subst.replace("${ctx:first}"));
+    }
+
+    @Test
+    public void testLookupThrows() {
+        final StrSubstitutor subst = new StrSubstitutor(new Interpolator(new 
StrLookup() {
+
+            @Override
+            public String lookup(String key) {
+                if ("throw".equals(key)) {
+                    throw new RuntimeException();
+                }
+                return "success";
+            }
+
+            @Override
+            public String lookup(LogEvent event, String key) {
+                return lookup(key);
+            }
+        }));
+        subst.setRecursiveEvaluationAllowed(false);
+        assertEquals("success ${foo:throw} success", subst.replace("${foo:a} 
${foo:throw} ${foo:c}"));
+    }
 }
diff --git a/log4j-core/src/test/resources/log4j-routing.xml 
b/log4j-core/src/test/resources/log4j-routing-lookup.xml
similarity index 51%
copy from log4j-core/src/test/resources/log4j-routing.xml
copy to log4j-core/src/test/resources/log4j-routing-lookup.xml
index 4d83886..33b8ec8 100644
--- a/log4j-core/src/test/resources/log4j-routing.xml
+++ b/log4j-core/src/test/resources/log4j-routing-lookup.xml
@@ -16,43 +16,23 @@
  limitations under the License.
 
 -->
-<Configuration status="OFF" name="RoutingTest">
-  <Properties>
-    <Property 
name="filename">target/routing1/routingtest-$${sd:type}.log</Property>
-  </Properties>
-  <ThresholdFilter level="debug"/>
-
+<Configuration status="OFF" name="RoutingAppenderKeyLookupEvaluationTest">
   <Appenders>
-    <Console name="STDOUT">
-      <PatternLayout pattern="%m%n"/>
-    </Console>
     <List name="List">
-      <ThresholdFilter level="debug"/>
+      <PatternLayout pattern="%p $${ctx:user} %m"/>
     </List>
     <Routing name="Routing">
-      <Routes pattern="$${sd:type}">
-        <Route>
-          <RollingFile name="Routing-${sd:type}" fileName="${filename}"
-                       
filePattern="target/routing1/test1-${sd:type}.%i.log.gz">
-            <PatternLayout>
-              <Pattern>%d %p %C{1.} [%t] %m%n</Pattern>
-            </PatternLayout>
-            <SizeBasedTriggeringPolicy size="500" />
-          </RollingFile>
-        </Route>
-        <Route ref="STDOUT" key="Audit"/>
-        <Route ref="List" key="Service"/>
+      <Routes pattern="$${ctx:user:-none}">
+        <Route ref="List" key="$${upper:name}"/>
+        <Route ref="List" key="none"/>
+        <Route ref="List" key="$${java:version}"/>
       </Routes>
     </Routing>
   </Appenders>
 
   <Loggers>
-    <Logger name="EventLogger" level="info" additivity="false">
+    <Root level="debug">
       <AppenderRef ref="Routing"/>
-    </Logger>
-
-    <Root level="error">
-      <AppenderRef ref="STDOUT"/>
     </Root>
   </Loggers>
 
diff --git a/log4j-core/src/test/resources/log4j-routing.json 
b/log4j-core/src/test/resources/log4j-routing.json
index 809b3c2..4322d8d 100644
--- a/log4j-core/src/test/resources/log4j-routing.json
+++ b/log4j-core/src/test/resources/log4j-routing.json
@@ -15,9 +15,6 @@
  * limitations under the license.
  */
 { "configuration": { "status": "error", "name": "RoutingTest",
-      "properties": {
-        "property": { "name": "filename", "value" : 
"target/rolling1/rollingtest-$${sd:type}.log" }
-      },
     "ThresholdFilter": { "level": "debug" },
     "appenders": {
       "Console": { "name": "STDOUT",
@@ -31,7 +28,7 @@
           "Route": [
             {
               "RollingFile": {
-                "name": "Rolling-${sd:type}", "fileName": "${filename}",
+                "name": "Rolling-${sd:type}", "fileName": 
"target/rolling1/rollingtest-${sd:type}.log",
                 "filePattern": "target/rolling1/test1-${sd:type}.%i.log.gz",
                 "PatternLayout": {"pattern": "%d %p %C{1.} [%t] %m%n"},
                 "SizeBasedTriggeringPolicy": { "size": "500" }
diff --git a/log4j-core/src/test/resources/log4j-routing.xml 
b/log4j-core/src/test/resources/log4j-routing.xml
index 4d83886..003f7d7 100644
--- a/log4j-core/src/test/resources/log4j-routing.xml
+++ b/log4j-core/src/test/resources/log4j-routing.xml
@@ -17,9 +17,6 @@
 
 -->
 <Configuration status="OFF" name="RoutingTest">
-  <Properties>
-    <Property 
name="filename">target/routing1/routingtest-$${sd:type}.log</Property>
-  </Properties>
   <ThresholdFilter level="debug"/>
 
   <Appenders>
@@ -32,7 +29,7 @@
     <Routing name="Routing">
       <Routes pattern="$${sd:type}">
         <Route>
-          <RollingFile name="Routing-${sd:type}" fileName="${filename}"
+          <RollingFile name="Routing-${sd:type}" 
fileName="target/routing1/routingtest-${sd:type}.log"
                        
filePattern="target/routing1/test1-${sd:type}.%i.log.gz">
             <PatternLayout>
               <Pattern>%d %p %C{1.} [%t] %m%n</Pattern>
diff --git a/log4j-core/src/test/resources/log4j-routing2.json 
b/log4j-core/src/test/resources/log4j-routing2.json
index fe50b37..af67454 100644
--- a/log4j-core/src/test/resources/log4j-routing2.json
+++ b/log4j-core/src/test/resources/log4j-routing2.json
@@ -15,9 +15,6 @@
  * limitations under the license.
  */
 { "configuration": { "status": "error", "name": "RoutingTest",
-      "properties": {
-        "property": { "name": "filename", "value" : 
"target/rolling1/rollingtest-$${sd:type}.log" }
-      },
     "ThresholdFilter": { "level": "debug" },
     "appenders": {
       "appender": [
@@ -28,7 +25,7 @@
             "Route": [
               {
                 "RollingFile": {
-                  "name": "Rolling-${sd:type}", "fileName": "${filename}",
+                  "name": "Rolling-${sd:type}", "fileName": 
"target/rolling1/rollingtest-${sd:type}.log",
                   "filePattern": "target/rolling1/test1-${sd:type}.%i.log.gz",
                   "PatternLayout": {"pattern": "%d %p %C{1.} [%t] %m%n"},
                   "SizeBasedTriggeringPolicy": { "size": "500" }
diff --git 
a/log4j-core/src/test/resources/log4j2-pattern-layout-with-context.xml 
b/log4j-core/src/test/resources/log4j2-pattern-layout-with-context.xml
new file mode 100644
index 0000000..e644d26
--- /dev/null
+++ b/log4j-core/src/test/resources/log4j2-pattern-layout-with-context.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<Configuration status="DEBUG" packages="">
+       <Properties>
+               <Property name="pattern">%p %c $${ctx:user} %m</Property>
+       </Properties>
+       <Appenders>
+               <List name="list">
+                       <PatternLayout pattern="${pattern}"/>
+               </List>
+       </Appenders>
+
+       <Loggers>
+               <Root level="INFO">
+                       <AppenderRef ref="list"/>
+               </Root>
+       </Loggers>
+</Configuration>
diff --git 
a/log4j-web/src/main/java/org/apache/logging/log4j/web/Log4jWebInitializerImpl.java
 
b/log4j-web/src/main/java/org/apache/logging/log4j/web/Log4jWebInitializerImpl.java
index 8a67ce2..8eda0a4 100644
--- 
a/log4j-web/src/main/java/org/apache/logging/log4j/web/Log4jWebInitializerImpl.java
+++ 
b/log4j-web/src/main/java/org/apache/logging/log4j/web/Log4jWebInitializerImpl.java
@@ -29,6 +29,7 @@ import org.apache.logging.log4j.core.LoggerContext;
 import org.apache.logging.log4j.core.config.Configurator;
 import org.apache.logging.log4j.core.impl.ContextAnchor;
 import org.apache.logging.log4j.core.impl.Log4jContextFactory;
+import org.apache.logging.log4j.core.lookup.ConfigurationStrSubstitutor;
 import org.apache.logging.log4j.core.lookup.Interpolator;
 import org.apache.logging.log4j.core.lookup.StrSubstitutor;
 import org.apache.logging.log4j.core.selector.ContextSelector;
@@ -58,7 +59,7 @@ final class Log4jWebInitializerImpl extends AbstractLifeCycle 
implements Log4jWe
     }
 
     private final Map<String, String> map = new ConcurrentHashMap<String, 
String>();
-    private final StrSubstitutor substitutor = new StrSubstitutor(new 
Interpolator(map));
+    private final StrSubstitutor substitutor = new 
ConfigurationStrSubstitutor(new Interpolator(map));
     private final ServletContext servletContext;
 
     private String name;
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 7d6508e..3905ae3 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -24,6 +24,9 @@
   </properties>
   <body>
     <release version="2.3.1" date="2021-12-xx" description="GA Release 2.3.1">
+      <action issue="LOG4J2-3230" dev="ckozak" type="fix">
+        Fix string substitution recursion.
+      </action>
       <action issue="LOG4J2-3198" dev="rgoers" type="add">
         Pattern layout no longer enables lookups within message text.
       </action>

Reply via email to