Skip to content

Commit 0dfdd8e

Browse files
committed
[1223] Change FormattedMessage pattern heuristic
We change the order in which `FormattedMessage` checks the format of the provided pattern: we first check for the presence of `{}` placeholders and only then for `java.util.Format` specifiers. This eliminates the need for a potentially exponential regular expression evalutation, which was reported by Spotbugs (#1849). The Javadoc and documentation were improved to clarify the heuristic used by `FormattedMessage`. Closes #1223. Remark: since `FormattedMessage` used the **same** regular expression as `java.util.Format`, if a message uses `java.util.Format` specifiers, it is still vulnerable to a ReDOS.
1 parent 2e7c2ca commit 0dfdd8e

File tree

4 files changed

+68
-18
lines changed

4 files changed

+68
-18
lines changed

log4j-api/src/main/java/org/apache/logging/log4j/message/FormattedMessage.java

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import java.text.MessageFormat;
2424
import java.util.Arrays;
2525
import java.util.Locale;
26-
import java.util.regex.Pattern;
2726

2827
/**
2928
* Handles messages that contain a format String. Dynamically determines if the format conforms to
@@ -33,8 +32,6 @@ public class FormattedMessage implements Message {
3332

3433
private static final long serialVersionUID = -665975803997290697L;
3534
private static final int HASHVAL = 31;
36-
private static final String FORMAT_SPECIFIER = "%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])";
37-
private static final Pattern MSG_PATTERN = Pattern.compile(FORMAT_SPECIFIER);
3835

3936
private String messagePattern;
4037
private transient Object[] argArray;
@@ -180,24 +177,44 @@ public String getFormattedMessage() {
180177
return formattedMessage;
181178
}
182179

180+
/**
181+
* Gets the message implementation to which formatting is delegated.
182+
*
183+
* <ul>
184+
* <li>if {@code msgPattern} contains {@link MessageFormat} format specifiers a {@link MessageFormatMessage}
185+
* is returned,</li>
186+
* <li>if {@code msgPattern} contains {@code {}} placeholders a {@link ParameterizedMessage} is returned,</li>
187+
* <li>if {@code msgPattern} contains {@link Format} specifiers a {@link StringFormattedMessage} is returned
188+
* .</li>
189+
* </ul>
190+
* <p>
191+
* Mixing specifiers from multiple types is not supported.
192+
* </p>
193+
*
194+
* @param msgPattern The message pattern.
195+
* @param args The parameters.
196+
* @param aThrowable The throwable
197+
* @return The message that performs formatting.
198+
*/
183199
protected Message getMessage(final String msgPattern, final Object[] args, final Throwable aThrowable) {
200+
// Check for valid `{ ArgumentIndex [, FormatType [, FormatStyle]] }` format specifiers
184201
try {
185202
final MessageFormat format = new MessageFormat(msgPattern);
186203
final Format[] formats = format.getFormats();
187-
if (formats != null && formats.length > 0) {
204+
if (formats.length > 0) {
188205
return new MessageFormatMessage(locale, msgPattern, args);
189206
}
190207
} catch (final Exception ignored) {
191208
// Obviously, the message is not a proper pattern for MessageFormat.
192209
}
193-
try {
194-
if (MSG_PATTERN.matcher(msgPattern).find()) {
195-
return new StringFormattedMessage(locale, msgPattern, args);
196-
}
197-
} catch (final Exception ignored) {
198-
// Also not properly formatted.
210+
// Check for non-escaped `{}` format specifiers
211+
// This case also includes patterns without any `java.util.Formatter` specifiers
212+
if (ParameterFormatter.analyzePattern(msgPattern, 1).placeholderCount > 0
213+
|| msgPattern.indexOf('%') == -1) {
214+
return new ParameterizedMessage(msgPattern, args, aThrowable);
199215
}
200-
return new ParameterizedMessage(msgPattern, args, aThrowable);
216+
// Interpret as `java.util.Formatter` format
217+
return new StringFormattedMessage(locale, msgPattern, args);
201218
}
202219

203220
/**

log4j-api/src/main/java/org/apache/logging/log4j/message/package-info.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
* Public Message Types used for Log4j 2. Users may implement their own Messages.
2020
*/
2121
@Export
22-
@Version("2.20.1")
22+
/**
23+
* Bumped to 2.21.0, since FormattedMessage behavior changed.
24+
*/
25+
@Version("2.21.0")
2326
package org.apache.logging.log4j.message;
2427

2528
import org.osgi.annotation.bundle.Export;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="http://logging.apache.org/log4j/changelog"
4+
xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.2.xsd"
5+
type="changed">
6+
<issue id="1223" link="https://github.com/apache/logging-log4j2/issues/1223"/>
7+
<description format="asciidoc">
8+
Change the order of evaluation of `FormattedMessage` formatters.
9+
Messages are evaluated using `java.util.Format` only if they don't comply to the `java.text.MessageFormat` or `ParameterizedMessage` format.
10+
</description>
11+
</entry>

src/site/xdoc/manual/messages.xml

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,31 @@ public class MyApp {
168168
<h4>FormattedMessage</h4>
169169
<a name="FormattedMessage"/>
170170
<p>
171-
The message pattern passed to a
172-
<a class="javadoc" href="../log4j-api/apidocs/org/apache/logging/log4j/message/FormattedMessage.html">FormattedMessage</a>
173-
is first checked to see if it is a valid java.text.MessageFormat pattern. If it is, a MessageFormatMessage is
174-
used to format it. If not it is next checked to see if it contains any tokens that are valid format
175-
specifiers for String.format(). If so, a StringFormattedMessage is used to format it. Finally, if the
176-
pattern doesn't match either of those then a ParameterizedMessage is used to format it.
171+
<a class="javadoc" href="../log4j-api/apidocs/org/apache/logging/log4j/message/FormattedMessage">FormattedMessage</a>
172+
is a message that supports multiple pattern formatters:
173+
<ul>
174+
<li>
175+
if the pattern is a valid
176+
<a class="javadoc" href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/text/MessageFormat.html">java.text.MessageFormat</a>
177+
pattern, then
178+
<a class="javadoc" href="https://logging.apache.org/log4j/2.x/javadoc/log4j-api/org/apache/logging/log4j/message/MessageFormatMessage.html">MessageFormatMessage</a>
179+
is used to format it,
180+
</li>
181+
<li>
182+
if the pattern contains valid unescaped <code>{}</code> specifiers, then
183+
<a class="javadoc" href="https://logging.apache.org/log4j/2.x/javadoc/log4j-api/org/apache/logging/log4j/message/ParameterizedMessage">ParameterizedMessage</a>
184+
is used to format it,
185+
</li>
186+
<li>
187+
if the pattern is a valid
188+
<a class="javadoc" href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Formatter.html">java.uti.Formatter</a>
189+
pattern, then
190+
<a class="javadoc" href="https://logging.apache.org/log4j/2.x/javadoc/log4j-api/org/apache/logging/log4j/message/StringFormattedMessage">StringFormattedMessage</a>
191+
is used to format it.
192+
</li>
193+
</ul>
194+
Mixing specifiers from multiple formatters is not supported.
195+
A pattern without any specifiers will use any of the above formatters.
177196
</p>
178197
<h4>LocalizedMessage</h4>
179198
<a name="LocalizedMessage"/>

0 commit comments

Comments
 (0)