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

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


The following commit(s) were added to refs/heads/release-2.x by this push:
     new 17a77f5  LOG4J2-3317: Fix RoutingAppender backcompat and improve 
lookup security
17a77f5 is described below

commit 17a77f5c27bda545de4b8eda46590401313e0be8
Author: Carter Kozak <[email protected]>
AuthorDate: Wed Jan 19 18:12:49 2022 -0500

    LOG4J2-3317: Fix RoutingAppender backcompat and improve lookup security
    
    This change unifies our configuration-time and runtime property
    handling logic in order to make configuration-time properties
    safer and more obvious, while allowing RoutingAppender configurations
    that were broken by 2.17.1 to work as expected once again.
    
    This is done using two high-level ideas:
    1. Properties defined in the configuration are aware of lookups,
       so the lookup-engine may resolve properties defined in the
       top level logging configuraton recursively. This applies only
       to the property literal, not the result of lookups used within
       configuration properties.
    2. No recursive evaluation of lookup results. While properties are
       a special case, the data returned from other sources must not
       itself be evaluated. This matches the restriction we introduced
       in 2.17.1 for runtime lookups, however it applies everywhere
       and shouldn't result in breaks due to property traversal being
       allowed.
    
    This introduces some level of risk, unlike the changes
    in 2.17.1, this update the behavior of all lookups and is not
    bounded to less-common runtime lookups.
    We do not expect this to break users configurations, however
    there are some cases in which we have intentionally changed
    behavior:
    
    Given environment `FOO=${lower:BAR}`
    Using `<File name="${env:FOO}">` in a configuration will result
    in a File appender with the literal name `${lower:BAR}` where
    it would have previously evaluated to `bar`. This resolves an
    ambiguity problem where those setting environment variables
    don't need to be aware of log4j2 or our lookup syntax.
---
 log4j-core/revapi.json                             |  27 ++++
 .../core/appender/HttpURLConnectionManager.java    |   2 +-
 .../logging/log4j/core/async/AsyncLogger.java      |   4 +-
 .../logging/log4j/core/config/LoggerConfig.java    |   6 +-
 .../log4j/core/config/PropertiesPlugin.java        |  67 +++++++--
 .../apache/logging/log4j/core/config/Property.java |  43 +++++-
 .../log4j/core/config/plugins/PluginValue.java     |   3 +
 .../plugins/visitors/PluginValueVisitor.java       |   4 +-
 .../logging/log4j/core/lookup/AbstractLookup.java  |  12 +-
 .../core/lookup/ConfigurationStrSubstitutor.java   |  11 --
 ...bstractLookup.java => DefaultLookupResult.java} |  35 +++--
 .../logging/log4j/core/lookup/Interpolator.java    |  25 +++-
 .../{AbstractLookup.java => LookupResult.java}     |  23 ++-
 .../log4j/core/lookup/PropertiesLookup.java        | 126 +++++++++++++---
 .../log4j/core/lookup/RuntimeStrSubstitutor.java   |  11 --
 .../logging/log4j/core/lookup/StrLookup.java       |  22 +++
 .../logging/log4j/core/lookup/StrSubstitutor.java  |  20 +--
 .../appender/routing/RoutingAppender3350Test.java  |  65 ++++++++
 .../PropertiesPluginTest.java}                     |  23 ++-
 .../log4j/core/lookup/PropertiesLookupTest.java    |  70 ++++++++-
 .../log4j/core/lookup/StrSubstitutorTest.java      | 166 ++++++++++++++++-----
 .../src/test/resources/log4j-routing-2767.xml      |   6 +-
 .../src/test/resources/log4j-routing-purge.xml     |   8 +-
 log4j-core/src/test/resources/log4j-routing.json   |   5 +-
 .../src/test/resources/log4j-routing.properties    |   4 +-
 log4j-core/src/test/resources/log4j-routing.xml    |   5 +-
 log4j-core/src/test/resources/log4j-routing2.json  |   5 +-
 ...og4j-routing-2767.xml => log4j-routing3350.xml} |  22 +--
 src/changes/changes.xml                            |   3 +
 29 files changed, 652 insertions(+), 171 deletions(-)

diff --git a/log4j-core/revapi.json b/log4j-core/revapi.json
index a4ff02a..10e586d 100644
--- a/log4j-core/revapi.json
+++ b/log4j-core/revapi.json
@@ -49,6 +49,33 @@
         "old": "method void 
org.apache.logging.log4j.core.script.ScriptManager::addScript(org.apache.logging.log4j.core.script.AbstractScript)",
         "new": "method boolean 
org.apache.logging.log4j.core.script.ScriptManager::addScript(org.apache.logging.log4j.core.script.AbstractScript)",
         "justification": "LOG4J2-2486 - Require enabled script languages to be 
specified in a system property"
+      },
+      {
+        "code": "java.method.returnTypeChanged",
+        "old": "method java.lang.String 
org.apache.logging.log4j.core.lookup.StrSubstitutor::resolveVariable(org.apache.logging.log4j.core.LogEvent,
 java.lang.String, java.lang.StringBuilder, int, int)",
+        "new": "method org.apache.logging.log4j.core.lookup.LookupResult 
org.apache.logging.log4j.core.lookup.StrSubstitutor::resolveVariable(org.apache.logging.log4j.core.LogEvent,
 java.lang.String, java.lang.StringBuilder, int, int)",
+        "justification": "LOG4J2-3317: Fix RoutingAppender backcompat while 
improving lookup security"
+      },
+      {
+        "code": "java.annotation.removed",
+        "old": "parameter org.apache.logging.log4j.core.config.Property 
org.apache.logging.log4j.core.config.Property::createProperty(===java.lang.String===,
 java.lang.String)",
+        "new": "parameter org.apache.logging.log4j.core.config.Property 
org.apache.logging.log4j.core.config.Property::createProperty(===java.lang.String===,
 java.lang.String)",
+        "annotation": 
"@org.apache.logging.log4j.core.config.plugins.PluginAttribute(\"name\")",
+        "justification": "LOG4J2-3317: Fix RoutingAppender backcompat while 
improving lookup security"
+      },
+      {
+        "code": "java.annotation.removed",
+        "old": "parameter org.apache.logging.log4j.core.config.Property 
org.apache.logging.log4j.core.config.Property::createProperty(java.lang.String, 
===java.lang.String===)",
+        "new": "parameter org.apache.logging.log4j.core.config.Property 
org.apache.logging.log4j.core.config.Property::createProperty(java.lang.String, 
===java.lang.String===)",
+        "annotation": 
"@org.apache.logging.log4j.core.config.plugins.PluginValue(\"value\")",
+        "justification": "LOG4J2-3317: Fix RoutingAppender backcompat while 
improving lookup security"
+      },
+      {
+        "code": "java.annotation.removed",
+        "old": "method org.apache.logging.log4j.core.config.Property 
org.apache.logging.log4j.core.config.Property::createProperty(java.lang.String, 
java.lang.String)",
+        "new": "method org.apache.logging.log4j.core.config.Property 
org.apache.logging.log4j.core.config.Property::createProperty(java.lang.String, 
java.lang.String)",
+        "annotation": 
"@org.apache.logging.log4j.core.config.plugins.PluginFactory",
+        "justification": "LOG4J2-3317: Fix RoutingAppender backcompat while 
improving lookup security"
       }
     ]
   }
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/HttpURLConnectionManager.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/HttpURLConnectionManager.java
index 55ccfdc..7d702f0 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/HttpURLConnectionManager.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/HttpURLConnectionManager.java
@@ -92,7 +92,7 @@ public class HttpURLConnectionManager extends HttpManager {
         for (final Property header : headers) {
             urlConnection.setRequestProperty(
                 header.getName(),
-                header.isValueNeedsLookup() ? 
getConfiguration().getStrSubstitutor().replace(event, header.getValue()) : 
header.getValue());
+                header.evaluate(getConfiguration().getStrSubstitutor()));
         }
         if (sslConfiguration != null) {
             
((HttpsURLConnection)urlConnection).setSSLSocketFactory(sslConfiguration.getSslSocketFactory());
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java
index 9d48c8a..f7c4173 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java
@@ -496,9 +496,7 @@ public class AsyncLogger extends Logger implements 
EventTranslatorVararg<RingBuf
             if (contextData.getValue(prop.getName()) != null) {
                 continue; // contextMap overrides config properties
             }
-            final String value = prop.isValueNeedsLookup() //
-                    ? privateConfig.config.getStrSubstitutor().replace(event, 
prop.getValue()) //
-                    : prop.getValue();
+            final String value = 
prop.evaluate(privateConfig.config.getStrSubstitutor());
             contextData.putValue(prop.getName(), value);
         }
         event.setContextData(contextData);
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/LoggerConfig.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/LoggerConfig.java
index a3bf60a..d7d7ce6 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/LoggerConfig.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/LoggerConfig.java
@@ -454,10 +454,8 @@ public class LoggerConfig extends AbstractFilterable 
implements LocationAware {
                 .build();
         for (int i = 0; i < props.size(); i++) {
             final Property prop = props.get(i);
-            final String value = prop.isValueNeedsLookup() // since LOG4J2-1575
-                    ? config.getStrSubstitutor().replace(event, 
prop.getValue()) //
-                    : prop.getValue();
-            results.add(Property.createProperty(prop.getName(), value));
+            final String value = prop.evaluate(config.getStrSubstitutor()); // 
since LOG4J2-1575
+            results.add(Property.createProperty(prop.getName(), 
prop.getRawValue(), value));
         }
         return results;
     }
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/PropertiesPlugin.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/PropertiesPlugin.java
index 7c0d770..51fcd69 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/PropertiesPlugin.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/PropertiesPlugin.java
@@ -16,16 +16,16 @@
  */
 package org.apache.logging.log4j.core.config;
 
-import java.util.HashMap;
-import java.util.Map;
-
+import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.config.plugins.Plugin;
 import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
 import org.apache.logging.log4j.core.config.plugins.PluginElement;
 import org.apache.logging.log4j.core.config.plugins.PluginFactory;
 import org.apache.logging.log4j.core.lookup.Interpolator;
+import org.apache.logging.log4j.core.lookup.LookupResult;
 import org.apache.logging.log4j.core.lookup.PropertiesLookup;
 import org.apache.logging.log4j.core.lookup.StrLookup;
+import org.apache.logging.log4j.core.lookup.StrSubstitutor;
 
 /**
  * Handles properties defined in the configuration.
@@ -33,6 +33,8 @@ import org.apache.logging.log4j.core.lookup.StrLookup;
 @Plugin(name = "properties", category = Node.CATEGORY, printObject = true)
 public final class PropertiesPlugin {
 
+    private static final StrSubstitutor UNESCAPING_SUBSTITUTOR = 
createUnescapingSubstitutor();
+
     private PropertiesPlugin() {
     }
 
@@ -45,15 +47,62 @@ public final class PropertiesPlugin {
     @PluginFactory
     public static StrLookup configureSubstitutor(@PluginElement("Properties") 
final Property[] properties,
                                                  @PluginConfiguration final 
Configuration config) {
-        if (properties == null) {
-            return new Interpolator(config.getProperties());
+        // For backwards compatibility, we unescape all escaped lookups when 
properties are parsed.
+        // This matches previous behavior for escaped components which were 
meant to be executed later on.
+        Property[] unescapedProperties = new Property[properties == null ? 0 : 
properties.length];
+        for (int i = 0; i < unescapedProperties.length; i++) {
+            unescapedProperties[i] = unescape(properties[i]);
         }
-        final Map<String, String> map = new HashMap<>(config.getProperties());
+        return new Interpolator(
+                new PropertiesLookup(unescapedProperties, 
config.getProperties()),
+                config.getPluginPackages());
+    }
+
+    private static Property unescape(Property input) {
+        return Property.createProperty(
+                input.getName(),
+                unescape(input.getRawValue()),
+                input.getValue());
+    }
 
-        for (final Property prop : properties) {
-            map.put(prop.getName(), prop.getValue());
+    // Visible for testing
+    static String unescape(String input) {
+        return UNESCAPING_SUBSTITUTOR.replace(input);
+    }
+
+    /**
+     * Creates a new {@link StrSubstitutor} which is configured with no 
lookups and does not handle
+     * defaults. This allows it to unescape one level of escaped lookups 
without any further processing
+     * or removing replacing {@code ${ctx:foo:-default}} with {@code default}.
+     */
+    private static StrSubstitutor createUnescapingSubstitutor() {
+        StrSubstitutor substitutor = new StrSubstitutor(NullLookup.INSTANCE);
+        substitutor.setValueDelimiter(null);
+        substitutor.setValueDelimiterMatcher(null);
+        return substitutor;
+    }
+
+    private enum NullLookup implements StrLookup {
+        INSTANCE;
+
+        @Override
+        public String lookup(String key) {
+            return null;
+        }
+
+        @Override
+        public String lookup(LogEvent event, String key) {
+            return null;
+        }
+
+        @Override
+        public LookupResult evaluate(String key) {
+            return null;
         }
 
-        return new Interpolator(new PropertiesLookup(map), 
config.getPluginPackages());
+        @Override
+        public LookupResult evaluate(LogEvent event, String key) {
+            return null;
+        }
     }
 }
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/Property.java 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/Property.java
index 076ae44..fe320ef 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/Property.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/Property.java
@@ -23,6 +23,7 @@ import org.apache.logging.log4j.core.config.plugins.Plugin;
 import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
 import org.apache.logging.log4j.core.config.plugins.PluginFactory;
 import org.apache.logging.log4j.core.config.plugins.PluginValue;
+import org.apache.logging.log4j.core.lookup.StrSubstitutor;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.Strings;
 
@@ -40,11 +41,13 @@ public final class Property {
     private static final Logger LOGGER = StatusLogger.getLogger();
 
     private final String name;
+    private final String rawValue;
     private final String value;
     private final boolean valueNeedsLookup;
 
-    private Property(final String name, final String value) {
+    private Property(final String name, final String rawValue, final String 
value) {
         this.name = name;
+        this.rawValue = rawValue;
         this.value = value;
         this.valueNeedsLookup = value != null && value.contains("${");
     }
@@ -58,6 +61,14 @@ public final class Property {
     }
 
     /**
+     * Returns the original raw property value without substitution.
+     * @return the raw value of the property, or empty string if it is not set.
+     */
+    public String getRawValue() {
+        return Objects.toString(rawValue, Strings.EMPTY);
+    }
+
+    /**
      * Returns the property value.
      * @return the value of the property.
      */
@@ -74,20 +85,48 @@ public final class Property {
     }
 
     /**
+     * Evaluate this property with the provided substitutor. If {@link 
#isValueNeedsLookup()} is {@code false},
+     * the {@link #getValue() value} is returned, otherwise the {@link 
#getRawValue() raw value} is evaluated
+     * with the given substitutor.
+     */
+    public String evaluate(StrSubstitutor substitutor) {
+        return valueNeedsLookup
+                // Unescape the raw value first, handling '$${ctx:foo}' -> 
'${ctx:foo}'
+                ? substitutor.replace(PropertiesPlugin.unescape(getRawValue()))
+                : getValue();
+    }
+
+    /**
+     * Creates a Property.
+     *
+     * @param name The key.
+     * @param value The value.
+     * @return A Property.
+     */
+    public static Property createProperty(final String name, final String 
value) {
+        if (name == null) {
+            LOGGER.error("Property name cannot be null");
+        }
+        return new Property(name, value, value);
+    }
+
+    /**
      * Creates a Property.
      *
      * @param name The key.
+     * @param rawValue The value without any substitution applied.
      * @param value The value.
      * @return A Property.
      */
     @PluginFactory
     public static Property createProperty(
             @PluginAttribute("name") final String name,
+            @PluginValue(value = "value", substitute = false) final String 
rawValue,
             @PluginValue("value") final String value) {
         if (name == null) {
             LOGGER.error("Property name cannot be null");
         }
-        return new Property(name, value);
+        return new Property(name, rawValue, value);
     }
 
     @Override
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/PluginValue.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/PluginValue.java
index 9c20cc2..56d664f 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/PluginValue.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/PluginValue.java
@@ -37,4 +37,7 @@ import 
org.apache.logging.log4j.core.config.plugins.visitors.PluginValueVisitor;
 public @interface PluginValue {
 
     String value();
+
+    /** If false, standard configuration value substitution is not done on the 
referenced value. */
+    boolean substitute() default true;
 }
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/visitors/PluginValueVisitor.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/visitors/PluginValueVisitor.java
index d2a49d5..fe848d8 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/visitors/PluginValueVisitor.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/visitors/PluginValueVisitor.java
@@ -49,7 +49,9 @@ public class PluginValueVisitor extends 
AbstractPluginVisitor<PluginValue> {
         } else {
             rawValue = removeAttributeValue(node.getAttributes(), name);
         }
-        final String value = this.substitutor.replace(event, rawValue);
+        final String value = this.annotation.substitute()
+                ? this.substitutor.replace(event, rawValue)
+                : rawValue;
         StringBuilders.appendKeyDqValue(log, name, value);
         return value;
     }
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/AbstractLookup.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/AbstractLookup.java
index c76561c..500116b 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/AbstractLookup.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/AbstractLookup.java
@@ -24,7 +24,7 @@ package org.apache.logging.log4j.core.lookup;
 public abstract class AbstractLookup implements StrLookup {
 
     /**
-     * Calls {@code lookup(null, key)} in the super class.
+     * Calls {@code lookup(null, key)} in the implementation.
      *
      * @see StrLookup#lookup(LogEvent, String)
      */
@@ -33,4 +33,14 @@ public abstract class AbstractLookup implements StrLookup {
         return lookup(null, key);
     }
 
+    /**
+     * Calls {@code evaluate(null, key)} in the implementation.
+     *
+     * @see StrLookup#evaluate(LogEvent, String)
+     */
+    @Override
+    public LookupResult evaluate(final String key) {
+        return evaluate(null, 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
index 1287721..6c4d20a 100644
--- 
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
@@ -46,17 +46,6 @@ public final class ConfigurationStrSubstitutor extends 
StrSubstitutor {
     }
 
     @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/AbstractLookup.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/DefaultLookupResult.java
similarity index 60%
copy from 
log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/AbstractLookup.java
copy to 
log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/DefaultLookupResult.java
index c76561c..580c538 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/AbstractLookup.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/DefaultLookupResult.java
@@ -14,23 +14,32 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
+
 package org.apache.logging.log4j.core.lookup;
 
-/**
- * A default lookup for others to extend.
- *
- * @since 2.1
- */
-public abstract class AbstractLookup implements StrLookup {
+import java.util.Objects;
+
+/** Default internal implementation of {@link LookupResult}. */
+final class DefaultLookupResult implements LookupResult {
+
+    private final String value;
+
+    DefaultLookupResult(String value) {
+        this.value = Objects.requireNonNull(value, "value is required");
+    }
+
+    @Override
+    public String value() {
+        return value;
+    }
 
-    /**
-     * Calls {@code lookup(null, key)} in the super class.
-     *
-     * @see StrLookup#lookup(LogEvent, String)
-     */
     @Override
-    public String lookup(final String key) {
-        return lookup(null, key);
+    public boolean isLookupEvaluationAllowedInValue() {
+        return false;
     }
 
+    @Override
+    public String toString() {
+        return "DefaultLookupResult{value='" + value + "'}";
+    }
 }
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 86a742e..44c72b7 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
@@ -154,6 +154,25 @@ public class Interpolator extends 
AbstractConfigurationAwareLookup {
      */
     @Override
     public String lookup(final LogEvent event, String var) {
+        LookupResult result = evaluate(event, var);
+        return result == null ? null : result.value();
+    }
+
+    /**
+     * Resolves the specified variable. This implementation will try to extract
+     * a variable prefix from the given variable name (the first colon (':') is
+     * used as prefix separator). It then passes the name of the variable with
+     * the prefix stripped to the lookup object registered for this prefix. If
+     * no prefix can be found or if the associated lookup object cannot resolve
+     * this variable, the default lookup object will be used.
+     *
+     * @param event The current LogEvent or null.
+     * @param var the name of the variable whose value is to be looked up
+     * @return the value of this variable or <b>null</b> if it cannot be
+     * resolved
+     */
+    @Override
+    public LookupResult evaluate(final LogEvent event, String var) {
         if (var == null) {
             return null;
         }
@@ -166,9 +185,9 @@ public class Interpolator extends 
AbstractConfigurationAwareLookup {
             if (lookup instanceof ConfigurationAware) {
                 ((ConfigurationAware) lookup).setConfiguration(configuration);
             }
-            String value = null;
+            LookupResult value = null;
             if (lookup != null) {
-                value = event == null ? lookup.lookup(name) : 
lookup.lookup(event, name);
+                value = event == null ? lookup.evaluate(name) : 
lookup.evaluate(event, name);
             }
 
             if (value != null) {
@@ -177,7 +196,7 @@ public class Interpolator extends 
AbstractConfigurationAwareLookup {
             var = var.substring(prefixPos + 1);
         }
         if (defaultLookup != null) {
-            return event == null ? defaultLookup.lookup(var) : 
defaultLookup.lookup(event, var);
+            return event == null ? defaultLookup.evaluate(var) : 
defaultLookup.evaluate(event, var);
         }
         return null;
     }
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/AbstractLookup.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/LookupResult.java
similarity index 59%
copy from 
log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/AbstractLookup.java
copy to 
log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/LookupResult.java
index c76561c..caa82e4 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/AbstractLookup.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/LookupResult.java
@@ -14,23 +14,22 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
+
 package org.apache.logging.log4j.core.lookup;
 
-/**
- * A default lookup for others to extend.
- *
- * @since 2.1
- */
-public abstract class AbstractLookup implements StrLookup {
+public interface LookupResult {
+
+    /** Value of the lookup result. Never null. */
+    String value();
 
     /**
-     * Calls {@code lookup(null, key)} in the super class.
-     *
-     * @see StrLookup#lookup(LogEvent, String)
+     * True if the {@link #value()} should be re-evaluated for other lookups.
+     * This is used by {@link PropertiesLookup} to allow properties to be 
evaluated against other properties,
+     * because the configuration properties are completely trusted and 
designed with lookups in mind. It is
+     * unsafe to return true in most cases because it may allow unintended 
lookups to evaluate other lookups.
      */
-    @Override
-    public String lookup(final String key) {
-        return lookup(null, key);
+    default boolean isLookupEvaluationAllowedInValue() {
+        return false;
     }
 
 }
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/PropertiesLookup.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/PropertiesLookup.java
index 3066792..a17846b 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/PropertiesLookup.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/PropertiesLookup.java
@@ -17,9 +17,12 @@
 package org.apache.logging.log4j.core.lookup;
 
 import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.Property;
 
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
+import java.util.Objects;
 
 /**
  * A lookup designed for {@code Properties} defined in the configuration. This 
is similar
@@ -29,29 +32,28 @@ import java.util.Map;
  */
 public final class PropertiesLookup implements StrLookup {
 
-    /**
-     * Configuration from which to read properties.
-     */
-    private final Map<String, String> properties;
+    /** Logger context properties. */
+    private final Map<String, String> contextProperties;
 
-    /**
-     * Constructs a new instance for the given map.
-     *
-     * @param properties map these.
-     */
-    public PropertiesLookup(final Map<String, String> properties) {
-        this.properties = properties == null
+    /** Configuration properties. */
+    private final Map<String, ConfigurationPropertyResult> 
configurationProperties;
+
+    public PropertiesLookup(final Property[] configProperties, final 
Map<String, String> contextProperties) {
+        this.contextProperties = contextProperties == null
                 ? Collections.emptyMap()
-                : properties;
+                : contextProperties;
+        this.configurationProperties = configProperties == null
+                ? Collections.emptyMap()
+                : createConfigurationPropertyMap(configProperties);
     }
 
     /**
-     * Gets the property map.
+     * Constructs a new instance for the given map.
      *
-     * @return the property map.
+     * @param properties map these.
      */
-    public Map<String, String> getProperties() {
-        return properties;
+    public PropertiesLookup(final Map<String, String> properties) {
+        this(Property.EMPTY_ARRAY, properties);
     }
 
     @Override
@@ -70,12 +72,100 @@ public final class PropertiesLookup implements StrLookup {
      */
     @Override
     public String lookup(final String key) {
-        return key == null ? null : properties.get(key);
+        LookupResult result = evaluate(key);
+        return result == null ? null : result.value();
+    }
+
+    @Override
+    public LookupResult evaluate(String key) {
+        if (key == null) {
+            return null;
+        }
+        LookupResult configResult = configurationProperties.get(key);
+        if (configResult != null) {
+            return configResult;
+        }
+        // Allow the context map to be mutated after this lookup has been 
initialized.
+        String contextResult = contextProperties.get(key);
+        return contextResult == null ? null : new 
ContextPropertyResult(contextResult);
+    }
+
+    @Override
+    public LookupResult evaluate(@SuppressWarnings("ignored") final LogEvent 
event, final String key) {
+        return evaluate(key);
     }
 
     @Override
     public String toString() {
-        return "PropertiesLookup{properties=" + properties + '}';
+        return "PropertiesLookup{" +
+                "contextProperties=" + contextProperties +
+                ", configurationProperties=" + configurationProperties +
+                '}';
     }
 
+    private static Map<String, ConfigurationPropertyResult> 
createConfigurationPropertyMap(Property[] props) {
+        // The raw property values must be used without the substitution 
handled by the plugin framework
+        // which calls this method, otherwise we risk re-interpolating through 
unexpected data.
+        // The PropertiesLookup is unique in that results from this lookup 
support recursive evaluation.
+        Map<String, ConfigurationPropertyResult> result = new 
HashMap<>(props.length);
+        for (Property property : props) {
+            result.put(property.getName(), new 
ConfigurationPropertyResult(property.getRawValue()));
+        }
+        return result;
+    }
+
+    private static final class ConfigurationPropertyResult implements 
LookupResult {
+
+        private final String value;
+
+        ConfigurationPropertyResult(String value) {
+            this.value = Objects.requireNonNull(value, "value is required");
+        }
+
+        @Override
+        public String value() {
+            return value;
+        }
+
+        /**
+         * Properties are a special case in which lookups contained
+         * within the properties map are allowed for recursive evaluation.
+         */
+        @Override
+        public boolean isLookupEvaluationAllowedInValue() {
+            return true;
+        }
+
+        @Override
+        public String toString() {
+            return "ConfigurationPropertyResult{'" + value + "'}";
+        }
+    }
+
+    private static final class ContextPropertyResult implements LookupResult {
+
+        private final String value;
+
+        ContextPropertyResult(String value) {
+            this.value = Objects.requireNonNull(value, "value is required");
+        }
+
+        @Override
+        public String value() {
+            return value;
+        }
+
+        /**
+         * Unlike configuration properties, context properties are not built 
around lookup syntax.
+         */
+        @Override
+        public boolean isLookupEvaluationAllowedInValue() {
+            return false;
+        }
+
+        @Override
+        public String toString() {
+            return "ContextPropertyResult{'" + value + "'}";
+        }
+    }
 }
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
index f002c6e..6000113 100644
--- 
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
@@ -44,17 +44,6 @@ public final class RuntimeStrSubstitutor extends 
StrSubstitutor {
     }
 
     @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/StrLookup.java 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/StrLookup.java
index e29d280..86daa46 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/StrLookup.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/StrLookup.java
@@ -99,4 +99,26 @@ public interface StrLookup {
      * @return the matching value, null if no match
      */
     String lookup(LogEvent event, String key);
+
+    /**
+     * Same as {@link #lookup(String)}, but provides additional metadata 
describing the result.
+     * Returns null if the key cannot be evaluated, otherwise a {@link 
LookupResult} wrapping the non-null string value.
+     */
+    default LookupResult evaluate(String key) {
+        String value = lookup(key);
+        return value == null
+                ? null
+                : new DefaultLookupResult(value);
+    }
+
+    /**
+     * Same as {@link #lookup(LogEvent, String)}, but provides additional 
metadata describing the result.
+     * Returns null if the key cannot be evaluated, otherwise a {@link 
LookupResult} wrapping the non-null string value.
+     */
+    default LookupResult evaluate(LogEvent event, String key) {
+        String value = lookup(event, key);
+        return value == null
+                ? null
+                : new DefaultLookupResult(value);
+    }
 }
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 2f7c95c..7e78d04 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
@@ -209,8 +209,6 @@ public class StrSubstitutor implements ConfigurationAware {
      */
     private Configuration configuration;
 
-    private boolean recursiveEvaluationAllowed;
-
     //-----------------------------------------------------------------------
     /**
      * Creates a new instance with defaults for variable prefix and suffix
@@ -392,7 +390,6 @@ public class StrSubstitutor implements ConfigurationAware {
         this.setValueDelimiterMatcher(other.valueDelimiterMatcher);
         this.valueEscapeDelimiterMatcher = other.valueEscapeDelimiterMatcher;
         this.configuration = other.configuration;
-        this.recursiveEvaluationAllowed = other.isRecursiveEvaluationAllowed();
         this.enableSubstitutionInVariables = 
other.isEnableSubstitutionInVariables();
         this.valueDelimiterString = other.valueDelimiterString;
     }
@@ -1098,7 +1095,8 @@ public class StrSubstitutor implements ConfigurationAware 
{
                             boolean isCyclic = isCyclicSubstitution(varName, 
priorVariables);
 
                             // resolve the variable
-                            String varValue = isCyclic ? null : 
resolveVariable(event, varName, buf, startPos, endPos);
+                            LookupResult resolvedResult = isCyclic ? null : 
resolveVariable(event, varName, buf, startPos, endPos);
+                            String varValue = resolvedResult == null ? null : 
resolvedResult.value();
                             if (varValue == null) {
                                 varValue = varDefaultValue;
                             }
@@ -1107,7 +1105,7 @@ public class StrSubstitutor implements ConfigurationAware 
{
                                 final int varLen = varValue.length();
                                 buf.replace(startPos, endPos, varValue);
                                 altered = true;
-                                int change = isRecursiveEvaluationAllowed()
+                                int change = resolvedResult != null && 
resolvedResult.isLookupEvaluationAllowedInValue()
                                         ? substitute(event, buf, startPos, 
varLen, priorVariables)
                                         : 0;
                                 change = change + (varLen - (endPos - 
startPos));
@@ -1175,14 +1173,14 @@ public class StrSubstitutor implements 
ConfigurationAware {
      * @param endPos  the end position of the variable including the suffix, 
valid
      * @return the variable's value or <b>null</b> if the variable is unknown
      */
-    protected String resolveVariable(final LogEvent event, final String 
variableName, final StringBuilder buf,
+    protected LookupResult resolveVariable(final LogEvent event, final String 
variableName, final StringBuilder buf,
                                      final int startPos, final int endPos) {
         final StrLookup resolver = getVariableResolver();
         if (resolver == null) {
             return null;
         }
         try {
-            return resolver.lookup(event, variableName);
+            return resolver.evaluate(event, variableName);
         } catch (Throwable t) {
             StatusLogger.getLogger().error("Resolver failed to lookup {}", 
variableName, t);
             return null;
@@ -1475,14 +1473,6 @@ public class StrSubstitutor implements 
ConfigurationAware {
         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/appender/routing/RoutingAppender3350Test.java
 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/routing/RoutingAppender3350Test.java
new file mode 100644
index 0000000..55f382b
--- /dev/null
+++ 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/routing/RoutingAppender3350Test.java
@@ -0,0 +1,65 @@
+/*
+ * 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.Logger;
+import org.apache.logging.log4j.junit.LoggerContextRule;
+import org.apache.logging.log4j.message.StringMapMessage;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.Assert.assertEquals;
+
+public class RoutingAppender3350Test {
+    private static final String CONFIG = "log4j-routing3350.xml";
+    private static final String LOG_FILE = "target/tmp/test.log";
+
+    private final LoggerContextRule loggerContextRule = new 
LoggerContextRule(CONFIG);
+
+    @Rule
+    public RuleChain rules = loggerContextRule.withCleanFilesRule(LOG_FILE);
+
+    @After
+    public void tearDown() throws Exception {
+        this.loggerContextRule.getLoggerContext().stop();
+    }
+
+    @Test
+    public void routingTest() throws IOException {
+        String expected = "expectedValue";
+        StringMapMessage message = new StringMapMessage().with("data", 
expected);
+        Logger logger = 
loggerContextRule.getLoggerContext().getLogger(getClass());
+        logger.error(message);
+        File file = new File(LOG_FILE);
+        try (InputStream inputStream = new FileInputStream(file);
+             InputStreamReader streamReader = new 
InputStreamReader(inputStream, StandardCharsets.UTF_8);
+             BufferedReader reader = new BufferedReader(streamReader)) {
+            String actual = reader.readLine();
+            assertEquals(expected, actual);
+        }
+    }
+}
diff --git 
a/log4j-core/src/test/java/org/apache/logging/log4j/core/lookup/PropertiesLookupTest.java
 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/config/PropertiesPluginTest.java
similarity index 67%
copy from 
log4j-core/src/test/java/org/apache/logging/log4j/core/lookup/PropertiesLookupTest.java
copy to 
log4j-core/src/test/java/org/apache/logging/log4j/core/config/PropertiesPluginTest.java
index 2c708f0..7f35ef8 100644
--- 
a/log4j-core/src/test/java/org/apache/logging/log4j/core/lookup/PropertiesLookupTest.java
+++ 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/config/PropertiesPluginTest.java
@@ -15,23 +15,22 @@
  * limitations under the license.
  */
 
-package org.apache.logging.log4j.core.lookup;
+package org.apache.logging.log4j.core.config;
 
-import static org.junit.Assert.assertEquals;
+import org.junit.jupiter.api.Test;
 
-import java.util.HashMap;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
-import org.junit.jupiter.api.Test;
+public class PropertiesPluginTest {
 
-/**
- * Tests {@link PropertiesLookup}.
- */
-public class PropertiesLookupTest {
+    @Test
+    public void testUnescape() {
+        assertEquals("${foo}", PropertiesPlugin.unescape("$${foo}"));
+    }
 
     @Test
-    public void testGetProperties() {
-        final HashMap<String, String> properties = new HashMap<>();
-        properties.put("A", "1");
-        assertEquals(properties, new 
PropertiesLookup(properties).getProperties());
+    public void testUnescapeNotEscapedWithDefault() {
+        String value = "${foo:-bar}";
+        assertEquals(value, PropertiesPlugin.unescape(value));
     }
 }
diff --git 
a/log4j-core/src/test/java/org/apache/logging/log4j/core/lookup/PropertiesLookupTest.java
 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/lookup/PropertiesLookupTest.java
index 2c708f0..1e34c5a 100644
--- 
a/log4j-core/src/test/java/org/apache/logging/log4j/core/lookup/PropertiesLookupTest.java
+++ 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/lookup/PropertiesLookupTest.java
@@ -17,10 +17,16 @@
 
 package org.apache.logging.log4j.core.lookup;
 
-import static org.junit.Assert.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.Map;
 
+import org.apache.logging.log4j.core.config.Property;
 import org.junit.jupiter.api.Test;
 
 /**
@@ -29,9 +35,63 @@ import org.junit.jupiter.api.Test;
 public class PropertiesLookupTest {
 
     @Test
-    public void testGetProperties() {
-        final HashMap<String, String> properties = new HashMap<>();
-        properties.put("A", "1");
-        assertEquals(properties, new 
PropertiesLookup(properties).getProperties());
+    public void testLookupContextProperty() {
+        final StrLookup propertiesLookup = new PropertiesLookup(
+                Property.EMPTY_ARRAY, Collections.singletonMap("A", "1"));
+        assertEquals("1", propertiesLookup.lookup("A"));
+        final LookupResult lookupResult = propertiesLookup.evaluate("A");
+        assertEquals("1", lookupResult.value());
+        assertFalse(lookupResult.isLookupEvaluationAllowedInValue());
+    }
+
+    @Test
+    public void testLookupConfigProperty() {
+        final StrLookup propertiesLookup = new PropertiesLookup(
+                new Property[] {Property.createProperty("A", "1")},
+                Collections.emptyMap());
+        assertEquals("1", propertiesLookup.lookup("A"));
+        final LookupResult lookupResult = propertiesLookup.evaluate("A");
+        assertEquals("1", lookupResult.value());
+        assertTrue(lookupResult.isLookupEvaluationAllowedInValue());
+    }
+
+    @Test
+    public void testConfigPropertiesPreferredOverContextProperties() {
+        final StrLookup propertiesLookup = new PropertiesLookup(
+                new Property[] {Property.createProperty("A", "1")},
+                Collections.singletonMap("A", "2"));
+        assertEquals("1", propertiesLookup.lookup("A"));
+        final LookupResult lookupResult = propertiesLookup.evaluate("A");
+        assertEquals("1", lookupResult.value());
+        assertTrue(lookupResult.isLookupEvaluationAllowedInValue());
+    }
+
+    @Test
+    public void testEvaluateResultsSupportRecursiveEvaluation() {
+        PropertiesLookup lookup = new 
PropertiesLookup(Collections.singletonMap("key", "value"));
+        assertFalse(lookup.evaluate("key").isLookupEvaluationAllowedInValue());
+    }
+
+    @Test
+    public void testEvaluateReturnsNullWhenKeyIsNotFound() {
+        PropertiesLookup lookup = new PropertiesLookup(Collections.emptyMap());
+        assertNull(lookup.evaluate("key"));
+    }
+
+    @Test
+    public void testEvaluateReturnsNullWhenKeyIsNull() {
+        PropertiesLookup lookup = new PropertiesLookup(Collections.emptyMap());
+        assertNull(lookup.evaluate(null));
+    }
+
+    @Test
+    public void testContextPropertiesAreMutable() {
+        Map<String, String> contextProperties = new HashMap<>();
+        PropertiesLookup lookup = new PropertiesLookup(Property.EMPTY_ARRAY, 
contextProperties);
+        assertNull(lookup.evaluate("key"));
+        contextProperties.put("key", "value");
+        LookupResult result = lookup.evaluate("key");
+        assertEquals("value", result.value());
+        assertFalse(result.isLookupEvaluationAllowedInValue());
     }
 }
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 efb492c..9397fb8 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
@@ -116,7 +116,7 @@ public class StrSubstitutorTest {
         valuesMap.put("main:--file", "path/file.txt");
         // Create substitutor, initially without support for escaping :-
         final StrSubstitutor sub = new StrSubstitutor(
-                new PropertiesLookup(valuesMap),
+                new RecursiveLookup(valuesMap),
                 StrSubstitutor.DEFAULT_PREFIX,
                 StrSubstitutor.DEFAULT_SUFFIX,
                 StrSubstitutor.DEFAULT_ESCAPE,
@@ -144,7 +144,7 @@ public class StrSubstitutorTest {
     public void testDefault() {
         final Map<String, String> map = new HashMap<>();
         map.put(TESTKEY, TESTVAL);
-        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrLookup lookup = new Interpolator(new NonRecursiveLookup(map));
         final StrSubstitutor subst = new StrSubstitutor(lookup);
         ThreadContext.put(TESTKEY, TESTVAL);
         //String value = subst.replace("${sys:TestKey1:-${ctx:TestKey}}");
@@ -156,36 +156,33 @@ public class StrSubstitutorTest {
     public void testDefaultReferencesLookupValue() {
         final Map<String, String> map = new HashMap<>();
         map.put(TESTKEY, "${java:version}");
-        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrLookup lookup = new Interpolator(new NonRecursiveLookup(map));
         final StrSubstitutor subst = new StrSubstitutor(lookup);
-        subst.setRecursiveEvaluationAllowed(false);
         final String value = subst.replace("${sys:TestKey1:-${ctx:TestKey}}");
         assertEquals("${java:version}", value);
     }
 
     @Test
     public void testInfiniteSubstitutionOnString() {
-        final StrLookup lookup = new Interpolator(new MapLookup(new 
HashMap<>()));
+        final StrLookup lookup = new Interpolator(new NonRecursiveLookup(new 
HashMap<>()));
         final StrSubstitutor subst = new StrSubstitutor(lookup);
-        subst.setRecursiveEvaluationAllowed(true);
         String infiniteSubstitution = "${${::-${::-$${::-j}}}}";
-        assertEquals(infiniteSubstitution, 
subst.replace(infiniteSubstitution));
+        assertEquals("j}", subst.replace(infiniteSubstitution));
     }
 
     @Test
     public void testInfiniteSubstitutionOnStringBuilder() {
-        final StrLookup lookup = new Interpolator(new MapLookup(new 
HashMap<>()));
+        final StrLookup lookup = new Interpolator(new NonRecursiveLookup(new 
HashMap<>()));
         final StrSubstitutor subst = new StrSubstitutor(lookup);
-        subst.setRecursiveEvaluationAllowed(true);
         String infiniteSubstitution = "${${::-${::-$${::-j}}}}";
-        assertEquals(infiniteSubstitution, subst.replace(null, new 
StringBuilder(infiniteSubstitution)));
+        assertEquals("j}", subst.replace(null, new 
StringBuilder(infiniteSubstitution)));
     }
 
     @Test
     public void testLookup() {
         final Map<String, String> map = new HashMap<>();
         map.put(TESTKEY, TESTVAL);
-        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrLookup lookup = new Interpolator(new NonRecursiveLookup(map));
         final StrSubstitutor subst = new StrSubstitutor(lookup);
         ThreadContext.put(TESTKEY, TESTVAL);
         String value = 
subst.replace("${TestKey}-${ctx:TestKey}-${sys:TestKey}");
@@ -205,9 +202,8 @@ public class StrSubstitutorTest {
     public void testLookupsNestedWithoutRecursiveEvaluation() {
         final Map<String, String> map = new HashMap<>();
         map.put("first", "${java:version}");
-        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrLookup lookup = new Interpolator(new NonRecursiveLookup(map));
         final StrSubstitutor subst = new StrSubstitutor(lookup);
-        subst.setRecursiveEvaluationAllowed(false);
         assertEquals("${java:version}", 
subst.replace("${${lower:C}t${lower:X}:first}"));
     }
 
@@ -228,7 +224,6 @@ public class StrSubstitutorTest {
                 return "success";
             }
         }));
-        subst.setRecursiveEvaluationAllowed(false);
         assertEquals("success ${foo:throw} success", subst.replace("${foo:a} 
${foo:throw} ${foo:c}"));
     }
 
@@ -236,9 +231,8 @@ public class StrSubstitutorTest {
     public void testNestedSelfReferenceWithRecursiveEvaluation() {
         final Map<String, String> map = new HashMap<>();
         map.put("first", "${${ctx:first}}");
-        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrLookup lookup = new Interpolator(new RecursiveLookup(map));
         final StrSubstitutor subst = new StrSubstitutor(lookup);
-        subst.setRecursiveEvaluationAllowed(true);
         assertEquals("${${ctx:first}}", subst.replace("${ctx:first}"));
     }
 
@@ -247,9 +241,8 @@ public class StrSubstitutorTest {
         final Map<String, String> map = new HashMap<>();
         map.put("first", "${java:version}");
         map.put("second", "${java:runtime}");
-        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrLookup lookup = new Interpolator(new NonRecursiveLookup(map));
         final StrSubstitutor subst = new StrSubstitutor(lookup);
-        subst.setRecursiveEvaluationAllowed(false);
         assertEquals("${java:version}", 
subst.replace("${ctx:first:-${ctx:second}}"));
     }
 
@@ -257,9 +250,8 @@ public class StrSubstitutorTest {
     public void testNoRecursiveEvaluationWithDepthOne() {
         final Map<String, String> map = new HashMap<>();
         map.put("first", "${java:version}");
-        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrLookup lookup = new Interpolator(new NonRecursiveLookup(map));
         final StrSubstitutor subst = new StrSubstitutor(lookup);
-        subst.setRecursiveEvaluationAllowed(false);
         assertEquals("${java:version}", subst.replace("${ctx:first}"));
     }
 
@@ -267,9 +259,8 @@ public class StrSubstitutorTest {
     public void testRandomWithRecursiveDefault() {
         final Map<String, String> map = new HashMap<>();
         map.put("first", "${env:RANDOM:-${ctx:first}}");
-        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrLookup lookup = new Interpolator(new RecursiveLookup(map));
         final StrSubstitutor subst = new StrSubstitutor(lookup);
-        subst.setRecursiveEvaluationAllowed(true);
         assertEquals("${ctx:first}", subst.replace("${ctx:first}"));
     }
 
@@ -278,9 +269,8 @@ public class StrSubstitutorTest {
         final Map<String, String> map = new HashMap<>();
         map.put("first", "${ctx:first}");
         map.put("second", "secondValue");
-        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrLookup lookup = new Interpolator(new RecursiveLookup(map));
         final StrSubstitutor subst = new StrSubstitutor(lookup);
-        subst.setRecursiveEvaluationAllowed(true);
         assertEquals("${ctx:first} and secondValue", 
subst.replace("${ctx:first} and ${ctx:second}"));
     }
 
@@ -288,9 +278,8 @@ public class StrSubstitutorTest {
     public void testRecursiveWithDefault() {
         final Map<String, String> map = new HashMap<>();
         map.put("first", "${ctx:first:-default}");
-        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrLookup lookup = new Interpolator(new RecursiveLookup(map));
         final StrSubstitutor subst = new StrSubstitutor(lookup);
-        subst.setRecursiveEvaluationAllowed(true);
         assertEquals("default", subst.replace("${ctx:first}"));
     }
 
@@ -298,9 +287,8 @@ public class StrSubstitutorTest {
     public void testRecursiveWithRecursiveDefault() {
         final Map<String, String> map = new HashMap<>();
         map.put("first", "${ctx:first:-${ctx:first}}");
-        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrLookup lookup = new Interpolator(new RecursiveLookup(map));
         final StrSubstitutor subst = new StrSubstitutor(lookup);
-        subst.setRecursiveEvaluationAllowed(true);
         assertEquals("${ctx:first}", subst.replace("${ctx:first}"));
     }
 
@@ -318,9 +306,8 @@ public class StrSubstitutorTest {
     public void testTopLevelLookupsWithoutRecursiveEvaluation() {
         final Map<String, String> map = new HashMap<>();
         map.put("key", "VaLuE");
-        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrLookup lookup = new Interpolator(new NonRecursiveLookup(map));
         final StrSubstitutor subst = new StrSubstitutor(lookup);
-        subst.setRecursiveEvaluationAllowed(false);
         assertEquals("value", subst.replace("${lower:${ctx:key}}"));
     }
 
@@ -328,9 +315,8 @@ public class StrSubstitutorTest {
     public void testTopLevelLookupsWithoutRecursiveEvaluation_doubleLower() {
         final Map<String, String> map = new HashMap<>();
         map.put("key", "VaLuE");
-        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrLookup lookup = new Interpolator(new NonRecursiveLookup(map));
         final StrSubstitutor subst = new StrSubstitutor(lookup);
-        subst.setRecursiveEvaluationAllowed(false);
         assertEquals("value", subst.replace("${lower:${lower:${ctx:key}}}"));
     }
 
@@ -338,9 +324,121 @@ public class StrSubstitutorTest {
     public void 
testTopLevelLookupsWithoutRecursiveEvaluationAndDefaultValueLookup() {
         final Map<String, String> map = new HashMap<>();
         map.put("key2", "TWO");
-        final StrLookup lookup = new Interpolator(new MapLookup(map));
+        final StrLookup lookup = new Interpolator(new NonRecursiveLookup(map));
         final StrSubstitutor subst = new StrSubstitutor(lookup);
-        subst.setRecursiveEvaluationAllowed(false);
         assertEquals("two", 
subst.replace("${lower:${ctx:key1:-${ctx:key2}}}"));
     }
+
+    @Test
+    public void testNonRecursiveReferencesRecursive() {
+        final StrLookup lookup = new StrLookup() {
+            @Override
+            public String lookup(final String key) {
+                return "unexpected";
+            }
+
+            @Override
+            public String lookup(final LogEvent event, final String key) {
+                return "unexpected";
+            }
+
+            @Override
+            public LookupResult evaluate(final String key) {
+                return evaluate(null, key);
+            }
+
+            @Override
+            public LookupResult evaluate(final LogEvent event, final String 
key) {
+                switch (key) {
+                    case "first":
+                        return new RecursiveLookupResult("${second}");
+                    case "second":
+                        return new NonRecursiveLookupResult("${third}");
+                    default:
+                        return new RecursiveLookupResult("should not be used: 
" + key);
+                }
+            }
+        };
+        final StrSubstitutor subst = new StrSubstitutor(lookup);
+        // First (recursive) expands to second, which is not recursive, so the 
literal '${third}' is used.
+        assertEquals("${third}", subst.replace("${first}"));
+    }
+
+    private static final class RecursiveLookup extends AbstractLookup {
+
+        private final Map<String, String> properties;
+
+        RecursiveLookup(Map<String, String> properties) {
+            this.properties = properties;
+        }
+
+        @Override
+        public String lookup(LogEvent event, String key) {
+            LookupResult result = evaluate(event, key);
+            return result == null ? null : result.value();
+        }
+
+        @Override
+        public LookupResult evaluate(LogEvent event, String key) {
+            String result = key == null ? null : properties.get(key);
+            return result == null ? null : new RecursiveLookupResult(result);
+        }
+    }
+
+    private static final class RecursiveLookupResult implements LookupResult {
+
+        private final String value;
+
+        RecursiveLookupResult(String value) {
+            this.value = value;
+        }
+        @Override
+        public String value() {
+            return value;
+        }
+
+        @Override
+        public boolean isLookupEvaluationAllowedInValue() {
+            return true;
+        }
+    }
+
+    private static final class NonRecursiveLookup extends AbstractLookup {
+
+        private final Map<String, String> properties;
+
+        NonRecursiveLookup(Map<String, String> properties) {
+            this.properties = properties;
+        }
+
+        @Override
+        public String lookup(LogEvent event, String key) {
+            LookupResult result = evaluate(event, key);
+            return result == null ? null : result.value();
+        }
+
+        @Override
+        public LookupResult evaluate(LogEvent event, String key) {
+            String result = key == null ? null : properties.get(key);
+            return result == null ? null : new 
NonRecursiveLookupResult(result);
+        }
+    }
+
+    private static final class NonRecursiveLookupResult implements 
LookupResult {
+
+        private final String value;
+
+        NonRecursiveLookupResult(String value) {
+            this.value = value;
+        }
+        @Override
+        public String value() {
+            return value;
+        }
+
+        @Override
+        public boolean isLookupEvaluationAllowedInValue() {
+            return false;
+        }
+    }
 }
diff --git a/log4j-core/src/test/resources/log4j-routing-2767.xml 
b/log4j-core/src/test/resources/log4j-routing-2767.xml
index 4002659..34b9c30 100644
--- a/log4j-core/src/test/resources/log4j-routing-2767.xml
+++ b/log4j-core/src/test/resources/log4j-routing-2767.xml
@@ -17,6 +17,10 @@
 
 -->
 <Configuration status="WARN" name="RoutingTest">
+  <Properties>
+    <Property 
name="filename">target/routing1/routingtest-$${sd:type}.log</Property>
+  </Properties>
+
   <Appenders>
     <Console name="STDOUT">
       <PatternLayout pattern="%m%n"/>
@@ -24,7 +28,7 @@
     <Routing name="Routing">
       <Routes>
         <Route>
-          <RollingFile name="Routing-${sd:type}" 
fileName="target/routing1/routingtest-${sd:type}.log"
+          <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>
diff --git a/log4j-core/src/test/resources/log4j-routing-purge.xml 
b/log4j-core/src/test/resources/log4j-routing-purge.xml
index 910ba80..d1de288 100644
--- a/log4j-core/src/test/resources/log4j-routing-purge.xml
+++ b/log4j-core/src/test/resources/log4j-routing-purge.xml
@@ -17,6 +17,10 @@
 
 -->
 <Configuration status="OFF" name="RoutingTest">
+  <Properties>
+    <Property 
name="filename-idle">target/routing-purge-idle/routingtest-$${sd:id}.log</Property>
+    <Property 
name="filename-manual">target/routing-purge-manual/routingtest-$${sd:id}.log</Property>
+  </Properties>
   <ThresholdFilter level="debug"/>
 
   <Appenders>
@@ -30,7 +34,7 @@
     <Routing name="RoutingPurgeIdle">
       <Routes pattern="$${sd:id}">
         <Route>
-          <File name="Routing-${sd:id}" 
fileName="target/routing-purge-idle/routingtest-${sd:id}.log">
+          <File name="Routing-${sd:id}" fileName="${filename-idle}">
             <PatternLayout>
               <Pattern>%d %p %C{1.} [%t] %m%n</Pattern>
             </PatternLayout>
@@ -54,7 +58,7 @@
     <Routing name="RoutingPurgeManual">
       <Routes pattern="$${sd:id}">
         <Route>
-          <File name="Routing-${sd:id}" 
fileName="target/routing-purge-manual/routingtest-${sd:id}.log">
+          <File name="Routing-${sd:id}" fileName="${filename-manual}">
             <PatternLayout>
               <Pattern>%d %p %C{1.} [%t] %m%n</Pattern>
             </PatternLayout>
diff --git a/log4j-core/src/test/resources/log4j-routing.json 
b/log4j-core/src/test/resources/log4j-routing.json
index 4322d8d..f6f97d4 100644
--- a/log4j-core/src/test/resources/log4j-routing.json
+++ b/log4j-core/src/test/resources/log4j-routing.json
@@ -15,6 +15,9 @@
  * 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",
@@ -28,7 +31,7 @@
           "Route": [
             {
               "RollingFile": {
-                "name": "Rolling-${sd:type}", "fileName": 
"target/rolling1/rollingtest-${sd:type}.log",
+                "name": "Rolling-${sd:type}", "fileName": "${filename}",
                 "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.properties 
b/log4j-core/src/test/resources/log4j-routing.properties
index 0dc7547..c365e35 100644
--- a/log4j-core/src/test/resources/log4j-routing.properties
+++ b/log4j-core/src/test/resources/log4j-routing.properties
@@ -16,6 +16,8 @@
 status = error
 name = RoutingTest
 
+property.filename = target/routing1/routingtestProps-$${sd:type}.log
+
 filter.threshold.type = ThresholdFilter
 filter.threshold.level = debug
 
@@ -31,7 +33,7 @@ appender.routing.routes.pattern = $${sd:type}
 appender.routing.routes.route1.type = Route
 appender.routing.routes.route1.rolling.type = RollingFile
 appender.routing.routes.route1.rolling.name = Routing-${sd:type}
-appender.routing.routes.route1.rolling.fileName = 
target/routing1/routingtestProps-${sd:type}.log
+appender.routing.routes.route1.rolling.fileName = ${filename}
 appender.routing.routes.route1.rolling.filePattern = 
target/routing1/test1-${sd:type}.%i.log.gz
 appender.routing.routes.route1.rolling.layout.type = PatternLayout
 appender.routing.routes.route1.rolling.layout.pattern = %d %p %C{1.} [%t] %m%n
diff --git a/log4j-core/src/test/resources/log4j-routing.xml 
b/log4j-core/src/test/resources/log4j-routing.xml
index 003f7d7..4d83886 100644
--- a/log4j-core/src/test/resources/log4j-routing.xml
+++ b/log4j-core/src/test/resources/log4j-routing.xml
@@ -17,6 +17,9 @@
 
 -->
 <Configuration status="OFF" name="RoutingTest">
+  <Properties>
+    <Property 
name="filename">target/routing1/routingtest-$${sd:type}.log</Property>
+  </Properties>
   <ThresholdFilter level="debug"/>
 
   <Appenders>
@@ -29,7 +32,7 @@
     <Routing name="Routing">
       <Routes pattern="$${sd:type}">
         <Route>
-          <RollingFile name="Routing-${sd:type}" 
fileName="target/routing1/routingtest-${sd:type}.log"
+          <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>
diff --git a/log4j-core/src/test/resources/log4j-routing2.json 
b/log4j-core/src/test/resources/log4j-routing2.json
index af67454..baf475a 100644
--- a/log4j-core/src/test/resources/log4j-routing2.json
+++ b/log4j-core/src/test/resources/log4j-routing2.json
@@ -15,6 +15,9 @@
  * 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": [
@@ -25,7 +28,7 @@
             "Route": [
               {
                 "RollingFile": {
-                  "name": "Rolling-${sd:type}", "fileName": 
"target/rolling1/rollingtest-${sd:type}.log",
+                  "name": "Rolling-${sd:type}", "fileName": "${filename}",
                   "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-2767.xml 
b/log4j-core/src/test/resources/log4j-routing3350.xml
similarity index 66%
copy from log4j-core/src/test/resources/log4j-routing-2767.xml
copy to log4j-core/src/test/resources/log4j-routing3350.xml
index 4002659..9b17066 100644
--- a/log4j-core/src/test/resources/log4j-routing-2767.xml
+++ b/log4j-core/src/test/resources/log4j-routing3350.xml
@@ -16,20 +16,24 @@
  limitations under the License.
 
 -->
-<Configuration status="WARN" name="RoutingTest">
+<Configuration status="OFF" name="Routing3350">
+  <Properties>
+    <Property name="pcode">def</Property>
+    <Property name="drive">target</Property>
+    <Property name="path">/tmp/</Property>
+    <Property name="name">test.log</Property>
+    <Property name="filename">${drive}${path}${name}</Property>
+    <Property name="filepattern">${filename}.%i.backup</Property>
+  </Properties>
   <Appenders>
-    <Console name="STDOUT">
-      <PatternLayout pattern="%m%n"/>
-    </Console>
     <Routing name="Routing">
-      <Routes>
+      <Routes pattern="$${map:pcode}">
         <Route>
-          <RollingFile name="Routing-${sd:type}" 
fileName="target/routing1/routingtest-${sd:type}.log"
-                       
filePattern="target/routing1/test1-${sd:type}.%i.log.gz">
+          <RollingFile name="Rolling" fileName="${filename}" 
filePattern="${filepattern}">
             <PatternLayout>
-              <Pattern>%d %p %C{1.} [%t] %m%n</Pattern>
+              <pattern>%map{data}%n</pattern>
             </PatternLayout>
-            <SizeBasedTriggeringPolicy size="500" />
+            <SizeBasedTriggeringPolicy size="500"/>
           </RollingFile>
         </Route>
       </Routes>
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index f0b3f3e..8300101 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -31,6 +31,9 @@
     -->
     <release version="2.17.2" date="20YY-MM-DD" description="GA Release 
2.17.2">
       <!-- FIXES -->
+      <action issue="LOG4J2-3317" dev="ckozak" type="fix">
+        Fix RoutingAppender backcompat and disallow recursive evaluation of 
lookup results outside of configuration properties.
+      </action>
       <action issue="LOG4J2-3333" dev="ckozak" type="fix">
         Fix ThreadContextDataInjector initialization deadlock
       </action>

Reply via email to