log4j12/CVE-2022-23305.patch
wk333 fdf748765f Fix cves
(cherry picked from commit 571db5722d6f4f5349a54ffb8575f9dcd2049c09)
2022-02-09 14:19:33 +08:00

632 lines
27 KiB
Diff

From: Markus Koschany <apo@debian.org>
Date: Mon, 31 Jan 2022 10:55:04 +0100
Subject: CVE-2022-23305
Origin: https://github.com/qos-ch/reload4j/pull/26/commits/33d1697bb13b8cf869c450a64f8550b1593f8035
---
pom.xml | 6 +
.../java/org/apache/log4j/jdbc/JDBCAppender.java | 97 ++++++++--
.../org/apache/log4j/jdbc/JdbcPatternParser.java | 101 ++++++++++
src/test/input/jdbc_h2_bufferSize1.properties | 25 +++
src/test/input/jdbc_h2_bufferSize2.properties | 25 +++
.../org/apache/log4j/jdbc/JdbcAppenderTest.java | 208 +++++++++++++++++++++
.../apache/log4j/jdbc/JdbcPatternParserTest.java | 50 +++++
7 files changed, 495 insertions(+), 17 deletions(-)
create mode 100644 src/main/java/org/apache/log4j/jdbc/JdbcPatternParser.java
create mode 100644 src/test/input/jdbc_h2_bufferSize1.properties
create mode 100644 src/test/input/jdbc_h2_bufferSize2.properties
create mode 100644 src/test/java/org/apache/log4j/jdbc/JdbcAppenderTest.java
create mode 100644 src/test/java/org/apache/log4j/jdbc/JdbcPatternParserTest.java
diff --git a/pom.xml b/pom.xml
index 93881cd..3ec2ccc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -577,6 +577,12 @@ target platform and specify -Dntdll_target=msbuild on the mvn command line.
<version>1.0</version>
<optional>true</optional>
</dependency>
+ <dependency>
+ <groupId>com.h2database</groupId>
+ <artifactId>h2</artifactId>
+ <version>2.1.210</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<distributionManagement>
<repository>
diff --git a/src/main/java/org/apache/log4j/jdbc/JDBCAppender.java b/src/main/java/org/apache/log4j/jdbc/JDBCAppender.java
index ad35f65..2f979ae 100644
--- a/src/main/java/org/apache/log4j/jdbc/JDBCAppender.java
+++ b/src/main/java/org/apache/log4j/jdbc/JDBCAppender.java
@@ -18,6 +18,7 @@ package org.apache.log4j.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
+import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
@@ -78,6 +79,12 @@ import org.apache.log4j.spi.LoggingEvent;
public class JDBCAppender extends org.apache.log4j.AppenderSkeleton
implements org.apache.log4j.Appender {
+ private final Boolean secureSqlReplacement =
+ Boolean.parseBoolean(System.getProperty("org.apache.log4j.jdbc.JDBCAppender.secure_jdbc_replacement", "true"));
+
+ private static final IllegalArgumentException ILLEGAL_PATTERN_FOR_SECURE_EXEC =
+ new IllegalArgumentException("Only org.apache.log4j.PatternLayout is supported for now due to CVE-2022-23305");
+
/**
* URL of the DB for default connection handling
*/
@@ -113,6 +120,8 @@ public class JDBCAppender extends org.apache.log4j.AppenderSkeleton
*/
protected String sqlStatement = "";
+ private JdbcPatternParser preparedStatementParser = new JdbcPatternParser();
+
/**
* size of LoggingEvent buffer before writting to the database.
* Default is 1.
@@ -245,11 +254,11 @@ public class JDBCAppender extends org.apache.log4j.AppenderSkeleton
*/
protected Connection getConnection() throws SQLException {
if (!DriverManager.getDrivers().hasMoreElements())
- setDriver("sun.jdbc.odbc.JdbcOdbcDriver");
+ setDriver("sun.jdbc.odbc.JdbcOdbcDriver");
if (connection == null) {
connection = DriverManager.getConnection(databaseURL, databaseUser,
- databasePassword);
+ databasePassword);
}
return connection;
@@ -280,29 +289,83 @@ public class JDBCAppender extends org.apache.log4j.AppenderSkeleton
* If a statement fails the LoggingEvent stays in the buffer!
*/
public void flushBuffer() {
+ if (buffer.isEmpty()) {
+ return;
+ }
//Do the actual logging
removes.ensureCapacity(buffer.size());
- for (Iterator i = buffer.iterator(); i.hasNext();) {
- LoggingEvent logEvent = (LoggingEvent)i.next();
- try {
- String sql = getLogStatement(logEvent);
- execute(sql);
- }
- catch (SQLException e) {
- errorHandler.error("Failed to excute sql", e,
- ErrorCode.FLUSH_FAILURE);
- } finally {
- removes.add(logEvent);
- }
+ if (secureSqlReplacement) {
+ flushBufferSecure();
+ } else {
+ flushBufferInsecure();
}
-
// remove from the buffer any events that were reported
buffer.removeAll(removes);
-
+
// clear the buffer of reported events
removes.clear();
}
+ private void flushBufferInsecure() {
+ for (Iterator i = buffer.iterator(); i.hasNext(); ) {
+ LoggingEvent logEvent = (LoggingEvent) i.next();
+ try {
+ String sql = getLogStatement(logEvent);
+ execute(sql);
+ } catch (SQLException e) {
+ errorHandler.error("Failed to excute sql", e, ErrorCode.FLUSH_FAILURE);
+ } finally {
+ removes.add(logEvent);
+ }
+ }
+ }
+
+ private void flushBufferSecure() {
+ // Prepare events that we will store to the DB
+ removes.addAll(buffer);
+
+ if (layout.getClass() != PatternLayout.class) {
+ errorHandler.error("Failed to convert pattern " + layout + " to SQL", ILLEGAL_PATTERN_FOR_SECURE_EXEC, ErrorCode.MISSING_LAYOUT);
+ return;
+ }
+ PatternLayout patternLayout = (PatternLayout) layout;
+ preparedStatementParser.setPattern(patternLayout.getConversionPattern());
+ Connection con = null;
+ boolean useBatch = removes.size() > 1;
+ try {
+ con = getConnection();
+ PreparedStatement ps = con.prepareStatement(preparedStatementParser.getParameterizedSql());
+ try {
+ for (Iterator i = removes.iterator(); i.hasNext(); ) {
+ LoggingEvent logEvent = (LoggingEvent) i.next();
+ try {
+ preparedStatementParser.setParameters(ps, logEvent);
+ if (useBatch) {
+ ps.addBatch();
+ }
+ } catch (SQLException e) {
+ errorHandler.error("Failed to append parameters", e, ErrorCode.FLUSH_FAILURE);
+ }
+ }
+ if (useBatch) {
+ ps.executeBatch();
+ } else {
+ ps.execute();
+ }
+ } finally {
+ try {
+ ps.close();
+ } catch (SQLException ignored) {
+ }
+ }
+ } catch (SQLException e) {
+ errorHandler.error("Failed to append messages sql", e, ErrorCode.FLUSH_FAILURE);
+ } finally {
+ if (con != null) {
+ closeConnection(con);
+ }
+ }
+ }
/** closes the appender before disposal */
public void finalize() {
@@ -391,7 +454,7 @@ public class JDBCAppender extends org.apache.log4j.AppenderSkeleton
Class.forName(driverClass);
} catch (Exception e) {
errorHandler.error("Failed to load driver", e,
- ErrorCode.GENERIC_FAILURE);
+ ErrorCode.GENERIC_FAILURE);
}
}
}
diff --git a/src/main/java/org/apache/log4j/jdbc/JdbcPatternParser.java b/src/main/java/org/apache/log4j/jdbc/JdbcPatternParser.java
new file mode 100644
index 0000000..691ed56
--- /dev/null
+++ b/src/main/java/org/apache/log4j/jdbc/JdbcPatternParser.java
@@ -0,0 +1,101 @@
+/*
+ * 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.log4j.jdbc;
+
+import org.apache.log4j.helpers.PatternConverter;
+import org.apache.log4j.helpers.PatternParser;
+import org.apache.log4j.spi.LoggingEvent;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+class JdbcPatternParser {
+ private final static Pattern STRING_LITERAL_PATTERN = Pattern.compile("'((?>[^']|'')+)'");
+
+ private String lastPattern;
+ private String parameterizedSql;
+ private final List<String> argPatterns = new ArrayList<String>();
+ private final List<PatternConverter> args = new ArrayList<PatternConverter>();
+ private StringBuffer buffer = new StringBuffer();
+
+ public String getParameterizedSql() {
+ return parameterizedSql;
+ }
+
+ @Override
+ public String toString() {
+ return "JdbcPatternParser{sql=" + parameterizedSql + ",args=" + argPatterns + "}";
+ }
+
+ /**
+ * Converts '....' literals into bind variables in JDBC.
+ */
+ void setPattern(String pattern) {
+ if (pattern == null) {
+ throw new IllegalArgumentException("Null pattern");
+ }
+ if (pattern.equals(lastPattern)) {
+ return;
+ }
+ Matcher m = STRING_LITERAL_PATTERN.matcher(pattern);
+ StringBuffer sb = new StringBuffer();
+ args.clear();
+ argPatterns.clear();
+ while (m.find()) {
+ String literal = m.group(1);
+ if (literal.indexOf('%') == -1) {
+ // Just literal, append it as is
+ // It can't contain user-provided parts like %m, etc.
+ m.appendReplacement(sb, "'$1'");
+ continue;
+ }
+
+ // Replace with bind
+ m.appendReplacement(sb, "?");
+ // We will use prepared statements, so we don't need to escape quotes.
+ // And we assume the users had 'That''s a string with quotes' in their configs.
+ literal = literal.replaceAll("''", "'");
+ argPatterns.add(literal);
+ args.add(new PatternParser(literal).parse());
+ }
+ m.appendTail(sb);
+ parameterizedSql = sb.toString();
+ lastPattern = pattern;
+ }
+
+ public void setParameters(PreparedStatement ps, LoggingEvent logEvent) throws SQLException {
+ for (int i = 0; i < args.size(); i++) {
+ buffer.setLength(0);
+ PatternConverter c = args.get(i);
+ while (c != null) {
+ c.format(buffer, logEvent);
+ c = c.next;
+ }
+ ps.setString(i + 1, buffer.toString());
+ }
+ // This clears "toString cache"
+ buffer.setLength(0);
+ if (buffer.capacity() > 100000) {
+ // Avoid leaking too much memory if we discover long parameter
+ buffer = new StringBuffer();
+ }
+ }
+}
diff --git a/src/test/input/jdbc_h2_bufferSize1.properties b/src/test/input/jdbc_h2_bufferSize1.properties
new file mode 100644
index 0000000..77e6083
--- /dev/null
+++ b/src/test/input/jdbc_h2_bufferSize1.properties
@@ -0,0 +1,25 @@
+# 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.
+
+log4j.threshold=ALL
+log4j.rootLogger=ALL,A
+log4j.appender.A=org.apache.log4j.jdbc.JDBCAppender
+log4j.appender.A.URL=jdbc:h2:mem:test_db
+log4j.appender.A.driver=org.h2.Driver
+log4j.appender.A.bufferSize=1
+log4j.appender.A.user=
+log4j.appender.A.password=
+log4j.appender.A.layout=org.apache.log4j.PatternLayout
+log4j.appender.A.sql=insert into logs(level,location,message,message2) values('%p','%c','%m', ' %c %p %m')
diff --git a/src/test/input/jdbc_h2_bufferSize2.properties b/src/test/input/jdbc_h2_bufferSize2.properties
new file mode 100644
index 0000000..fba02a3
--- /dev/null
+++ b/src/test/input/jdbc_h2_bufferSize2.properties
@@ -0,0 +1,25 @@
+# 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.
+
+log4j.threshold=ALL
+log4j.rootLogger=ALL,A
+log4j.appender.A=org.apache.log4j.jdbc.JDBCAppender
+log4j.appender.A.URL=jdbc:h2:mem:test_db
+log4j.appender.A.driver=org.h2.Driver
+log4j.appender.A.bufferSize=2
+log4j.appender.A.user=
+log4j.appender.A.password=
+log4j.appender.A.layout=org.apache.log4j.PatternLayout
+log4j.appender.A.sql=insert into logs(level,location,message,message2) values('%p','%c','%m', ' %c %p %m')
diff --git a/src/test/java/org/apache/log4j/jdbc/JdbcAppenderTest.java b/src/test/java/org/apache/log4j/jdbc/JdbcAppenderTest.java
new file mode 100644
index 0000000..f851063
--- /dev/null
+++ b/src/test/java/org/apache/log4j/jdbc/JdbcAppenderTest.java
@@ -0,0 +1,208 @@
+/*
+ * 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.log4j.jdbc;
+
+import org.apache.log4j.Appender;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.apache.log4j.PropertyConfigurator;
+import org.apache.log4j.TestContants;
+import org.apache.log4j.VectorErrorHandler;
+import org.apache.log4j.xml.XLevel;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class JdbcAppenderTest {
+ // Closing the last connection to "in-memory h2" removes the database
+ // So we keep the connection during the test
+ // The logger opens its own connection
+ Connection con;
+
+ @Before
+ public void setup() throws SQLException {
+ con = DriverManager.getConnection("jdbc:h2:mem:test_db");
+ Statement st = con.createStatement();
+ try {
+ st.execute("create table logs(level varchar(100),location varchar(100),message varchar(100),message2 varchar(100))");
+ } finally {
+ st.close();
+ }
+ }
+
+ @After
+ public void cleanup() throws SQLException {
+ LogManager.shutdown();
+ con.close();
+ }
+
+ @Test
+ public void verifyJdbcInsecure() throws SQLException {
+ String secureJdbcReplacement = "org.apache.log4j.jdbc.JDBCAppender.secure_jdbc_replacement";
+ System.setProperty(secureJdbcReplacement, "false");
+ try {
+ PropertyConfigurator.configure(TestContants.TEST_INPUT_PREFIX + "jdbc_h2_bufferSize1.properties");
+ Appender jdbcAppender = LogManager.getRootLogger().getAppender("A");
+ // Keep errors
+ VectorErrorHandler errorHandler = new VectorErrorHandler();
+ jdbcAppender.setErrorHandler(errorHandler);
+
+ Logger logger = Logger.getLogger(JdbcAppenderTest.class);
+
+ String oldThreadName = Thread.currentThread().getName();
+ try {
+ Thread.currentThread().setName("main");
+ logger.debug("message with '' quote");
+ Assert.assertEquals(
+ "batchSize=1, so messages should be added immediately",
+ "DEBUG; org.apache.log4j.jdbc.JdbcAppenderTest; message with ' quote; org.apache.log4j.jdbc.JdbcAppenderTest DEBUG message with ' quote\n",
+ joinSorted(getMessages()));
+
+ // It should fail
+ logger.fatal("message with ' quote");
+
+ Assert.assertEquals(
+ "Inserting a message with ' should cause failure in insecure mode, so only one message should be in the DB",
+ "DEBUG; org.apache.log4j.jdbc.JdbcAppenderTest; message with ' quote; org.apache.log4j.jdbc.JdbcAppenderTest DEBUG message with ' quote\n",
+ joinSorted(getMessages()));
+
+ StringBuilder exceptions = new StringBuilder();
+ StringBuilder errorCodes = new StringBuilder();
+ for (int i = 0; i < errorHandler.size(); i++) {
+ Exception ex = errorHandler.getException(i);
+ exceptions.append(ex.toString());
+ errorCodes.append(
+ ex instanceof SQLException
+ ? ("SQLException.getErrorCode() = " + ((SQLException) ex).getErrorCode())
+ : ("SQL Exception expected, got " + ex.getClass()));
+ exceptions.append('\n');
+ errorCodes.append('\n');
+ }
+ Assert.assertEquals(
+ "Logging a message with ' should trigger SQLException 'Syntax error in SQL statement...' when using insecure JDBCAppender mode, actual exceptions are\n" + exceptions,
+ "SQLException.getErrorCode() = 42001\n",
+ errorCodes.toString()
+ );
+ } finally {
+ Thread.currentThread().setName(oldThreadName);
+ }
+ } finally {
+ System.getProperties().remove(secureJdbcReplacement);
+ }
+ }
+
+ @Test
+ public void verifyJdbcBufferSize1() throws SQLException {
+ PropertyConfigurator.configure(TestContants.TEST_INPUT_PREFIX + "jdbc_h2_bufferSize1.properties");
+
+ Logger logger = Logger.getLogger(JdbcAppenderTest.class);
+
+ String oldThreadName = Thread.currentThread().getName();
+ try {
+ Thread.currentThread().setName("main");
+ logger.debug("message with ' quote");
+ Assert.assertEquals(
+ "batchSize=1, so messages should be added immediately",
+ "DEBUG; org.apache.log4j.jdbc.JdbcAppenderTest; message with ' quote; org.apache.log4j.jdbc.JdbcAppenderTest DEBUG message with ' quote\n",
+ joinSorted(getMessages()));
+
+ logger.fatal("message with \" quote");
+
+ Assert.assertEquals(
+ "batchSize=1, so two messages should be in DB after two logging calls",
+ "DEBUG; org.apache.log4j.jdbc.JdbcAppenderTest; message with ' quote; org.apache.log4j.jdbc.JdbcAppenderTest DEBUG message with ' quote\n" +
+ "FATAL; org.apache.log4j.jdbc.JdbcAppenderTest; message with \" quote; org.apache.log4j.jdbc.JdbcAppenderTest FATAL message with \" quote\n",
+ joinSorted(getMessages()));
+ } finally {
+ Thread.currentThread().setName(oldThreadName);
+ }
+ }
+
+ @Test
+ public void verifyJdbcBufferSize2() throws SQLException {
+ PropertyConfigurator.configure(TestContants.TEST_INPUT_PREFIX + "jdbc_h2_bufferSize2.properties");
+
+ Logger logger = Logger.getLogger(JdbcAppenderTest.class);
+
+ String oldThreadName = Thread.currentThread().getName();
+ try {
+ Thread.currentThread().setName("main");
+ logger.log(XLevel.TRACE, "xtrace message");
+ logger.debug("message with ' quote");
+ logger.info("message with \" quote");
+ logger.warn("?");
+ // bufferSize=2, so this message won't yet be stored to the db
+ logger.error("m4");
+
+ Assert.assertEquals(
+ "DEBUG; org.apache.log4j.jdbc.JdbcAppenderTest; message with ' quote; org.apache.log4j.jdbc.JdbcAppenderTest DEBUG message with ' quote\n" +
+ "INFO; org.apache.log4j.jdbc.JdbcAppenderTest; message with \" quote; org.apache.log4j.jdbc.JdbcAppenderTest INFO message with \" quote\n" +
+ "TRACE; org.apache.log4j.jdbc.JdbcAppenderTest; xtrace message; org.apache.log4j.jdbc.JdbcAppenderTest TRACE xtrace message\n" +
+ "WARN; org.apache.log4j.jdbc.JdbcAppenderTest; ?; org.apache.log4j.jdbc.JdbcAppenderTest WARN ?\n",
+ joinSorted(getMessages()));
+
+ logger.fatal("m5");
+
+ Assert.assertEquals(
+ "Logging m5 message should trigger buffer flush for both m4 and m5",
+ "DEBUG; org.apache.log4j.jdbc.JdbcAppenderTest; message with ' quote; org.apache.log4j.jdbc.JdbcAppenderTest DEBUG message with ' quote\n" +
+ "ERROR; org.apache.log4j.jdbc.JdbcAppenderTest; m4; org.apache.log4j.jdbc.JdbcAppenderTest ERROR m4\n" +
+ "FATAL; org.apache.log4j.jdbc.JdbcAppenderTest; m5; org.apache.log4j.jdbc.JdbcAppenderTest FATAL m5\n" +
+ "INFO; org.apache.log4j.jdbc.JdbcAppenderTest; message with \" quote; org.apache.log4j.jdbc.JdbcAppenderTest INFO message with \" quote\n" +
+ "TRACE; org.apache.log4j.jdbc.JdbcAppenderTest; xtrace message; org.apache.log4j.jdbc.JdbcAppenderTest TRACE xtrace message\n" +
+ "WARN; org.apache.log4j.jdbc.JdbcAppenderTest; ?; org.apache.log4j.jdbc.JdbcAppenderTest WARN ?\n",
+ joinSorted(getMessages()));
+ } finally {
+ Thread.currentThread().setName(oldThreadName);
+ }
+ }
+
+ private static String joinSorted(List<String> list) {
+ Collections.sort(list);
+ StringBuilder sb = new StringBuilder();
+ for (String s : list) {
+ sb.append(s).append('\n');
+ }
+ return sb.toString();
+ }
+
+ private List<String> getMessages() throws SQLException {
+ List<String> res = new ArrayList<String>();
+ PreparedStatement ps = con.prepareStatement("select level,location,message,message2 from logs");
+ ResultSet rs = ps.executeQuery();
+ try {
+ while (rs.next()) {
+ res.add(rs.getString(1) + "; " + rs.getString(2)
+ + "; " + rs.getString(3) + "; " + rs.getString(4));
+ }
+ } finally {
+ rs.close();
+ }
+ return res;
+ }
+}
diff --git a/src/test/java/org/apache/log4j/jdbc/JdbcPatternParserTest.java b/src/test/java/org/apache/log4j/jdbc/JdbcPatternParserTest.java
new file mode 100644
index 0000000..aafbd80
--- /dev/null
+++ b/src/test/java/org/apache/log4j/jdbc/JdbcPatternParserTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.log4j.jdbc;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class JdbcPatternParserTest {
+ JdbcPatternParser parser = new JdbcPatternParser();
+
+ @Test
+ public void testParameterizedSql() {
+ assertParameterizedSql(
+ "JdbcPatternParser{sql=INSERT INTO A1 (TITLE3) VALUES ( ? ),args=[ %d - %c %-5p %c %x - %m%n ]}",
+ "INSERT INTO A1 (TITLE3) VALUES ( ' %d - %c %-5p %c %x - %m%n ' )"
+ );
+ assertParameterizedSql(
+ "JdbcPatternParser{sql=INSERT INTO A1 (TITLE3) VALUES ( ?, ?, ?, ?, ?, ? ),args=[%d, %c, %-5p, '%c, %x, - %m%n ]}",
+ "INSERT INTO A1 (TITLE3) VALUES ( '%d', '%c', '%-5p', ' ''%c', '%x', ' - %m%n ' )"
+ );
+
+ assertParameterizedSql(
+ "JdbcPatternParser{sql=INSERT INTO A1 (TITLE3) VALUES ( ' just string literal', 'another literal with quotes '' asdf', ?),args=[message: %m]}",
+ "INSERT INTO A1 (TITLE3) VALUES ( ' just string literal', 'another literal with quotes '' asdf', 'message: %m')"
+ );
+ }
+
+ private void assertParameterizedSql(String expected, String input) {
+ parser.setPattern(input);
+ Assert.assertEquals(
+ "parser.setPattern(...).toString() for " + input,
+ expected,
+ parser.toString()
+ );
+ }
+}