Blob Blame History Raw
From 6370372f5a04cdec6598fa8199b762ce33fa4d40 Mon Sep 17 00:00:00 2001
From: Mikolaj Izdebski <mizdebsk@redhat.com>
Date: Wed, 2 Feb 2022 19:37:17 +0100
Subject: [PATCH] Fix CVE-2022-23305 JDBCAppender

---
 .../apache/log4j/helpers/PatternParser.java   | 944 +++++++++---------
 .../org/apache/log4j/jdbc/JDBCAppender.java   | 715 ++++++-------
 .../apache/log4j/jdbc/JdbcPatternParser.java  | 115 +++
 3 files changed, 942 insertions(+), 832 deletions(-)
 create mode 100644 src/main/java/org/apache/log4j/jdbc/JdbcPatternParser.java

diff --git a/src/main/java/org/apache/log4j/helpers/PatternParser.java b/src/main/java/org/apache/log4j/helpers/PatternParser.java
index 0d3ead67..4b169a2d 100644
--- a/src/main/java/org/apache/log4j/helpers/PatternParser.java
+++ b/src/main/java/org/apache/log4j/helpers/PatternParser.java
@@ -30,541 +30,503 @@ import java.util.Arrays;
 //                 Reinhard Deschler <reinhard.deschler@web.de>
 
 /**
-   Most of the work of the {@link org.apache.log4j.PatternLayout} class
-   is delegated to the PatternParser class.
-
-   <p>It is this class that parses conversion patterns and creates
-   a chained list of {@link OptionConverter OptionConverters}.
-
-   @author <a href=mailto:"cakalijp@Maritz.com">James P. Cakalic</a>
-   @author Ceki G&uuml;lc&uuml;
-   @author Anders Kristensen
-
-   @since 0.8.2
-*/
+ * Most of the work of the {@link org.apache.log4j.PatternLayout} class is
+ * delegated to the PatternParser class.
+ * 
+ * <p>
+ * It is this class that parses conversion patterns and creates a chained list
+ * of {@link OptionConverter OptionConverters}.
+ * 
+ * @author James P. Cakalic
+ * @author Ceki G&uuml;lc&uuml;
+ * @author Anders Kristensen
+ * 
+ * @since 0.8.2
+ */
 public class PatternParser {
 
-  private static final char ESCAPE_CHAR = '%';
-
-  private static final int LITERAL_STATE = 0;
-  private static final int CONVERTER_STATE = 1;
-  private static final int DOT_STATE = 3;
-  private static final int MIN_STATE = 4;
-  private static final int MAX_STATE = 5;
-
-  static final int FULL_LOCATION_CONVERTER = 1000;
-  static final int METHOD_LOCATION_CONVERTER = 1001;
-  static final int CLASS_LOCATION_CONVERTER = 1002;
-  static final int LINE_LOCATION_CONVERTER = 1003;
-  static final int FILE_LOCATION_CONVERTER = 1004;
-
-  static final int RELATIVE_TIME_CONVERTER = 2000;
-  static final int THREAD_CONVERTER = 2001;
-  static final int LEVEL_CONVERTER = 2002;
-  static final int NDC_CONVERTER = 2003;
-  static final int MESSAGE_CONVERTER = 2004;
-
-  int state;
-  protected StringBuffer currentLiteral = new StringBuffer(32);
-  protected int patternLength;
-  protected int i;
-  PatternConverter head;
-  PatternConverter tail;
-  protected FormattingInfo formattingInfo = new FormattingInfo();
-  protected String pattern;
-
-  public
-  PatternParser(String pattern) {
-    this.pattern = pattern;
-    patternLength =  pattern.length();
-    state = LITERAL_STATE;
-  }
-
-  private
-  void  addToList(PatternConverter pc) {
-    if(head == null) {
-      head = tail = pc;
-    } else {
-      tail.next = pc;
-      tail = pc;
+    private static final char LEFT_BRACKET = '{';
+    private static final char RIGHT_BRACKET = '}';
+    private static final char N_CHAR = 'n';
+    private static final char DOT_CHAR = '.';
+    private static final char DASH_CHAR = '-';
+    private static final char ESCAPE_CHAR = '%';
+
+    private static final int LITERAL_STATE = 0;
+    private static final int CONVERTER_STATE = 1;
+    private static final int DOT_STATE = 3;
+    private static final int MIN_STATE = 4;
+    private static final int MAX_STATE = 5;
+
+    static final int FULL_LOCATION_CONVERTER = 1000;
+    static final int METHOD_LOCATION_CONVERTER = 1001;
+    static final int CLASS_LOCATION_CONVERTER = 1002;
+    static final int LINE_LOCATION_CONVERTER = 1003;
+    static final int FILE_LOCATION_CONVERTER = 1004;
+
+    static final int RELATIVE_TIME_CONVERTER = 2000;
+    static final int THREAD_CONVERTER = 2001;
+    static final int LEVEL_CONVERTER = 2002;
+    static final int NDC_CONVERTER = 2003;
+    static final int MESSAGE_CONVERTER = 2004;
+
+    int state;
+    protected StringBuffer currentLiteral = new StringBuffer(32);
+    protected int patternLength;
+    protected int i;
+    PatternConverter head;
+    PatternConverter tail;
+    protected FormattingInfo formattingInfo = new FormattingInfo();
+    protected String pattern;
+
+    public PatternParser(String pattern) {
+	this.pattern = pattern;
+	patternLength = pattern.length();
+	state = LITERAL_STATE;
     }
-  }
-
-  protected
-  String extractOption() {
-    if((i < patternLength) && (pattern.charAt(i) == '{')) {
-      int end = pattern.indexOf('}', i);
-      if (end > i) {
-	String r = pattern.substring(i + 1, end);
-	i = end+1;
-	return r;
-      }
-    }
-    return null;
-  }
-
-
-  /**
-     The option is expected to be in decimal and positive. In case of
-     error, zero is returned.  */
-  protected
-  int extractPrecisionOption() {
-    String opt = extractOption();
-    int r = 0;
-    if(opt != null) {
-      try {
-	r = Integer.parseInt(opt);
-	if(r <= 0) {
-	    LogLog.error(
-	        "Precision option (" + opt + ") isn't a positive integer.");
-	    r = 0;
+
+    private void addToList(PatternConverter pc) {
+	if (head == null) {
+	    head = tail = pc;
+	} else {
+	    tail.next = pc;
+	    tail = pc;
 	}
-      }
-      catch (NumberFormatException e) {
-	LogLog.error("Category option \""+opt+"\" not a decimal integer.", e);
-      }
     }
-    return r;
-  }
-
-  public
-  PatternConverter parse() {
-    char c;
-    i = 0;
-    while(i < patternLength) {
-      c = pattern.charAt(i++);
-      switch(state) {
-      case LITERAL_STATE:
-        // In literal state, the last char is always a literal.
-        if(i == patternLength) {
-          currentLiteral.append(c);
-          continue;
-        }
-        if(c == ESCAPE_CHAR) {
-          // peek at the next char.
-          switch(pattern.charAt(i)) {
-          case ESCAPE_CHAR:
-            currentLiteral.append(c);
-            i++; // move pointer
-            break;
-          case 'n':
-            currentLiteral.append(Layout.LINE_SEP);
-            i++; // move pointer
-            break;
-          default:
-            if(currentLiteral.length() != 0) {
-              addToList(new LiteralPatternConverter(
-                                                  currentLiteral.toString()));
-              //LogLog.debug("Parsed LITERAL converter: \""
-              //           +currentLiteral+"\".");
-            }
-            currentLiteral.setLength(0);
-            currentLiteral.append(c); // append %
-            state = CONVERTER_STATE;
-            formattingInfo.reset();
-          }
-        }
-        else {
-          currentLiteral.append(c);
-        }
-        break;
-      case CONVERTER_STATE:
-	currentLiteral.append(c);
-	switch(c) {
-	case '-':
-	  formattingInfo.leftAlign = true;
-	  break;
-	case '.':
-	  state = DOT_STATE;
-	  break;
-	default:
-	  if(c >= '0' && c <= '9') {
-	    formattingInfo.min = c - '0';
-	    state = MIN_STATE;
-	  }
-	  else
-	    finalizeConverter(c);
-	} // switch
-	break;
-      case MIN_STATE:
-	currentLiteral.append(c);
-	if(c >= '0' && c <= '9')
-	  formattingInfo.min = formattingInfo.min*10 + (c - '0');
-	else if(c == '.')
-	  state = DOT_STATE;
-	else {
-	  finalizeConverter(c);
-	}
-	break;
-      case DOT_STATE:
-	currentLiteral.append(c);
-	if(c >= '0' && c <= '9') {
-	  formattingInfo.max = c - '0';
-	   state = MAX_STATE;
-	}
-	else {
-	  LogLog.error("Error occured in position "+i
-		     +".\n Was expecting digit, instead got char \""+c+"\".");
-	  state = LITERAL_STATE;
-	}
-	break;
-      case MAX_STATE:
-	currentLiteral.append(c);
-	if(c >= '0' && c <= '9')
-	  formattingInfo.max = formattingInfo.max*10 + (c - '0');
-	else {
-	  finalizeConverter(c);
-	  state = LITERAL_STATE;
+
+    protected String extractOption() {
+	if ((i < patternLength) && (pattern.charAt(i) == LEFT_BRACKET)) {
+	    int end = pattern.indexOf(RIGHT_BRACKET, i);
+	    if (end > i) {
+		String r = pattern.substring(i + 1, end);
+		i = end + 1;
+		return r;
+	    }
 	}
-	break;
-      } // switch
-    } // while
-    if(currentLiteral.length() != 0) {
-      addToList(new LiteralPatternConverter(currentLiteral.toString()));
-      //LogLog.debug("Parsed LITERAL converter: \""+currentLiteral+"\".");
+	return null;
     }
-    return head;
-  }
-
-  protected
-  void finalizeConverter(char c) {
-    PatternConverter pc = null;
-    switch(c) {
-    case 'c':
-      pc = new CategoryPatternConverter(formattingInfo,
-					extractPrecisionOption());
-      //LogLog.debug("CATEGORY converter.");
-      //formattingInfo.dump();
-      currentLiteral.setLength(0);
-      break;
-    case 'C':
-      pc = new ClassNamePatternConverter(formattingInfo,
-					 extractPrecisionOption());
-      //LogLog.debug("CLASS_NAME converter.");
-      //formattingInfo.dump();
-      currentLiteral.setLength(0);
-      break;
-    case 'd':
-      String dateFormatStr = AbsoluteTimeDateFormat.ISO8601_DATE_FORMAT;
-      DateFormat df;
-      String dOpt = extractOption();
-      if(dOpt != null)
-	dateFormatStr = dOpt;
-
-      if(dateFormatStr.equalsIgnoreCase(
-                                    AbsoluteTimeDateFormat.ISO8601_DATE_FORMAT))
-	df = new  ISO8601DateFormat();
-      else if(dateFormatStr.equalsIgnoreCase(
-                                   AbsoluteTimeDateFormat.ABS_TIME_DATE_FORMAT))
-	df = new AbsoluteTimeDateFormat();
-      else if(dateFormatStr.equalsIgnoreCase(
-                              AbsoluteTimeDateFormat.DATE_AND_TIME_DATE_FORMAT))
-	df = new DateTimeDateFormat();
-      else {
-	try {
-	  df = new SimpleDateFormat(dateFormatStr);
-	}
-	catch (IllegalArgumentException e) {
-	  LogLog.error("Could not instantiate SimpleDateFormat with " +
-		       dateFormatStr, e);
-	  df = (DateFormat) OptionConverter.instantiateByClassName(
-			           "org.apache.log4j.helpers.ISO8601DateFormat",
-				   DateFormat.class, null);
+
+    /**
+     * The option is expected to be in decimal and positive. In case of error, zero
+     * is returned.
+     */
+    protected int extractPrecisionOption() {
+	String opt = extractOption();
+	int r = 0;
+	if (opt != null) {
+	    try {
+		r = Integer.parseInt(opt);
+		if (r <= 0) {
+		    LogLog.error("Precision option (" + opt + ") isn't a positive integer.");
+		    r = 0;
+		}
+	    } catch (NumberFormatException e) {
+		LogLog.error("Category option \"" + opt + "\" not a decimal integer.", e);
+	    }
 	}
-      }
-      pc = new DatePatternConverter(formattingInfo, df);
-      //LogLog.debug("DATE converter {"+dateFormatStr+"}.");
-      //formattingInfo.dump();
-      currentLiteral.setLength(0);
-      break;
-    case 'F':
-      pc = new LocationPatternConverter(formattingInfo,
-					FILE_LOCATION_CONVERTER);
-      //LogLog.debug("File name converter.");
-      //formattingInfo.dump();
-      currentLiteral.setLength(0);
-      break;
-    case 'l':
-      pc = new LocationPatternConverter(formattingInfo,
-					FULL_LOCATION_CONVERTER);
-      //LogLog.debug("Location converter.");
-      //formattingInfo.dump();
-      currentLiteral.setLength(0);
-      break;
-    case 'L':
-      pc = new LocationPatternConverter(formattingInfo,
-					LINE_LOCATION_CONVERTER);
-      //LogLog.debug("LINE NUMBER converter.");
-      //formattingInfo.dump();
-      currentLiteral.setLength(0);
-      break;
-    case 'm':
-      pc = new BasicPatternConverter(formattingInfo, MESSAGE_CONVERTER);
-      //LogLog.debug("MESSAGE converter.");
-      //formattingInfo.dump();
-      currentLiteral.setLength(0);
-      break;
-    case 'M':
-      pc = new LocationPatternConverter(formattingInfo,
-					METHOD_LOCATION_CONVERTER);
-      //LogLog.debug("METHOD converter.");
-      //formattingInfo.dump();
-      currentLiteral.setLength(0);
-      break;
-    case 'p':
-      pc = new BasicPatternConverter(formattingInfo, LEVEL_CONVERTER);
-      //LogLog.debug("LEVEL converter.");
-      //formattingInfo.dump();
-      currentLiteral.setLength(0);
-      break;
-    case 'r':
-      pc = new BasicPatternConverter(formattingInfo,
-					 RELATIVE_TIME_CONVERTER);
-      //LogLog.debug("RELATIVE time converter.");
-      //formattingInfo.dump();
-      currentLiteral.setLength(0);
-      break;
-    case 't':
-      pc = new BasicPatternConverter(formattingInfo, THREAD_CONVERTER);
-      //LogLog.debug("THREAD converter.");
-      //formattingInfo.dump();
-      currentLiteral.setLength(0);
-      break;
-      /*case 'u':
-      if(i < patternLength) {
-	char cNext = pattern.charAt(i);
-	if(cNext >= '0' && cNext <= '9') {
-	  pc = new UserFieldPatternConverter(formattingInfo, cNext - '0');
-	  LogLog.debug("USER converter ["+cNext+"].");
-	  formattingInfo.dump();
-	  currentLiteral.setLength(0);
-	  i++;
+	return r;
+    }
+
+    public PatternConverter parse() {
+	char c;
+	i = 0;
+	while (i < patternLength) {
+	    c = pattern.charAt(i++);
+	    switch (state) {
+	    case LITERAL_STATE:
+		// In literal state, the last char is always a literal.
+		if (i == patternLength) {
+		    currentLiteral.append(c);
+		    continue;
+		}
+		if (c == ESCAPE_CHAR) {
+		    // peek at the next char.
+		    switch (pattern.charAt(i)) {
+		    case ESCAPE_CHAR:
+			currentLiteral.append(c);
+			i++; // move pointer
+			break;
+		    case N_CHAR:
+			currentLiteral.append(Layout.LINE_SEP);
+			i++; // move pointer
+			break;
+		    default:
+			if (currentLiteral.length() != 0) {
+			    addToList(new LiteralPatternConverter(currentLiteral.toString()));
+			    // LogLog.debug("Parsed LITERAL converter: \""
+			    // +currentLiteral+"\".");
+			}
+			currentLiteral.setLength(0);
+			currentLiteral.append(c); // append %
+			state = CONVERTER_STATE;
+			formattingInfo.reset();
+		    }
+		} else {
+		    currentLiteral.append(c);
+		}
+		break;
+	    case CONVERTER_STATE:
+		currentLiteral.append(c);
+		switch (c) {
+		case DASH_CHAR:
+		    formattingInfo.leftAlign = true;
+		    break;
+		case DOT_CHAR:
+		    state = DOT_STATE;
+		    break;
+		default:
+		    if (c >= '0' && c <= '9') {
+			formattingInfo.min = c - '0';
+			state = MIN_STATE;
+		    } else
+			finalizeConverter(c);
+		} // switch
+		break;
+	    case MIN_STATE:
+		currentLiteral.append(c);
+		if (c >= '0' && c <= '9')
+		    formattingInfo.min = formattingInfo.min * 10 + (c - '0');
+		else if (c == '.')
+		    state = DOT_STATE;
+		else {
+		    finalizeConverter(c);
+		}
+		break;
+	    case DOT_STATE:
+		currentLiteral.append(c);
+		if (c >= '0' && c <= '9') {
+		    formattingInfo.max = c - '0';
+		    state = MAX_STATE;
+		} else {
+		    LogLog.error("Error occured in position " + i + ".\n Was expecting digit, instead got char \"" + c
+			    + "\".");
+		    state = LITERAL_STATE;
+		}
+		break;
+	    case MAX_STATE:
+		currentLiteral.append(c);
+		if (c >= '0' && c <= '9')
+		    formattingInfo.max = formattingInfo.max * 10 + (c - '0');
+		else {
+		    finalizeConverter(c);
+		    state = LITERAL_STATE;
+		}
+		break;
+	    } // switch
+	} // while
+	if (currentLiteral.length() != 0) {
+	    addToList(new LiteralPatternConverter(currentLiteral.toString()));
+	    // LogLog.debug("Parsed LITERAL converter: \""+currentLiteral+"\".");
 	}
-	else
-	  LogLog.error("Unexpected char" +cNext+" at position "+i);
-      }
-      break;*/
-    case 'x':
-      pc = new BasicPatternConverter(formattingInfo, NDC_CONVERTER);
-      //LogLog.debug("NDC converter.");
-      currentLiteral.setLength(0);
-      break;
-    case 'X':
-      String xOpt = extractOption();
-      pc = new MDCPatternConverter(formattingInfo, xOpt);
-      currentLiteral.setLength(0);
-      break;
-    default:
-      LogLog.error("Unexpected char [" +c+"] at position "+i
-		   +" in conversion patterrn.");
-      pc = new LiteralPatternConverter(currentLiteral.toString());
-      currentLiteral.setLength(0);
+	return head;
     }
 
-    addConverter(pc);
-  }
-
-  protected
-  void addConverter(PatternConverter pc) {
-    currentLiteral.setLength(0);
-    // Add the pattern converter to the list.
-    addToList(pc);
-    // Next pattern is assumed to be a literal.
-    state = LITERAL_STATE;
-    // Reset formatting info
-    formattingInfo.reset();
-  }
-
-  // ---------------------------------------------------------------------
-  //                      PatternConverters
-  // ---------------------------------------------------------------------
-
-  private static class BasicPatternConverter extends PatternConverter {
-    int type;
-
-    BasicPatternConverter(FormattingInfo formattingInfo, int type) {
-      super(formattingInfo);
-      this.type = type;
+    protected void finalizeConverter(char c) {
+	PatternConverter pc = null;
+	switch (c) {
+	case 'c':
+	    pc = new CategoryPatternConverter(formattingInfo, extractPrecisionOption());
+	    // LogLog.debug("CATEGORY converter.");
+	    // formattingInfo.dump();
+	    currentLiteral.setLength(0);
+	    break;
+	case 'C':
+	    pc = new ClassNamePatternConverter(formattingInfo, extractPrecisionOption());
+	    // LogLog.debug("CLASS_NAME converter.");
+	    // formattingInfo.dump();
+	    currentLiteral.setLength(0);
+	    break;
+	case 'd':
+	    String dateFormatStr = AbsoluteTimeDateFormat.ISO8601_DATE_FORMAT;
+	    DateFormat df;
+	    String dOpt = extractOption();
+	    if (dOpt != null)
+		dateFormatStr = dOpt;
+
+	    if (dateFormatStr.equalsIgnoreCase(AbsoluteTimeDateFormat.ISO8601_DATE_FORMAT))
+		df = new ISO8601DateFormat();
+	    else if (dateFormatStr.equalsIgnoreCase(AbsoluteTimeDateFormat.ABS_TIME_DATE_FORMAT))
+		df = new AbsoluteTimeDateFormat();
+	    else if (dateFormatStr.equalsIgnoreCase(AbsoluteTimeDateFormat.DATE_AND_TIME_DATE_FORMAT))
+		df = new DateTimeDateFormat();
+	    else {
+		try {
+		    df = new SimpleDateFormat(dateFormatStr);
+		} catch (IllegalArgumentException e) {
+		    LogLog.error("Could not instantiate SimpleDateFormat with " + dateFormatStr, e);
+		    df = (DateFormat) OptionConverter.instantiateByClassName(
+			    "org.apache.log4j.helpers.ISO8601DateFormat", DateFormat.class, null);
+		}
+	    }
+	    pc = new DatePatternConverter(formattingInfo, df);
+	    // LogLog.debug("DATE converter {"+dateFormatStr+"}.");
+	    // formattingInfo.dump();
+	    currentLiteral.setLength(0);
+	    break;
+	case 'F':
+	    pc = new LocationPatternConverter(formattingInfo, FILE_LOCATION_CONVERTER);
+	    // LogLog.debug("File name converter.");
+	    // formattingInfo.dump();
+	    currentLiteral.setLength(0);
+	    break;
+	case 'l':
+	    pc = new LocationPatternConverter(formattingInfo, FULL_LOCATION_CONVERTER);
+	    // LogLog.debug("Location converter.");
+	    // formattingInfo.dump();
+	    currentLiteral.setLength(0);
+	    break;
+	case 'L':
+	    pc = new LocationPatternConverter(formattingInfo, LINE_LOCATION_CONVERTER);
+	    // LogLog.debug("LINE NUMBER converter.");
+	    // formattingInfo.dump();
+	    currentLiteral.setLength(0);
+	    break;
+	case 'm':
+	    pc = new BasicPatternConverter(formattingInfo, MESSAGE_CONVERTER);
+	    // LogLog.debug("MESSAGE converter.");
+	    // formattingInfo.dump();
+	    currentLiteral.setLength(0);
+	    break;
+	case 'M':
+	    pc = new LocationPatternConverter(formattingInfo, METHOD_LOCATION_CONVERTER);
+	    // LogLog.debug("METHOD converter.");
+	    // formattingInfo.dump();
+	    currentLiteral.setLength(0);
+	    break;
+	case 'p':
+	    pc = new BasicPatternConverter(formattingInfo, LEVEL_CONVERTER);
+	    // LogLog.debug("LEVEL converter.");
+	    // formattingInfo.dump();
+	    currentLiteral.setLength(0);
+	    break;
+	case 'r':
+	    pc = new BasicPatternConverter(formattingInfo, RELATIVE_TIME_CONVERTER);
+	    // LogLog.debug("RELATIVE time converter.");
+	    // formattingInfo.dump();
+	    currentLiteral.setLength(0);
+	    break;
+	case 't':
+	    pc = new BasicPatternConverter(formattingInfo, THREAD_CONVERTER);
+	    // LogLog.debug("THREAD converter.");
+	    // formattingInfo.dump();
+	    currentLiteral.setLength(0);
+	    break;
+	/*
+	 * case 'u': if(i < patternLength) { char cNext = pattern.charAt(i); if(cNext >=
+	 * '0' && cNext <= '9') { pc = new UserFieldPatternConverter(formattingInfo,
+	 * cNext - '0'); LogLog.debug("USER converter ["+cNext+"].");
+	 * formattingInfo.dump(); currentLiteral.setLength(0); i++; } else
+	 * LogLog.error("Unexpected char" +cNext+" at position "+i); } break;
+	 */
+	case 'x':
+	    pc = new BasicPatternConverter(formattingInfo, NDC_CONVERTER);
+	    // LogLog.debug("NDC converter.");
+	    currentLiteral.setLength(0);
+	    break;
+	case 'X':
+	    String xOpt = extractOption();
+	    pc = new MDCPatternConverter(formattingInfo, xOpt);
+	    currentLiteral.setLength(0);
+	    break;
+	default:
+	    LogLog.error("Unexpected char [" + c + "] at position " + i + " in conversion patterrn.");
+	    pc = new LiteralPatternConverter(currentLiteral.toString());
+	    currentLiteral.setLength(0);
+	}
+
+	addConverter(pc);
     }
 
-    public
-    String convert(LoggingEvent event) {
-      switch(type) {
-      case RELATIVE_TIME_CONVERTER:
-	return (Long.toString(event.timeStamp - LoggingEvent.getStartTime()));
-      case THREAD_CONVERTER:
-	return event.getThreadName();
-      case LEVEL_CONVERTER:
-	return event.getLevel().toString();
-      case NDC_CONVERTER:
-	return event.getNDC();
-      case MESSAGE_CONVERTER: {
-	return event.getRenderedMessage();
-      }
-      default: return null;
-      }
+    protected void addConverter(PatternConverter pc) {
+	currentLiteral.setLength(0);
+	// Add the pattern converter to the list.
+	addToList(pc);
+	// Next pattern is assumed to be a literal.
+	state = LITERAL_STATE;
+	// Reset formatting info
+	formattingInfo.reset();
     }
-  }
 
-  private static class LiteralPatternConverter extends PatternConverter {
-    private String literal;
+    // ---------------------------------------------------------------------
+    // PatternConverters
+    // ---------------------------------------------------------------------
 
-    LiteralPatternConverter(String value) {
-      literal = value;
-    }
+    private static class BasicPatternConverter extends PatternConverter {
+	int type;
 
-    public
-    final
-    void format(StringBuffer sbuf, LoggingEvent event) {
-      sbuf.append(literal);
-    }
+	BasicPatternConverter(FormattingInfo formattingInfo, int type) {
+	    super(formattingInfo);
+	    this.type = type;
+	}
 
-    public
-    String convert(LoggingEvent event) {
-      return literal;
+	public String convert(LoggingEvent event) {
+	    switch (type) {
+	    case RELATIVE_TIME_CONVERTER:
+		return (Long.toString(event.timeStamp - LoggingEvent.getStartTime()));
+	    case THREAD_CONVERTER:
+		return event.getThreadName();
+	    case LEVEL_CONVERTER:
+		return event.getLevel().toString();
+	    case NDC_CONVERTER:
+		return event.getNDC();
+	    case MESSAGE_CONVERTER: {
+		return event.getRenderedMessage();
+	    }
+	    default:
+		return null;
+	    }
+	}
     }
-  }
 
-  private static class DatePatternConverter extends PatternConverter {
-    private DateFormat df;
-    private Date date;
+    private static class LiteralPatternConverter extends PatternConverter {
+	private String literal;
 
-    DatePatternConverter(FormattingInfo formattingInfo, DateFormat df) {
-      super(formattingInfo);
-      date = new Date();
-      this.df = df;
-    }
+	LiteralPatternConverter(String value) {
+	    literal = value;
+	}
 
-    public
-    String convert(LoggingEvent event) {
-      date.setTime(event.timeStamp);
-      String converted = null;
-      try {
-        converted = df.format(date);
-      }
-      catch (Exception ex) {
-        LogLog.error("Error occured while converting date.", ex);
-      }
-      return converted;
+	public final void format(StringBuffer sbuf, LoggingEvent event) {
+	    sbuf.append(literal);
+	}
+
+	public String convert(LoggingEvent event) {
+	    return literal;
+	}
     }
-  }
 
-  private static class MDCPatternConverter extends PatternConverter {
-    private String key;
+    private static class DatePatternConverter extends PatternConverter {
+	private DateFormat df;
+	private Date date;
 
-    MDCPatternConverter(FormattingInfo formattingInfo, String key) {
-      super(formattingInfo);
-      this.key = key;
-    }
+	DatePatternConverter(FormattingInfo formattingInfo, DateFormat df) {
+	    super(formattingInfo);
+	    date = new Date();
+	    this.df = df;
+	}
 
-    public
-    String convert(LoggingEvent event) {
-      if (key == null) {
-          StringBuffer buf = new StringBuffer("{");
-          Map properties = event.getProperties();
-          if (properties.size() > 0) {
-            Object[] keys = properties.keySet().toArray();
-            Arrays.sort(keys);
-            for (int i = 0; i < keys.length; i++) {
-                buf.append('{');
-                buf.append(keys[i]);
-                buf.append(',');
-                buf.append(properties.get(keys[i]));
-                buf.append('}');
-            }
-          }
-          buf.append('}');
-          return buf.toString();
-      } else {
-        Object val = event.getMDC(key);
-        if(val == null) {
-	        return null;
-        } else {
-	        return val.toString();
-        }
-      }
+	public String convert(LoggingEvent event) {
+	    date.setTime(event.timeStamp);
+	    String converted = null;
+	    try {
+		converted = df.format(date);
+	    } catch (Exception ex) {
+		LogLog.error("Error occured while converting date.", ex);
+	    }
+	    return converted;
+	}
     }
-  }
 
+    private static class MDCPatternConverter extends PatternConverter {
+	private String key;
 
-  private class LocationPatternConverter extends PatternConverter {
-    int type;
+	MDCPatternConverter(FormattingInfo formattingInfo, String key) {
+	    super(formattingInfo);
+	    this.key = key;
+	}
 
-    LocationPatternConverter(FormattingInfo formattingInfo, int type) {
-      super(formattingInfo);
-      this.type = type;
+	public String convert(LoggingEvent event) {
+	    if (key == null) {
+		StringBuffer buf = new StringBuffer("{");
+		Map properties = event.getProperties();
+		if (properties.size() > 0) {
+		    Object[] keys = properties.keySet().toArray();
+		    Arrays.sort(keys);
+		    for (int i = 0; i < keys.length; i++) {
+			buf.append('{');
+			buf.append(keys[i]);
+			buf.append(',');
+			buf.append(properties.get(keys[i]));
+			buf.append('}');
+		    }
+		}
+		buf.append('}');
+		return buf.toString();
+	    } else {
+		Object val = event.getMDC(key);
+		if (val == null) {
+		    return null;
+		} else {
+		    return val.toString();
+		}
+	    }
+	}
     }
 
-    public
-    String convert(LoggingEvent event) {
-      LocationInfo locationInfo = event.getLocationInformation();
-      switch(type) {
-      case FULL_LOCATION_CONVERTER:
-	return locationInfo.fullInfo;
-      case METHOD_LOCATION_CONVERTER:
-	return locationInfo.getMethodName();
-      case LINE_LOCATION_CONVERTER:
-	return locationInfo.getLineNumber();
-      case FILE_LOCATION_CONVERTER:
-	return locationInfo.getFileName();
-      default: return null;
-      }
-    }
-  }
+    private class LocationPatternConverter extends PatternConverter {
+	int type;
 
-  private static abstract class NamedPatternConverter extends PatternConverter {
-    int precision;
+	LocationPatternConverter(FormattingInfo formattingInfo, int type) {
+	    super(formattingInfo);
+	    this.type = type;
+	}
 
-    NamedPatternConverter(FormattingInfo formattingInfo, int precision) {
-      super(formattingInfo);
-      this.precision =  precision;
+	public String convert(LoggingEvent event) {
+	    LocationInfo locationInfo = event.getLocationInformation();
+	    switch (type) {
+	    case FULL_LOCATION_CONVERTER:
+		return locationInfo.fullInfo;
+	    case METHOD_LOCATION_CONVERTER:
+		return locationInfo.getMethodName();
+	    case LINE_LOCATION_CONVERTER:
+		return locationInfo.getLineNumber();
+	    case FILE_LOCATION_CONVERTER:
+		return locationInfo.getFileName();
+	    default:
+		return null;
+	    }
+	}
     }
 
-    abstract
-    String getFullyQualifiedName(LoggingEvent event);
-
-    public
-    String convert(LoggingEvent event) {
-      String n = getFullyQualifiedName(event);
-      if(precision <= 0)
-	return n;
-      else {
-	int len = n.length();
-
-	// We substract 1 from 'len' when assigning to 'end' to avoid out of
-	// bounds exception in return r.substring(end+1, len). This can happen if
-	// precision is 1 and the category name ends with a dot.
-	int end = len -1 ;
-	for(int i = precision; i > 0; i--) {
-	  end = n.lastIndexOf('.', end-1);
-	  if(end == -1)
-	    return n;
+    private static abstract class NamedPatternConverter extends PatternConverter {
+	int precision;
+
+	NamedPatternConverter(FormattingInfo formattingInfo, int precision) {
+	    super(formattingInfo);
+	    this.precision = precision;
+	}
+
+	abstract String getFullyQualifiedName(LoggingEvent event);
+
+	public String convert(LoggingEvent event) {
+	    String n = getFullyQualifiedName(event);
+	    if (precision <= 0)
+		return n;
+	    else {
+		int len = n.length();
+
+		// We substract 1 from 'len' when assigning to 'end' to avoid out of
+		// bounds exception in return r.substring(end+1, len). This can happen if
+		// precision is 1 and the category name ends with a dot.
+		int end = len - 1;
+		for (int i = precision; i > 0; i--) {
+		    end = n.lastIndexOf('.', end - 1);
+		    if (end == -1)
+			return n;
+		}
+		return n.substring(end + 1, len);
+	    }
 	}
-	return n.substring(end+1, len);
-      }
     }
-  }
 
-  private class ClassNamePatternConverter extends NamedPatternConverter {
+    private class ClassNamePatternConverter extends NamedPatternConverter {
 
-    ClassNamePatternConverter(FormattingInfo formattingInfo, int precision) {
-      super(formattingInfo, precision);
-    }
+	ClassNamePatternConverter(FormattingInfo formattingInfo, int precision) {
+	    super(formattingInfo, precision);
+	}
 
-    String getFullyQualifiedName(LoggingEvent event) {
-      return event.getLocationInformation().getClassName();
+	String getFullyQualifiedName(LoggingEvent event) {
+	    return event.getLocationInformation().getClassName();
+	}
     }
-  }
 
-  private class CategoryPatternConverter extends NamedPatternConverter {
+    private class CategoryPatternConverter extends NamedPatternConverter {
 
-    CategoryPatternConverter(FormattingInfo formattingInfo, int precision) {
-      super(formattingInfo, precision);
-    }
+	CategoryPatternConverter(FormattingInfo formattingInfo, int precision) {
+	    super(formattingInfo, precision);
+	}
 
-    String getFullyQualifiedName(LoggingEvent event) {
-      return event.getLoggerName();
+	String getFullyQualifiedName(LoggingEvent event) {
+	    return event.getLoggerName();
+	}
     }
-  }
 }
-
diff --git a/src/main/java/org/apache/log4j/jdbc/JDBCAppender.java b/src/main/java/org/apache/log4j/jdbc/JDBCAppender.java
index ad35f657..6ba9b66c 100644
--- a/src/main/java/org/apache/log4j/jdbc/JDBCAppender.java
+++ b/src/main/java/org/apache/log4j/jdbc/JDBCAppender.java
@@ -18,381 +18,414 @@ 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;
-import java.util.Iterator;
 
 import org.apache.log4j.PatternLayout;
+import org.apache.log4j.helpers.LogLog;
 import org.apache.log4j.spi.ErrorCode;
 import org.apache.log4j.spi.LoggingEvent;
 
-
 /**
-  The JDBCAppender provides for sending log events to a database.
-  
- <p><b><font color="#FF2222">WARNING: This version of JDBCAppender
- is very likely to be completely replaced in the future. Moreoever,
- it does not log exceptions</font></b>.
-
-  <p>Each append call adds to an <code>ArrayList</code> buffer.  When
-  the buffer is filled each log event is placed in a sql statement
-  (configurable) and executed.
-
-  <b>BufferSize</b>, <b>db URL</b>, <b>User</b>, & <b>Password</b> are
-  configurable options in the standard log4j ways.
-
-  <p>The <code>setSql(String sql)</code> sets the SQL statement to be
-  used for logging -- this statement is sent to a
-  <code>PatternLayout</code> (either created automaticly by the
-  appender or added by the user).  Therefore by default all the
-  conversion patterns in <code>PatternLayout</code> can be used
-  inside of the statement.  (see the test cases for examples)
-
-  <p>Overriding the {@link #getLogStatement} method allows more
-  explicit control of the statement used for logging.
-
-  <p>For use as a base class:
-
-    <ul>
-
-    <li>Override <code>getConnection()</code> to pass any connection
-    you want.  Typically this is used to enable application wide
-    connection pooling.
-
-     <li>Override <code>closeConnection(Connection con)</code> -- if
-     you override getConnection make sure to implement
-     <code>closeConnection</code> to handle the connection you
-     generated.  Typically this would return the connection to the
-     pool it came from.
-
-     <li>Override <code>getLogStatement(LoggingEvent event)</code> to
-     produce specialized or dynamic statements. The default uses the
-     sql option value.
-
-    </ul>
-
-    @author Kevin Steppe (<A HREF="mailto:ksteppe@pacbell.net">ksteppe@pacbell.net</A>)
-
-*/
-public class JDBCAppender extends org.apache.log4j.AppenderSkeleton
-    implements org.apache.log4j.Appender {
-
-  /**
-   * URL of the DB for default connection handling
-   */
-  protected String databaseURL = "jdbc:odbc:myDB";
-
-  /**
-   * User to connect as for default connection handling
-   */
-  protected String databaseUser = "me";
-
-  /**
-   * User to use for default connection handling
-   */
-  protected String databasePassword = "mypassword";
-
-  /**
-   * Connection used by default.  The connection is opened the first time it
-   * is needed and then held open until the appender is closed (usually at
-   * garbage collection).  This behavior is best modified by creating a
-   * sub-class and overriding the <code>getConnection</code> and
-   * <code>closeConnection</code> methods.
-   */
-  protected Connection connection = null;
-
-  /**
-   * Stores the string given to the pattern layout for conversion into a SQL
-   * statement, eg: insert into LogTable (Thread, Class, Message) values
-   * ("%t", "%c", "%m").
-   *
-   * Be careful of quotes in your messages!
-   *
-   * Also see PatternLayout.
-   */
-  protected String sqlStatement = "";
-
-  /**
-   * size of LoggingEvent buffer before writting to the database.
-   * Default is 1.
-   */
-  protected int bufferSize = 1;
-
-  /**
-   * ArrayList holding the buffer of Logging Events.
-   */
-  protected ArrayList buffer;
-
-  /**
-   * Helper object for clearing out the buffer
-   */
-  protected ArrayList removes;
-  
-  private boolean locationInfo = false;
-
-  public JDBCAppender() {
-    super();
-    buffer = new ArrayList(bufferSize);
-    removes = new ArrayList(bufferSize);
-  }
-
-  /**
-   * Gets whether the location of the logging request call
-   * should be captured.
-   *
-   * @since 1.2.16
-   * @return the current value of the <b>LocationInfo</b> option.
-   */
-  public boolean getLocationInfo() {
-    return locationInfo;
-  }
-  
-  /**
-   * The <b>LocationInfo</b> option takes a boolean value. By default, it is
-   * set to false which means there will be no effort to extract the location
-   * information related to the event. As a result, the event that will be
-   * ultimately logged will likely to contain the wrong location information
-   * (if present in the log format).
-   * <p/>
-   * <p/>
-   * Location information extraction is comparatively very slow and should be
-   * avoided unless performance is not a concern.
-   * </p>
-   * @since 1.2.16
-   * @param flag true if location information should be extracted.
-   */
-  public void setLocationInfo(final boolean flag) {
-    locationInfo = flag;
-  }
-  
-
-  /**
-   * Adds the event to the buffer.  When full the buffer is flushed.
-   */
-  public void append(LoggingEvent event) {
-    event.getNDC();
-    event.getThreadName();
-    // Get a copy of this thread's MDC.
-    event.getMDCCopy();
-    if (locationInfo) {
-      event.getLocationInformation();
-    }
-    event.getRenderedMessage();
-    event.getThrowableStrRep();
-    buffer.add(event);
-
-    if (buffer.size() >= bufferSize)
-      flushBuffer();
-  }
-
-  /**
-   * By default getLogStatement sends the event to the required Layout object.
-   * The layout will format the given pattern into a workable SQL string.
-   *
-   * Overriding this provides direct access to the LoggingEvent
-   * when constructing the logging statement.
-   *
-   */
-  protected String getLogStatement(LoggingEvent event) {
-    return getLayout().format(event);
-  }
-
-  /**
-   *
-   * Override this to provide an alertnate method of getting
-   * connections (such as caching).  One method to fix this is to open
-   * connections at the start of flushBuffer() and close them at the
-   * end.  I use a connection pool outside of JDBCAppender which is
-   * accessed in an override of this method.
-   * */
-  protected void execute(String sql) throws SQLException {
-
-    Connection con = null;
-    Statement stmt = null;
-
-    try {
-        con = getConnection();
-
-        stmt = con.createStatement();
-        stmt.executeUpdate(sql);
-    } finally {
-        if(stmt != null) {
-            stmt.close();
-        }
-        closeConnection(con);
+ * The JDBCAppender provides for sending log events to a database.
+ * 
+ * <p>
+ * <b><font color="#FF2222">WARNING: This version of JDBCAppender is very likely
+ * to be completely replaced in the future. Moreoever, it does not log
+ * exceptions</font></b>.
+ * 
+ * <p>
+ * Each append call adds to an <code>ArrayList</code> buffer. When the buffer is
+ * filled each log event is placed in a sql statement (configurable) and
+ * executed.
+ * 
+ * <b>BufferSize</b>, <b>db URL</b>, <b>User</b>, &amp; <b>Password</b> are
+ * configurable options in the standard log4j ways.
+ * 
+ * <p>
+ * The <code>setSql(String sql)</code> sets the SQL statement to be used for
+ * logging -- this statement is sent to a <code>PatternLayout</code> (either
+ * created automatically by the appender or added by the user). Therefore by
+ * default all the conversion patterns in <code>PatternLayout</code> can be used
+ * inside of the statement. (see the test cases for examples)
+ * 
+ * <p>
+ * Overriding the {@link #getLogStatement} method allows more explicit control
+ * of the statement used for logging.
+ * 
+ * <p>
+ * For use as a base class:
+ * 
+ * <ul>
+ * 
+ * <li>Override <code>getConnection()</code> to pass any connection you want.
+ * Typically this is used to enable application wide connection pooling.
+ * 
+ * <li>Override <code>closeConnection(Connection con)</code> -- if you override
+ * getConnection make sure to implement <code>closeConnection</code> to handle
+ * the connection you generated. Typically this would return the connection to
+ * the pool it came from.
+ * 
+ * <li>Override <code>getLogStatement(LoggingEvent event)</code> to produce
+ * specialized or dynamic statements. The default uses the sql option value.
+ * 
+ * </ul>
+ * 
+ * @author Kevin Steppe
+ *         (<A HREF="mailto:ksteppe@pacbell.net">ksteppe@pacbell.net</A>)
+ * 
+ */
+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
+     */
+    protected String databaseURL = "jdbc:odbc:myDB";
+
+    /**
+     * User to connect as for default connection handling
+     */
+    protected String databaseUser = "me";
+
+    /**
+     * User to use for default connection handling
+     */
+    protected String databasePassword = "mypassword";
+
+    /**
+     * Connection used by default. The connection is opened the first time it is
+     * needed and then held open until the appender is closed (usually at garbage
+     * collection). This behavior is best modified by creating a sub-class and
+     * overriding the <code>getConnection</code> and <code>closeConnection</code>
+     * methods.
+     */
+    protected Connection connection = null;
+
+    /**
+     * Stores the string given to the pattern layout for conversion into a SQL
+     * statement, eg: insert into LogTable (Thread, Class, Message) values ("%t",
+     * "%c", "%m").
+     *
+     * Be careful of quotes in your messages!
+     *
+     * Also see PatternLayout.
+     */
+    protected String sqlStatement = "";
+
+    private JdbcPatternParser preparedStatementParser;
+    /**
+     * size of LoggingEvent buffer before writting to the database. Default is 1.
+     */
+    protected int bufferSize = 1;
+
+    /**
+     * ArrayList holding the buffer of Logging Events.
+     */
+    protected ArrayList<LoggingEvent> buffer;
+
+    private boolean locationInfo = false;
+
+    private boolean isActive = false;
+
+    public JDBCAppender() {
+	super();
+	buffer = new ArrayList<LoggingEvent>(bufferSize);
     }
 
-    //System.out.println("Execute: " + sql);
-  }
-
-
-  /**
-   * Override this to return the connection to a pool, or to clean up the
-   * resource.
-   *
-   * The default behavior holds a single connection open until the appender
-   * is closed (typically when garbage collected).
-   */
-  protected void closeConnection(Connection con) {
-  }
-
-  /**
-   * Override this to link with your connection pooling system.
-   *
-   * By default this creates a single connection which is held open
-   * until the object is garbage collected.
-   */
-  protected Connection getConnection() throws SQLException {
-      if (!DriverManager.getDrivers().hasMoreElements())
-	     setDriver("sun.jdbc.odbc.JdbcOdbcDriver");
-
-      if (connection == null) {
-        connection = DriverManager.getConnection(databaseURL, databaseUser,
-					databasePassword);
-      }
-
-      return connection;
-  }
-
-  /**
-   * Closes the appender, flushing the buffer first then closing the default
-   * connection if it is open.
-   */
-  public void close()
-  {
-    flushBuffer();
-
-    try {
-      if (connection != null && !connection.isClosed())
-          connection.close();
-    } catch (SQLException e) {
-        errorHandler.error("Error closing connection", e, ErrorCode.GENERIC_FAILURE);
+    @Override
+    public void activateOptions() {
+
+	if (getSql() == null || getSql().trim().length() == 0) {
+	    LogLog.error("JDBCAppender.sql parameter is mandatory. Skipping all futher processing");
+	    isActive = false;
+	    return;
+	}
+
+	LogLog.debug("JDBCAppender constructing internal pattern parser");
+	preparedStatementParser = new JdbcPatternParser(getSql());
+	isActive = true;
     }
-    this.closed = true;
-  }
-
-  /**
-   * loops through the buffer of LoggingEvents, gets a
-   * sql string from getLogStatement() and sends it to execute().
-   * Errors are sent to the errorHandler.
-   *
-   * If a statement fails the LoggingEvent stays in the buffer!
-   */
-  public void flushBuffer() {
-    //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);
-      }
+
+    /**
+     * Gets whether the location of the logging request call should be captured.
+     *
+     * @since 1.2.16
+     * @return the current value of the <b>LocationInfo</b> option.
+     */
+    public boolean getLocationInfo() {
+	return locationInfo;
     }
-    
-    // remove from the buffer any events that were reported
-    buffer.removeAll(removes);
-    
-    // clear the buffer of reported events
-    removes.clear();
-  }
-
-
-  /** closes the appender before disposal */
-  public void finalize() {
-    close();
-  }
-
-
-  /**
-   * JDBCAppender requires a layout.
-   * */
-  public boolean requiresLayout() {
-    return true;
-  }
-
-
-  /**
-   *
-   */
-  public void setSql(String s) {
-    sqlStatement = s;
-    if (getLayout() == null) {
-        this.setLayout(new PatternLayout(s));
+
+    /**
+     * The <b>LocationInfo</b> option takes a boolean value. By default, it is set
+     * to false which means there will be no effort to extract the location
+     * information related to the event. As a result, the event that will be
+     * ultimately logged will likely to contain the wrong location information (if
+     * present in the log format).
+     * <p/>
+     * <p/>
+     * Location information extraction is comparatively very slow and should be
+     * avoided unless performance is not a concern.
+     * </p>
+     * 
+     * @since 1.2.16
+     * @param flag true if location information should be extracted.
+     */
+    public void setLocationInfo(final boolean flag) {
+	locationInfo = flag;
     }
-    else {
-        ((PatternLayout)getLayout()).setConversionPattern(s);
+
+    /**
+     * Adds the event to the buffer. When full the buffer is flushed.
+     */
+    public void append(LoggingEvent event) {
+	if (!isActive) {
+	    return;
+	}
+	event.getNDC();
+	event.getThreadName();
+	// Get a copy of this thread's MDC.
+	event.getMDCCopy();
+	if (locationInfo) {
+	    event.getLocationInformation();
+	}
+	event.getRenderedMessage();
+	event.getThrowableStrRep();
+	buffer.add(event);
+
+	if (buffer.size() >= bufferSize)
+	    flushBuffer();
     }
-  }
 
+    /**
+     * By default getLogStatement sends the event to the required Layout object. The
+     * layout will format the given pattern into a workable SQL string.
+     *
+     * Overriding this provides direct access to the LoggingEvent when constructing
+     * the logging statement.
+     *
+     */
+    protected String getLogStatement(LoggingEvent event) {
+	return getLayout().format(event);
+    }
 
-  /**
-   * Returns pre-formated statement eg: insert into LogTable (msg) values ("%m")
-   */
-  public String getSql() {
-    return sqlStatement;
-  }
+    /**
+     *
+     * Override this to provide an alertnate method of getting connections (such as
+     * caching). One method to fix this is to open connections at the start of
+     * flushBuffer() and close them at the end. I use a connection pool outside of
+     * JDBCAppender which is accessed in an override of this method.
+     */
+    protected void execute(String sql) throws SQLException {
+
+	Connection con = null;
+	Statement stmt = null;
+
+	try {
+	    con = getConnection();
+
+	    stmt = con.createStatement();
+	    stmt.executeUpdate(sql);
+	} finally {
+	    if (stmt != null) {
+		stmt.close();
+	    }
+	    closeConnection(con);
+	}
+
+	// System.out.println("Execute: " + sql);
+    }
 
+    /**
+     * Override this to return the connection to a pool, or to clean up the
+     * resource.
+     *
+     * The default behavior holds a single connection open until the appender is
+     * closed (typically when garbage collected).
+     */
+    protected void closeConnection(Connection con) {
+    }
 
-  public void setUser(String user) {
-    databaseUser = user;
-  }
+    /**
+     * Override this to link with your connection pooling system.
+     *
+     * By default this creates a single connection which is held open until the
+     * object is garbage collected.
+     */
+    protected Connection getConnection() throws SQLException {
+	if (!DriverManager.getDrivers().hasMoreElements())
+	    setDriver("sun.jdbc.odbc.JdbcOdbcDriver");
+
+	if (connection == null) {
+	    connection = DriverManager.getConnection(databaseURL, databaseUser, databasePassword);
+	}
+
+	return connection;
+    }
 
+    /**
+     * Closes the appender, flushing the buffer first then closing the default
+     * connection if it is open.
+     */
+    public void close() {
+	flushBuffer();
+
+	try {
+	    if (connection != null && !connection.isClosed())
+		connection.close();
+	} catch (SQLException e) {
+	    errorHandler.error("Error closing connection", e, ErrorCode.GENERIC_FAILURE);
+	}
+	this.closed = true;
+    }
 
-  public void setURL(String url) {
-    databaseURL = url;
-  }
+    /**
+     * loops through the buffer of LoggingEvents, gets a sql string from
+     * getLogStatement() and sends it to execute(). Errors are sent to the
+     * errorHandler.
+     *
+     * If a statement fails the LoggingEvent stays in the buffer!
+     */
+    public void flushBuffer() {
+	if (buffer.isEmpty()) {
+	    return;
+	}
+	flushBufferSecure();
+    }
 
+    private void flushBufferSecure() {
+	// Prepare events that we will store to the DB
+	ArrayList<LoggingEvent> removes = new ArrayList<LoggingEvent>(buffer);
+	buffer.removeAll(removes);
+	  
+	if (layout.getClass() != PatternLayout.class) {
+	    errorHandler.error("Failed to convert pattern " + layout + " to SQL", ILLEGAL_PATTERN_FOR_SECURE_EXEC,
+		    ErrorCode.MISSING_LAYOUT);
+	    return;
+	}
+	Connection con = null;
+	boolean useBatch = removes.size() > 1;
+	try {
+	    con = getConnection();
+	    PreparedStatement ps = con.prepareStatement(preparedStatementParser.getParameterizedSql());
+	    safelyInsertIntoDB(removes, useBatch, ps);
+	} catch (SQLException e) {
+	    errorHandler.error("Failed to append messages sql", e, ErrorCode.FLUSH_FAILURE);
+	} finally {
+	    if (con != null) {
+		closeConnection(con);
+	    }
+	}
+
+   }
+
+    private void safelyInsertIntoDB(ArrayList<LoggingEvent> removes, boolean useBatch, PreparedStatement ps)
+	    throws SQLException {
+	try {
+	    for (LoggingEvent logEvent : removes) {
+		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) {
+	    }
+	}
+    }
 
-  public void setPassword(String password) {
-    databasePassword = password;
-  }
+    /** closes the appender before disposal */
+    public void finalize() {
+	close();
+    }
 
+    /**
+     * JDBCAppender requires a layout.
+     */
+    public boolean requiresLayout() {
+	return true;
+    }
 
-  public void setBufferSize(int newBufferSize) {
-    bufferSize = newBufferSize;
-    buffer.ensureCapacity(bufferSize);
-    removes.ensureCapacity(bufferSize);
-  }
+    /**
+     *
+     */
+    public void setSql(String s) {
+	sqlStatement = s;
+	if (getLayout() == null) {
+	    this.setLayout(new PatternLayout(s));
+	} else {
+	    ((PatternLayout) getLayout()).setConversionPattern(s);
+	}
+    }
 
+    /**
+     * Returns pre-formated statement eg: insert into LogTable (msg) values ("%m")
+     */
+    public String getSql() {
+	return sqlStatement;
+    }
 
-  public String getUser() {
-    return databaseUser;
-  }
+    public void setUser(String user) {
+	databaseUser = user;
+    }
 
+    public void setURL(String url) {
+	databaseURL = url;
+    }
 
-  public String getURL() {
-    return databaseURL;
-  }
+    public void setPassword(String password) {
+	databasePassword = password;
+    }
 
+    public void setBufferSize(int newBufferSize) {
+	bufferSize = newBufferSize;
+	buffer.ensureCapacity(bufferSize);
+    }
 
-  public String getPassword() {
-    return databasePassword;
-  }
+    public String getUser() {
+	return databaseUser;
+    }
 
+    public String getURL() {
+	return databaseURL;
+    }
 
-  public int getBufferSize() {
-    return bufferSize;
-  }
+    public String getPassword() {
+	return databasePassword;
+    }
 
+    public int getBufferSize() {
+	return bufferSize;
+    }
 
-  /**
-   * Ensures that the given driver class has been loaded for sql connection
-   * creation.
-   */
-  public void setDriver(String driverClass) {
-    try {
-      Class.forName(driverClass);
-    } catch (Exception e) {
-      errorHandler.error("Failed to load driver", e,
-			 ErrorCode.GENERIC_FAILURE);
+    /**
+     * Ensures that the given driver class has been loaded for sql connection
+     * creation.
+     */
+    public void setDriver(String driverClass) {
+	try {
+	    Class.forName(driverClass);
+	} catch (Exception e) {
+	    errorHandler.error("Failed to load driver", e, 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 00000000..d6807dee
--- /dev/null
+++ b/src/main/java/org/apache/log4j/jdbc/JdbcPatternParser.java
@@ -0,0 +1,115 @@
+/*
+ * 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.Collections;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+class JdbcPatternParser {
+    private static final String QUESTION_MARK = "?";
+
+    private static final char PERCENT_CHAR = '%';
+
+    private final static Pattern STRING_LITERAL_PATTERN = Pattern.compile("'((?>[^']|'')+)'");
+    // NOTE: capturing group work seem to work just as well.
+    //private final static Pattern STRING_LITERAL_PATTERN = Pattern.compile("'(([^']|'')+)'");
+
+    private String parameterizedSql;
+    final private List<String> patternStringRepresentationList = new ArrayList<String>();
+    final private List<PatternConverter> args = new ArrayList<PatternConverter>();
+
+    JdbcPatternParser(String insertString) {
+	init(insertString);
+    }
+
+    public String getParameterizedSql() {
+	return parameterizedSql;
+    }
+
+    public List<String> getUnmodifiablePatternStringRepresentationList() {
+	return Collections.unmodifiableList(patternStringRepresentationList);
+    }
+
+    @Override
+    public String toString() {
+	return "JdbcPatternParser{sql=" + parameterizedSql + ",args=" + patternStringRepresentationList + "}";
+    }
+
+    /**
+     * Converts '....' literals into bind variables in JDBC.
+     */
+    private void init(String insertString) {
+	if (insertString == null) {
+	    throw new IllegalArgumentException("Null pattern");
+	}
+
+	Matcher m = STRING_LITERAL_PATTERN.matcher(insertString);
+	StringBuffer sb = new StringBuffer();
+	while (m.find()) {
+	    String matchedStr = m.group(1);
+	    if (matchedStr.indexOf(PERCENT_CHAR) == -1) {
+		replaceWithMatchedStr(m, sb);
+	    } else {
+		// Replace with bind
+		replaceWithBind(m, sb, matchedStr);
+	    }
+	}
+	m.appendTail(sb);
+	this.parameterizedSql = sb.toString();
+    }
+
+    private void replaceWithMatchedStr(Matcher m, StringBuffer sb) {
+	// Just literal, append it as is
+	m.appendReplacement(sb, "'$1'");
+    }
+
+    private void replaceWithBind(Matcher m, StringBuffer sb, String matchedStr) {
+	m.appendReplacement(sb, QUESTION_MARK);
+	// 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.
+	matchedStr = matchedStr.replaceAll("''", "'");
+	patternStringRepresentationList.add(matchedStr);
+	args.add(new PatternParser(matchedStr).parse());
+    }
+
+    public void setParameters(PreparedStatement ps, LoggingEvent logEvent) throws SQLException {
+	for (int i = 0; i < args.size(); i++) {
+	    final PatternConverter head = args.get(i);
+	    String value = buildValueStr(logEvent, head);
+	    ps.setString(i + 1, value);
+	}
+    }
+
+    private String buildValueStr(LoggingEvent logEvent, final PatternConverter head) {
+	StringBuffer buffer = new StringBuffer();
+	PatternConverter c = head;
+	while (c != null) {
+	    c.format(buffer, logEvent);
+	    c = c.next;
+	}
+	return buffer.toString();
+    }
+}
-- 
2.33.1